New Multiple Blog Feature - Site Aggregation

April 15, 2012 at 12:10 AMBen

A new feature named Site Aggregation is available in the BlogEngine.NET developer builds, and will of course be available in the next public release.  Site Aggregation is a feature related to Multiple Blog instances and this has been asked about by several people.

What is Site Aggregation?

You can designate up to 1 blog instance as being the Site Aggregation instance.  Typically it would be the primary blog instance.  When a blog instance becomes a Site Aggregation instance, the data displayed on the homepage, RSS feed and widgets contain blog posts from all the blog instances (see below for a complete list).  So the data is an aggregate of the blog posts across all instances.

This is useful for site visitors who will be able to visit the main blog, and see blog posts across all instances.  And if the RSS feed is subscribed to, that too aggregates blog posts across all instances.

What's Included

The main homepage, Posts by Tag page, and Posts by Date Range pages display the site aggregate data.  All the main widgets including the Tag Cloud, Recent Comments, Month List, Recent Posts, Calendar also take into account the site aggregate posts.  The Search results page, and RSS feed too.

For each post listed, clicking on the post title will take you to that post within that post's blog instance.  Each post usually also lists Tags and Categories tied to that post.  Clicking on a tag or category link will take you to the tag/category listing page for that post's blog instance (i.e. you will leave the site aggregate blog).

What's Not Included

Category based pages/widgets do not currently aggregate posts across all sites.  In particular, this includes the Category List widget and the Archive page.  These items still can be used on the site aggregate blog instance, but they will only display Posts for the site aggregate blog instance -- not for any of the non-site aggregate blogs.  The code needed to make these Category pages work with the site aggregated data is partially implemented in the Category core class (in particular, Category.AllBlogCategories and Category.ApplicableCategories), but some more work is needed for this.  It may not be until the following release where these category widgets/pages are site-aggregate capable.

The other item which is not included are Pages.  I think in most cases, it is preferable that Pages are not aggregated across all sites.

Configuring the Site Aggregate Blog Instance

It's very easy to designate a blog instance as being the site aggregate blog instance.  On the Blogs management page, there is a new "Is For Site Aggregation" property/checkbox you can check -- as shown below.

 

A Look at the Site Aggregate Blog Instance

In my test system, I have a primary blog instance, which is also the site aggregate blog instance, and 2 sub-blogs.  One sub-blog uses a virtual path, and the other sub-blog uses a different hostname.  Each blog instance has a post in it.  Here's what the homepage of the site aggregate blog instance looks like:

 

Similarly, here's a view of the RSS feed:

 

Site Aggregation Site - Includes "Local" Posts

The Site Aggregation site includes posts from other blog instances, as well as posts from its own instance (local posts).  This means you can create posts in the site aggregation instance as well, which is probably a handy feature for some.

Relative & Absolute Links

One issue that is accounted for is if you are using different hostnames for your blog instances.  For example, if your site aggregation instance is running at www.example.com and you have a sub-blog running at sub1.example.com, a lot of the links in BlogEngine.NET use relative paths, via RelativeLink and RelativeWebRoot.  A typical example is a hyperlink that you would find in PostView.ascx.

<a href="<%=Post.RelativeLink %>" class="taggedlink"><%=Server.HtmlEncode(Post.Title) %></a>

This URL in the HREF would end up being an invalid URL when the site aggregation site is displaying a post for sub1.example.com.  It would need to be an absolute link to "escape" out of the current hostname and get over to the other hostname (sub1.example.com).  The simple solution is to convert all these RelativeLink to AbsoluteLink.  A new property has been created off of IPublishable named RelativeOrAbsoluteLink.  This will return a relative link if it works, and if not, it will return an absolute link.  While an absolute link in theory always works, there are some advantages to using relative paths whenever possible.  So when using this new RelativeOrAbsoluteLink property, if the sub-blog's hostname matches the hostname of the site aggregation blog instance, you'll get a relative link, otherwise an absolute link.  Because the minority of BlogEngine.NET users won't be this situation where they are (a) using multiple blogs, (b) using the Site Aggregation feature, and (c) using different hostnames, RelativeOrAbsoluteLink will basically preserve backwards compatibility for everyone else not in this situation.

On this note, if you are using a custom theme, you may need to change RelativeLink in the PostView.ascx page to RelativeOrAbsoluteLink -- if you are in this situation.

A similar new property has been created in the Utils class, named RelativeOrAbsoluteWebRoot.  Just like RelativeOrAbsoluteLink, RelativeOrAbsoluteWebRoot will return the RelativeWebRoot if it works, otherwise it will return AbsoluteWebRoot.

AllBlogPosts and ApplicableBlogPosts

These are 2 new properties in the Post class, which are used to support this site aggregation feature.  AllBlogPosts is a list of posts across all blog instances.  It's very similar to Post.Posts.  Post.Posts is just for the current blog instance, where Post.AllBlogPosts is posts across all instances.  The only real challenge here is the blog posts for a particular blog instance are not loaded into memory until the first time that blog instance is hit.  If the site aggregation blog is hit before some of the sub-blogs have been hit, the data (Posts) for those sub-blogs will not yet be loaded in memory.  The AllBlogPosts property takes care of this by loading the posts of these sub-blogs into memory that have not yet had their first hit.

ApplicableBlogPosts is a shortcut property that either returns AllBlogPosts or Posts depending on which one is needed.

The Category class has similar properties that have been added to it -- AllBlogCategories and ApplicableBlogCategories.  These properties will be useful when the Archive page and Category based widgets such as the Category widget become site aggregate data capable (as mentioned above, these items are not yet site aggregate capable).

Blog and BlogId added to BusinessBase

The last notable change for this feature is the BusinessBase class now has Blog and BlogId properties.  This means objects such as Post, Category, BlogRollItem  (and anything else inheriting from BusinessBase) are now aware of which blog instance they belong to.  This has a lot of potential uses.  The main purpose for this at this point is properties such as RelativeLink (as in Post.RelativeLink) now take into account its blog instance's virtual path and hostname.  So when working with Post.AllBlogPosts which contains blog posts across all the blog instances, calling Post.RelativeLink or Post.AbsoluteLink on any of the posts in this AllBlogPosts collection will return the correct path.  As noted above, it's not safe to use Post.RelativeLink especially when working with Post.AllBlogPosts data.  Instead, Post.RelativeOrAbsoluteLink or just Post.AbsoluteLink are the safer choices.

Give it a Try

If you think this new Site Aggregation feature may be of use to you, I encourage you to try it out, either now or once the beta for the next version of BlogEngine.NET comes out.  The best place to bring up any issues or suggestions regarding this feature is in the CodePlex discussions group.

Introducing Multiple Blogs in Single Instance for BlogEngine.NET

June 19, 2011 at 1:00 PMBen

The much requested and highly anticipated "multiple blogs" feature has made it into BlogEngine.NET 2.5.  BE.NET 2.5 will be released at the end of June.  This post goes over how it works and some information on making themes, widgets, extensions and your code be multiple-blog compatible.

Blogs Management Page

Each installation of BE.NET will have a "primary" blog instance.  By default this will be the normal blog you are accustomed to.  The primary blog has a new "Blogs" admin page.  That page looks like:

Here the blog instances (the primary blog instance and child blogs) can be managed.  A blog instance can be made Inactive.  An inactive blog instance will appear as if it does not exist.  Other properties that can be configured for each blog instance can be seen in the following "Add New Blog" dialog window:

Let's look at the properties for each blog instance.

Name

Any name you'd like to give the blog instance.  The name does not appear anywhere outside this Blogs management page.

Storage Container Name

This is the name of the folder where the blog data will live.  Basically a copy of the files and folders for each blog instance will be made -- i.e. each blog instance will have its own separate folder of data.  This makes the blog instances portable and keeps the data separated.  Databases can also be used with multiple blogs.  Even when using a database, there is still some data (blog post images/files, and a few other small files) that is stored in the file system.  So a separate folder will be created for each blog instance even when using the DbBlogProvider.

As in the past, the primary blog instance data is stored in App_Data.  There's now a new Blogs folder under App_Data.  Within App_Data\Blogs, a new folder matching the "Storage Container Name" will be created when a new blog instance is created.  The data that is initially put into this child blog storage folder is explained next.

Existing Blog to Create New Blog from

This is partially related to the above Storage Container Name.  When a new blog instance is created, the "Existing Blog to Create New Blog From" dropdown list will contain all of the existing blog instances.  The files & folders from the existing blog instance you choose here will be copied into the storage folder for the new blog instance as "initial data".  BE.NET 2.5 also includes a "Template" blog instance (which is inactive), and can be used as the existing blog instance to create new blog instances from.  The Template blog instance has the same data and settings as the primary blog.  You can optionally change the Template blog data, or even create multiple template blog instances to choose from when creating new blog instances.

If using a database, in addition to the storage container folder being copied for the new blog instance, the DB data from the existing blog instance will be copied in the DB for the new blog instance.  So for both XML and DB, the new blog instance created will start off using the exact data, files, etc that the blog instance you copied from has.

Note, there is no Template blog included with the database.  You can still create your own template blog for the DB by creating a new blog instance of your Primary blog, naming it "Template" (or any name) and customizing it to your liking.

Host Name & Virtual Path

These two properties along with the "Accept Any Text before Host Name" checkbox are what is used for BE.NET to determine which blog instance a request to the application is being made for.  Virtual Path refers to the path following the root.  Host name is the domain name (plus any subdomain).  You can mix and match Hostname and Virtual Path to create many URL combinations.  For example, the following URLs can be used:

http://www.example.com
http://www.example.com/blog1
http://blog1.example.com
http://blog2.domain.com
http://blog.domain.com/blog2
... etc ...

The 2nd and 5th examples above would be given a Virtual Path of ~/blog1 and ~/blog2 respectively.  Prefixing the virtual path with ~/ is required, and the ~/ will automatically be inserted for you in the Virtual Path field.  In the 3rd example above, the Host Name would be "blog1.example.com", in the 4th example the host name would be "blog2.domain.com" and in the 5th example, the host name would be "blog.domain.com".

If you are only using a single hostname for your blog, you can omit the hostname.  Entering it is really only required if you will be using multiple hostnames.

With the Virtual Paths above of blog1 and blog2 (examples 2 and 5), you do not need to create physical directories named blog1 and blog2.  These are virtual directories that BE.NET will look for in the URL to treat that request to the web server as being on behalf of those blog instances.  At the same time, you will want to make sure that physical directories matching the virtual paths (blog1, blog2, etc) do not already exist.

The "Accept Any Text before Host Name" checkbox is another option to allow any text (subdomains) to appear before the Host Name you enter.  If you leave this box unchecked, then the Host Name you enter will need to match exactly what is in the browser address bar.

Multiple-Blog Capable Themes, Widgets and Extensions

If you will be running a single instance blog, all of the themes, extensions and widgets that run under BE.NET 2.0 will still work perfectly fine for BE.NET 2.5.

If you will be running multiple blog instances, existing themes, extensions and widgets may need some adjustments to work with this new multiple blogs feature.  The following types of issues need to be addressed.

Caching - HttpRuntime.Cache

If an existing theme, widget or extension is storing data in the HttpRuntime.Cache, a likely needed change is to store that data separately for each blog instance.  With multiple blog instances, the same set of (physical) themes, extensions and widgets are being used across all the blog instances.  So if you have code in a widget that looks like:

HttpRuntime.Cache["recent-posts"] = recentPostList;

This same data you are storing will be shared across all blog instances that are using this widget.  This is probably not what was intended.  You can either change that code so it uses a dynamic cache key based on the "current blog instance", or as a convenient shortcut, you can use a new Caching provider built into BE.NET 2.5.  That line of code above using the new caching provider would look like:

Blog.CurrentInstance.Cache["recent-posts"] = recentPostList;

The cache provider will use your "recent-posts" cache key, prefixing it with the ID of the current blog instance (a GUID).  You can retrieve the current blog instance and/or the ID of the current blog instance via:

Blog currentInstance = Blog.CurrentInstance;
Guid currentInstanceId = Blog.CurrentInstance.Id;

Caching - Static

If your theme, widget or extension is storing data in static fields or properties, the cache provider described above cannot be used, however one approach is to convert your static data into a generic dictionary, with Guid as the key and your data type as the value.  The Guid key will represent the current blog instance ID.  You need to manage this dictionary by adding in each blog instance in the dictionary, ideally as the data needs to be stored the first time.  That code might look like:

private static Dictionary<Guid, string> _recentPostsMarkup =
    new Dictionary<Guid, string>();

private static string RecentPostsMarkup
{
	get
	{
		if (!_recentPostsMarkup.ContainsKey(Blog.CurrentInstance.Id))
		{
			_recentPostsMarkup[Blog.CurrentInstance.Id] = GetRecentPostsMarkup();
		}

		return _recentPostsMarkup[Blog.CurrentInstance.Id];
	}
}

URL Paths

This is important and a problem you may notice, especially if you are using Virtual Paths for your blog instances.  Let's look at a simple line of code in BE.NET 2.0 that worked fine there:

Response.Redirect("~/Account/login.aspx");

Simple enough.  However, if you're using Virtual Paths and you have a blog instance with a Virtual Path of "blog1", the homepage for that blog instance will be something like:

http://www.example.com/blog1

If you have code that uses that redirect shown above, you will end up at:

http://www.example.com/Account/login.aspx

When you probably wanted to end up at:

http://www.example.com/blog1/Account/login.aspx

So instead of redirecting to the login page for "blog1", you've incorrectly redirected the person to the login page for the primary blog instance.  That redirect should now look like the following to work well with multiple-blogs:

Response.Redirect(Utils.RelativeWebRoot + "Account/login.aspx");

In the past, Utils.RelativeWebRoot would resolve to the root of the blog.  Starting with BE.NET 2.5, Utils.RelativeWebRoot will resolve to the homepage/root of the current blog instance.  So when you are in "blog1", Utils.RelativeWebRoot will resolve to:

/blog1/

Similarly, Utils.AbsoluteWebRoot also takes the relative path of the current blog instance into consideration.

If you need the "real" web root of the application, you could use ~/ or you can use a new Utils.ApplicationRelativeWebRoot property.

While on the topic, a couple of other similar new properties are Blog.CurrentInstance.StorageLocation which will return a storage location beginning with ~/ to the physical location of the storage container for the current blog instance.  So Blog.CurrentInstance.StorageLocation may return a value looking like:

~/App_Data/Blogs/blog1/

BlogConfig.StorageLocation will return the same type of path, but to the direct App_Data folder (which equates to the storage location of the primary blog instance), so BlogConfig.StorageLocation will generally return:

~/App_Data/

Extensions

With extensions, the primary blog instance has the "master" switch over whether extensions are enabled.  If the primary blog instance disables an extension, that extension is no longer available to any of the child blogs.  If the primary blog instance enables an extension, by default this extension will be enabled for all the child blogs.  A child blog can disable an extension.

As long as the primary blog instance has an extension enabled, that extension will be loaded into memory and if it wires up any of the normal event handlers (Post.Serving, Commnet.Serving, etc), that extension is going to fire even if the event is occurring in a child blog that has explicilty disabled the extension.  In this scenario, the extension is handling an event for the child blog, but it should not do anything since the child blog disabled the extension.  In other words, we want the extension to "pretend" it was never called, or at least not take any action.  This is where a change is required in extensions so they check to see if the blog instance the extension is firing for has the extension enabled or disabled.  If it is disabled, then the extension should not run any of it's normal code.  The extensions included with BE.NET 2.5 have a new line of code added to them for this.  Taking the ResolveLinks extensions as an example, it has this new line of code (technically 2 lines of code) added to the beginning of PostCommentServing:

private static void PostCommentServing(object sender, ServingEventArgs e)
{
	if (!ExtensionManager.ExtensionEnabled("ResolveLinks"))
		return;

	if (string.IsNullOrEmpty(e.Body))
		return;

	e.Body = LinkRegex.Replace(e.Body, new MatchEvaluator(Evaluator));
}

The new lines of code above are lines 3 and 4.  It makes a call into the Extension Manager, passing to it the name of the extension (in this case, ResolveLinks).  ExtensionEnabled() will return false if the extension is disabled for the current blog instance.

In Summary

As mentioned earlier, if you will be running a single instance of BE.NET, these changes described above will not need to be made -- although they can be made.  Overall, I think we maintained a lot of backwards compatibility as the changes you will need to make to have a multiple-blog capable BE.NET application are very minimal.  The themes, widgets and extensions included with BE.NET 2.5 already have these changes  made to them.  Enjoy the new multiple blog capability and if you run into any issues or have any suggestions, the best place to pass along your thoughts is in the CodePlex discussion group.

CSS & JavaScript Injection Bookmarklets

January 30, 2010 at 11:34 PMBen

I like using bookmarklets to inject scripts into a page I’m on.  One of the popular ones for this is the jQueryify bookmarklet to inject jQuery into a page.

After getting the jQuerify bookmarklet, I created a separate bookmarklet to inject my own JavaScript file into a document.  This is handy when I’m debugging a website I either don’t have the files for, or just want to test changes out within the confines of my own browser without touching any of the live files.  To do this, I simply Edit the bookmark in my browser, and change the URL embedded in the bookmarklet to point to the URL of the JavaScript file I want to inject into the page.  The URL to the script can even be a script on your own computer accessible via http://localhost, if you’re too lazy to upload the script to a public website :)

Recently, I needed to inject a CSS stylesheet into the page I’m on.  The bookmarklet for this is very similar to the JavaScript injection bookmarklet.  The bookmarklet I created for this is at the bottom of this post.

Dynamic URL Bookmarklets

Even though it’s not a big hassle to Edit the CSS/JS injection bookmarks to change the URL to the JS or CSS file embedded within the bookmarklet, I realized a very convenient bookmarklet would be one that would prompt me for the URL to the CSS/JS file, and then inject that URL.  By doing this, I don’t need to edit the bookmark, and can easily inject any CSS or JS file.  It’s also easy to inject multiple URLs by running the bookmark more than once, and entering a different URL each time.

Bookmarklets – For your Browser

For convenience’s sake, I have these 4 bookmarklets down below.  Feel free to use them.  Two of the bookmarklets are for JS injections and two are for CSS injections.  Within each pair, one bookmarklet has the URL already embedded within it, and the other one will prompt you for the URL.

Just drag these links into your Bookmarks toolbar or menu area.  For reference, the bookmarklet code is under each link.

> Inject JS file <
javascript:(function(){var%20s=document.createElement('script');s.setAttribute('src','http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js');document.getElementsByTagName('body')[0].appendChild(s);alert('Script%20injected!');})();

> Inject JS file (get prompted) <
javascript:(function(){var%20sUrl=prompt('Enter%20URL%20to%20JavaScript%20file');if(sUrl){var%20s=document.createElement('script');s.setAttribute('src',sUrl);document.getElementsByTagName('body')[0].appendChild(s);alert('Script%20injected!');}})();

> Inject CSS file <
javascript:(function(){var%20s=document.createElement('link');s.setAttribute('href','http://l.yimg.com/a/lib/arc/core_1.0.5.css');s.setAttribute('rel','stylesheet');s.setAttribute('type','text/css');document.getElementsByTagName('head')[0].appendChild(s);alert('Stylesheet%20injected!');})();

> Inject CSS file (get prompted) <
javascript:(function(){var%20sUrl=prompt('Enter%20URL%20to%20Stylesheet');if(sUrl){var%20s=document.createElement('link');s.setAttribute('href',sUrl);s.setAttribute('rel','stylesheet');s.setAttribute('type','text/css');document.getElementsByTagName('head')[0].appendChild(s);alert('Stylesheet%20injected!');}})();

Posted in: Development

Tags: , ,

Giving Comment Spammers Less Incentive to Spam You

December 8, 2009 at 10:01 PMBen

The latest check-in of BE.NET (1.5.1.36) has a small, but important change.  The three themes included with BE.NET now include rel=”nofollow” on the links of commenter’s websites.

This is a theme specific change.  So if you’re using a custom theme, and even if you upgrade to the latest build of BE.NET, there’s a good chance you might not have the NOFOLLOW instruction on these links.  It can simply be added in the CommentView.ascx file in your theme’s blog folder.

As I’m using a custom theme myself, I just added NOFOLLOW to this blog too.  Wikipedia has a good writeup on NOFOLLOW, in case you aren’t familiar with its purpose.  I’m a little surprised it’s taken this long to get NOFOLLOW into the themes that are included with BE.NET.  Better late than never!

I get a lot of comment spam on this blog.  As I’m moderating comments, it ends up never showing up since I don’t approve any of it (TIP to spammers, stop wasting your time!).

Comment spammers are a lot more likely to leave comments on blogs that do not include NOFOLLOW.  Yes, I’m sure a lot of the spammers actually look at these types of details when scoping out blogs to attack.

Incidentally, the ResolveLinks extension that comes with BE.NET already includes the NOFOLLOW instructions.  This is the extension that will convert URLs in comments into hyperlinks.  If the extension finds a URL like www.google.com in the comment content, it will convert that into:

<a href="http://www.google.com" rel="nofollow">www.google.com</a>

This conversion is done as the comment is being served.

I’m anxious to see what type of impact adding NOFOLLOW will have on my level of comment spam.
Fingers crossed ...

App_offline.htm – Page Not Found

September 5, 2009 at 6:11 PMBen

For a few years, .NET has had the built-in capability to easily take your entire application offline when you need to make an update or perform some maintenance on your site.

By simply putting a file named app_offline.htm in the root directory of your site, ASP.NET will serve the app_offline.htm file, instead of the requested page.

I recently employed this feature for probably the first time.  I put the app_offline.htm file on the site, and pulled up my site in Firefox.  The contents of app_offline.htm displayed as expected.

However, if I were to pull my site in Chrome or IE, I would get a Page Not Found error that appeared as though my entire site did not exist.

App_offline.htm result in IE8:

App_offline.htm in IE

App_offline.htm result in Chrome: 

App_offline in Chrome

As mentioned above, in Firefox, the contents of App_offline.htm would display as expected.

The problem is that when ASP.NET serves the App_offline.htm file, the HTTP Response code it passes out is 404.  Chrome will display the page shown above for 404 errors.  In IE, you can actually avoid that generic error page shown above if you turn off HTTP Friendly errors.

But I obviously cannot expect IE visitors to my site to have HTTP Friendly errors turned off.

The way ASP.NET has implemented app_offline.htm by passing out a 404 HTTP status code is not well designed, in my opinion.  A much better implementation would be for ASP.NET to return a normal 200 HTTP status code.

To accomplish this, for this site, I created a simple HTTP Module that processed the beginning of each request.  It checks an “offline” appSetting in web.config to see if the application should be offline.  If the offline setting is turned on, the module will do a server transfer to my own app offline HTML file.

One thing I found on an IIS7 server is requests for items such as JPG, GIF, CSS files, etc. will also go through this HTTP module.  This is normally a great benefit of IIS7’s integrated mode pipeline.  However, if the application offline HTML file includes an IMG tag for an image on the same site, or a link to a CSS file on the same site, the HTTP module is also going to do a server transfer for these other files (JPG, CSS, etc).  This will result in the image not displaying on the application offline page, or the CSS file not loading in the browser, etc.

A simple filter in the HTTP module to only do a server transfer for actual pages is all that is required.  The fairly simple HTTP Module I ended up creating is below.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Configuration;
using System.IO;

public class AppOffline : IHttpModule
{
    public void Dispose()
    {
    }

    public void Init(HttpApplication context)
    {
        context.BeginRequest += new EventHandler(context_BeginRequest);
    }

    void context_BeginRequest(object sender, EventArgs e)
    {
        HttpContext context = ((HttpApplication)sender).Context;

        if (ConfigurationManager.AppSettings["offline"] == "true")
        {
            string extension = Path.GetExtension(context.Request.Path);

            // Don't server transfer for extensions like .JPG, .CSS, etc.
            string targetedExtensions = ".aspx.ashx.asmx";
            if (targetedExtensions.IndexOf(extension, StringComparison.OrdinalIgnoreCase) == -1)
                return;
            
            context.Server.Transfer("~/application_offline.html");
        }
    }
}

It’s a pretty simple, but effective HTTP module.  When the server transfer is done to my own application offline HTML file, the HTTP status code returned to the client is 200.  No more Page Not Found problems with browsers like IE and Chrome.

Posted in: Development

Tags: ,

Performance: Compiled vs. Interpreted Regular Expressions

August 6, 2009 at 10:33 PMBen

When a regular expression in .NET will be used multiple times, it’s common to create that Regex with the Compiled flag, e.g. RegexOptions.Compiled.  Compiled regexp’s take a bit more time to create initially, but will run faster than a regexp created without the Compiled flag.  At least that’s what the documentation states!

Without the Compiled flag, your regexp will be interpreted.  There’s even "precompiled” regular expressions.  You need to compile these regular expressions into an assembly before runtime.  This might be a good option if you have constant regexps that don’t change.  If your regexps are subject to change, pre-compiled is not a good option.  These three types of regexp’s (interpreted, compiled and pre-compiled) are explained with a few more technical details in this somewhat dated MS blog article.

Theory is great, but real benchmarks are more meaningful.  I’ve assembled some code that benchmarks the difference in speed it takes to create and run 5,000 regular expressions.  There’s actually a big difference in the time taken to run a compiled regular expression the first time, versus subsequent times.  So the results shown here will include the first run time as well as the subsequent run times.

Here’s some code to get us started:

    private static List<Regex> _expressions;
    private static object _SyncRoot = new object();

    private static List<Regex> GetExpressions()
    {
        if (_expressions != null)
            return _expressions;

        lock (_SyncRoot)
        {
            if (_expressions == null)
            {
                DateTime startTime = DateTime.Now;

                List<Regex> tempExpressions = new List<Regex>();
                string regExPattern =
                    @"^[a-zA-Z0-9]+[a-zA-Z0-9._%-]*@{0}$";

                for (int i = 0; i < 5000; i++)
                {
                    tempExpressions.Add(new Regex(
                        string.Format(regExPattern,
                        Regex.Escape("domain" + i.ToString() + "." +
                        (i % 3 == 0 ? ".com" : ".net"))),
                        RegexOptions.IgnoreCase | RegexOptions.Compiled));
                }

                _expressions = new List<Regex>(tempExpressions);
                DateTime endTime = DateTime.Now;
                double msTaken = endTime.Subtract(startTime).TotalMilliseconds;
            }
        }

        return _expressions;
    }

We’re storing 5,000 regular expressions in a static list.  Notice the RegexOptions.Compiled flag is being used.  The regexp’s are just looking for email addresses with specific domain names – domain1.net, domain2.net, domain3.com, etc.  Not very useful, but I just wanted the regexp’s to vary.  You can see we’re also recording the number of milliseconds taken to create the regular expressions.  Now here’s the code that calls GetExpressions() and actually invokes the IsMatch function on each regexp.

    private static void CheckForMatches(string text)
    {
        List<Regex> expressions = GetExpressions();
        DateTime startTime = DateTime.Now;

        foreach (Regex e in expressions)
        {
            bool isMatch = e.IsMatch(text);
        }

        DateTime endTime = DateTime.Now;
        double msTaken = endTime.Subtract(startTime).TotalMilliseconds;
    }

And we call CheckForMatches like so:

    CheckForMatches("some random text with email address, [email protected]");

How much time does it take to create and run these 5,000 compiled expressions?  Here’s what I get:

Compiled Regular Expressions
CREATION TIME: 1662 ms
FIRST RUN TIME: 25137 ms
SUBSEQUENT RUN TIMES: 41 ms

Subsequent runs of all 5,000 expressions is very fast.  However, look how much time it takes the first time these 5,000 expressions are run in CheckForMatches() – 25 seconds!!!

Let’s make ONE change.  Remove the RegexOptions.Compiled flag.  By doing this, our regular expressions will be interpreted.  Here’s what we get:

Interpreted Regular Expressions
CREATION TIME:
493 ms
FIRST RUN TIME: 22 ms
SUBSEQUENT RUN TIMES: 20 ms

Interpreted regexp’s beat compiled in every category!  Running these tests several times produces similar results.  The BIG difference here is obviously the First Run Time.  25 seconds versus .022 seconds.

I’ve seen some benchmarks showing static regexp’s performing a little slower than instance regexp’s.  I ran the same tests without the static modifier on the fields and methods above.  Same results – using the Compiled flag takes around 25 seconds for the regular expressions to run the first time.  Without the Compiled flag, they run in hundredths of a second.

Clearly, interpreted regexps are the winner.  Granted, if you’re only dealing with a small number of regular expressions, and you use the compiled flag, the first run time isn’t going to be anywhere near what I’ve shown here with 5,000 regexps.  However, even with just a few regular expressions, in .NET, you’ll see me sticking with interpreted regular expressions!

Logging & Improved Error Reporting in BlogEngine

June 13, 2009 at 6:26 PMBen

There’s two new features in the latest build of BE, 1.5.1.11.  These features, Logging and Improved Error Reporting, are separate but related features.  I think both features will turn out to be very helpful – especially when trying to diagnose a problem.  I’ll explain both features and how they can be used independently of each other, as well as with each other.

Logging

There’s a new event handler in BE that any extension or other component can subscribe to: Utils.OnLog.  It can be subscribed to in an extension, like:

Utils.OnLog += new EventHandler<EventArgs>(OnLog);

In this case, there’s an OnLog event handler that will fire every time any piece of code in BE.NET logs a message.  I created a simple Logger extension that is now included in BE 1.5.1.11 that subscribes to log notifications, and writes the log message to a logger.txt file in the App_Data folder.  Anyone can write a similar extension that will save log messages to a database.  The code for the this new Logger extension can be viewed below.

#region using

using System;
using BlogEngine.Core;
using BlogEngine.Core.Web.Controls;
using System.IO;
using System.Text;

#endregion

/// <summary>
/// Subscribes to Log events and records the events in a file.
/// </summary>
[Extension("Subscribes to Log events and records the events in a file.", "1.0", "BlogEngine.NET")]
public class Logger
{
    static Logger()
    {
        Utils.OnLog += new EventHandler<EventArgs>(OnLog);
    }

    /// <summary>
    /// The event handler that is triggered every time there is a log notification.
    /// </summary>
    private static void OnLog(object sender, EventArgs e)
    {
        if (sender == null || !(sender is string))
            return;

        string logMsg = (string)sender;

        if (string.IsNullOrEmpty(logMsg))
            return;

        string file = GetFileName();

        StringBuilder sb = new StringBuilder();

        lock (_SyncRoot)
        {
            try
            {
                using (FileStream fs = new FileStream(file, FileMode.Append))
                {
                    using (StreamWriter sw = new StreamWriter(fs))
                    {
                        sw.WriteLine(@"*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*");
                        sw.WriteLine("Date: " + DateTime.Now.ToString());
                        sw.WriteLine("Contents Below");
                        sw.WriteLine(logMsg);

                        sw.Close();
                        fs.Close();
                    }
                }
            }
            catch
            {
                // Absorb the error.
            }
        }
    }

    private static string _FileName;
    private static object _SyncRoot = new object();

    private static string GetFileName()
    { 
        if (_FileName != null)
            return _FileName;

        _FileName = System.Web.Hosting.HostingEnvironment.MapPath(Path.Combine(BlogSettings.Instance.StorageLocation, "logger.txt"));
        return _FileName;
    }
}


Any code within BE.NET, a widget, extension, etc. can now log any message like:

Utils.Log("some message to log");


If more than one event handler is subscribed to the OnLog notifications, each event handler will of course fire.  It’s worth noting that Utils.Log() accepts a parameter of type object.  The Logger extension is designed to receive messages of a string type (it actually casts the object type parameter to a string type).  If Logger receives a non-string type, it doesn’t log the message – because the Logger extension is designed to receive simple string-based messages.  If an extension or other piece of code wants to pass a non-string type message to a logger, a different extension could be created that is equipped to handle log messages that are of a type different than string.

If you don’t want logging to take place, either because you don’t want to worry about having a log file that keeps growing, or because you prefer not to store data in the App_Data folder (as the Logger extension does), you can simply disable the Logger extension on the Extensions tab in the control panel.  As of right now, even if you leave the Logger extension enabled, there’s going to be virtually no messages logged as there isn’t any code that currently calls Utils.Log().

This new logging feature is going to help keep track of events going on.  Although logging can be used for many purposes, one of the reasons I wanted to have this in BE.NET was to be able to record unhandled errors.

Error Handling & Reporting

There’s now an event handler in the Global.asax file that catches all unhandled exceptions.  Up till now, if a 500 server error occurred, you would be redirected to error404.aspx, as defined by the <customErrors> tag in the web.config file.  While this is a nice catch-all method to handling errors, people just getting started with BE.NET are often confused why they are seeing a “Page cannot be found” message when they are trying to do something like save changes and an unhandled error occurs -- which they don’t know has occurred when all they see is a “Page cannot be found” message.  To be fair, most people just getting started with BE.NET are not getting errors.  But some do, and adjusting folder permissions or other settings fixes the errors they are seeing.  In order to fix the problem, one must first know an error is actually occurring, and they need to know what the actual error is!

The new Application_Error event handler in Global.asax does up to three things for non-404 errors:

  1. It generates a summary, including details, of the unhandled error that just occurred.
  2. If error logging is turned on (a new option, explained below), it makes a call to Utils.Log(), passing the error summary to any event handlers registered to receive log notifications.
  3. It does a Server.Transfer() to a new error.aspx page in the root of the BE.NET web folder.

For #1, the summary generated includes a stack trace, inner exceptions, and the URL the page occurred on (both Request.Url and Request.RawUrl).

The summary that is generated is stored in the Items collection of HttpContext.Current.  Why?  When Application_Error in Global.asax does a Server.Transfer to the new error.aspx page, the error.aspx page checks to see if the person is logged in (i.e. if they’re authenticated).  If they are logged in, error.aspx will display the error summary generated within Global.asax.  The error summary is retrieved out of the HttpContext.Current.Items collection.  If the person isn’t logged in, they just see a message similar to error404.aspx, indicating an “unexpected error has occurred”, and the developer will be tortured, blah blah. :-)

Displaying the error details directly on error.aspx for logged in users is helpful for two reasons.  (a) Immediate knowledge of the error, and (b) even if the new error logging option is turned off, you still can see the error message in your browser when you’re transferred to error.aspx.

I’ve mentioned this new error logging option twice now.  On the Settings tab in the control panel, in the Advanced Settings section, there is a new checkbox labeled “Enable error logging”.  By default, it’s turned off.  While this is turned off, the only notifications the Logger extension will receive (if you leave the Logger extension enabled) will be messages coming from some piece of code that explicitly makes a call to Utils.Log().  If you turn this new Enable Error Logging feature on, then when an unhandled exception occurs, Global.asax will pass the error details to Utils.Log() for any registered event handlers to deal with.

Result & Extensibility Possibilities

From version 1.5.1.11, any extension, widget or even BE.NET itself can now simply make a call to Utils.Log() to have any message logged.  Logging can be done with the built-in Logger extension, or messages can be logged to a database, sent via email, etc. by any other extension someone creates.  I’m also very excited about the new error handling mechanism which will give administrators a lot more information about errors that may be occurring in their blog.

Please download and test out the latest build.  If any problems show up or you have any ideas for improvements, you can leave a comment here, or post a message on the CodePlex discussion boards.

Web Farm Extension 1.0

May 10, 2009 at 12:13 AMBen

The caching of data in BlogEngine.NET becomes a problem when BE.NET is installed in a web farm.  When you add, edit or delete a post, that change is occurring on one machine within the farm as well as within the data store (App_Data or DB).  But the other servers within the farm are unaware there’s been a change in data.  Not until the data loaded in memory on these other servers clears out anywhere from minutes to hours to days later, the old set of data will continue to be shown to visitors hitting one of these other servers.

I created a WebFarm extension which may help some people in this situation.  I haven’t worked much with web farms, so I’m not sure how well this extension will work.  Any feedback is appreciated.  I was able to test this extension on Vista/IIS7 where I had two separate web applications pointing to the same physical BE.NET location on my machine.  Even in this situation, creating a new post within one application would normally result in the post NOT showing up in the other application.  This new Web Farm extension did solve the caching problem for this scenario.  I’m hoping this success will carry over to a Web Farm scenario.

There’s two files in the ZIP file download.  WebFarm.cs should go in the App_Code\Extensions folder.  The other file, webfarm_data_update_listener.ashx, should go in the root of your blog.

Once those files are in their correct locations, if you go to the Extensions tab in the control panel, you’ll want to click ‘Edit’ for this new WebFarm extension.

WebFarm Extension

I wanted the help box on the right side to include as much information as possible.  But as you can see, the Extensions page doesn’t currently handle long description boxes very well :)

The idea behind this extension is that if each server within the farm has a unique internal Ip address, this extension can notify each server that a change in data has occurred.  Not knowing a lot about web farms, this is the part I’m unsure is possible.  But it does some reasonable each server would have its own unique IP address.  If you’re using host headers, this extension may not work for you – unless you have a unique URL to each server within the farm.

The extension currently notifies the servers in the web farm when a new post or page has been created, updated or deleted.  Other data such as Settings, Profiles, Categories, Comments, etc. is not handled by this extension.  Or at least not in this version.

The webfarm_data_update_listener.ashx file you placed in the root of the blog is the handler that receives notifications when a change in Post or Pages has occurred.  The data passed to the handler includes the type of data changed (Post, Page), the type of change (Insert, Update, Deletion) and the ID of the Post or Page that has been inserted/updated/deleted.  Rather than the handler re-loading all the Post/Page data, it will insert, update or delete just the one piece of data that changed.  This is more efficient than re-loading all the data which could be taxing for those with a lot of blog data.

As described in the help area when adding the server Ip/Urls in the WebFarm extension, make sure the “Shared Key” you enter matches the key in the webfarm_data_update_listener.ashx handler file.  The default key in the handler is “blogengine”.  For security purposes, you may change the key in the ASHX handler.  But be sure the ASHX key matches the key you enter for each Ip/Url.

Also, because of this same data caching issue in web farms, after you enter all the web farm server Ip addresses into the WebFarm extension, you’ll want to re-start BE.NET so the extension data you just entered is detected by all the servers in your farm.  Updating the web.config file with any meaningless change will accomplish re-starting the blog application.  This is just a one-time requirement so all servers have the list of servers they need to notify when a post or page change has occurred.

I realize this extension has its limitations and isn’t a comprehensive solution to undesired caching in a web farm scenario.  But it does handle propagating changes in posts across the servers in the farm – one of the more important areas.  This extension is also easy to get started with in contrast to making various changes within BE.NET itself.  Again, any feedback on this extension is appreciated.

Download: WebFarmExtension_1.0.zip (3.26 kb)

Multiple WidgetZones in BE.NET

April 18, 2009 at 2:35 PMBen

One of the commonly asked for features in BlogEngine.NET has just made it into the codebase.  This feature is Multiple WidgetZones.

To use multiple widget zones in your BE.NET blog, there's not too much you need to do.  I'll explain how it works and a couple of minor changes you may need to make to your blog -- even if you won't be using more than one widgetzone.

Stylesheet Changes

When there was only one widgetzone, the widgetzone <div> and the widget selector dropdown list had a fixed ID on their HTML elements.  So, there was these two elements:

<div id="widgetzone">
<select id="widgetselector">

Because there can now be multiple widget zones and multiple widget selectors, and because there cannot legally be more than one element with the same ID, the widgetzone <div> now uses "widgetzone" for its class, and the selector uses "widgetselector" for its class.  Both the widgetzone and the selector also have a unique ID based on the new ZoneName property (see below).  So, example markup of what the <div> and <select> elements now look like are:

<div class="widgetzone" id="widgetzone_PageBottom">
<select class="widgetselector" id="widgetselector_PageBottom">

There's a good chance you have #widgetzone or #widgetselector styles defined in your theme's CSS file.  Since there no longer are elements with those IDs, you'll want to do a search-and-replace in your CSS file.  Replacing #widgetzone with .widgetzone and #widgetselector with .widgetselector is all that is required.  Fortunately, because each widgetzone <div> and <select> element now have their own unique IDs, you can use those IDs to style individual widgetzones if they need to be styled differently in your blog.

ZoneName

Each widgetzone you add to your blog needs to have it's own unique ZoneName.  The ZoneName can be added as a simple property to the widgetzone's control markup.  For example:

<blog:WidgetZone runat="server" ZoneName="PageBottom" />

Although the ZoneName property is new to BE.NET, the existing widgetzone you've had in your blog up till now will start off with a ZoneName of "be_WIDGET_ZONE".  This was the name used to store widgetzone data in either the App_Data folder, or in a database.  Although not required, it wouldn't hurt to add that ZoneName to your existing widgetzone for clarity:

<blog:WidgetZone runat="server" ZoneName="be_WIDGET_ZONE" />

If you don't explicitly state a ZoneName, that is okay for one widgetzone, as BE.NET will default to using "be_WIDGET_ZONE" when a ZoneName is not provided.  But, you MUST give each widgetzone its own unique ZoneName if you will use more than one widgetzone in your blog.

App_Data and Database - Both OK

Sometimes designing a flexible, well structured application pays off.  It's great that BE.NET was designed as such.  There was very little change required to enable multiple widget zones to be saved in either the App_Data folder or in a database.  No database changes are needed either since the data for all the widgetzones will be stored in the existing be_DataStoreSettings table.  Whether you're using XML storage in the App_Data folder or a DB backend, multiple widget zones work in both scenarios.

Moving Widgets

Drag-and-drop is out, and dropdown is in!  Rearranging widgets within a widgetzone has always been done by dragging and dropping a widget to a new location within the zone.  While initially implementing multiple widgetzones, I had a zone on the left side of the page, and another on the right side.  While trying to drag-and-drop a widget on the right side to a new location within the same right side zone, I was having troubles with the drag-and-drop library trying to drop the widget into the zone on the left side of the page!  Instead of spending time trying to weed through the logic of the drag-and-drop library, it occurred to me a simpler interface would be the better route to go.  Drag-and-drop may be sexy, but getting widgets to move without any hassles or confusion is a better goal.

There is now a 'Move' link for each widget, next to the Edit and X (remove) links.

New Move Link

Clicking the Move link will display a dropdown list and Move button above the Move link.

Move Dropdown List and Button

In this example, I have 3 widget zones in my blog.  Opening the dropdown list shows all 3 zones and the widgets within each zone.

Move Widget Dropdown List Contents

The 3 zones appear in the list with brackets around them.  They are the HeaderZone, be_WIDGET_ZONE and the PageBottom zone.  Under each zone name is the list of widgets within that zone.  You can either select a widget to move the current widget in front of, or you can select a zone which will move the current widget to the bottom of that zone.

As you can tell, moving a widget from one zone to another zone is fully supported.  There's lots of possibilities with this capability.

Few More Stylesheet Changes

In addition to the search-and-replace stylesheet modification mentioned above, there's a couple of other additions you may want to make to your stylesheet in support of the new Move link and dropdown list.  These changes are already in the stylesheets of the themes included with BE.NET.  So you know what they are, I've listed them below.  The first style below was a modification of an existing style.

div.widget a.edit, div.widget a.move{
	font-size: 10px;
	font-weight: normal;
	float: right;
	z-index: 1;
	margin-left: 5px;
}

.widgetzone div#moveWidgetToContainer {
	text-align: right;
	margin: 3px;
}

Credits

Thanks to McZosch who posted the initial code changes for multiple widgetzones in the Issue Tracker at CodePlex.  I was able to use mostly everything he contributed.  Integration of his code went smoothly.  Most of the time I spent with adding this feature was with moving widgets.  Switching from drag-and-drop to dropdown and enabling widgets to move from one zone to another took some time altogether.

Give it a Try!

Since it's of course not required to fill up a widgetzone with many widgets in it, multiple widgetzones will allow you to add a zone anywhere in your blog with just a single widget in it.  There's a lot of possibilities with this, and for many BE.NET bloggers out there, I think multiple widgetzones are going to be very useful.

Everything seems to work well after testing out the new feature in a few scenarios.  You can download the latest build of BE.NET in the Source Code section at CodePlex.  If you run into any issues, please post them in the Discussions area at CodePlex.

Posted in: Development

Tags: ,

Multiple Forms in Master Page Site

March 30, 2009 at 11:14 PMBen

When integrating a website with 3rd party services such as search providers or payment processors, it's not an uncommon need to have a submit button that posts to that 3rd party's site.

ASP.NET 2.0 introduced the PostBackUrl property for controls that can initiate a post -- Buttons, ImageButtons, LinkButtons.  With this property, you can have your page post to any URL.  This is a somewhat decent solution to this problem.  The major downside to PostBackUrl is it requires the visitor to have JavaScript enabled in their browser.  If JavaScript is disabled, you end up with a normal postback to your own page.  To me, this makes PostBackUrl not a very good choice to turn to.

The other day, I needed to send a visitor to a payment processor's site.  My site was using master pages.  I was collecting some preliminary information from the visitor on a Content page.  Once collected, I was going to show them a confirmation of what they would be paying for and give them a button to move onto the payment processor.  I decided to use the PostBackUrl property on this button.

The payment processor needed a few hidden fields included in the form submission.  I put those hidden input fields including the values into non-server hidden input fields since I didn't want the Name or Ids of the input fields mangled by ASP.NET.  The form looked good, and clicking the button took me to the payment processor's website -- but as soon as I got there, the only thing on the page was some error message complaining about incoming data (a very non-specific error message).

I knew their site and this particular landing page worked when I tested posting data in a non-ASP.NET environment.  I confirmed the correct hidden input fields were being sent in the POST through Fiddler, but still no dice.  The only explanation I could come up with is when using PostBackUrl, not only are my custom hidden fields being passed to this other site, but so are all the other standard ASP.NET hidden fields -- ViewState, EventValidation, etc.  It's possible the payment processor's site was not expecting other form values to be passed to it.

So I decided to ditch PostBackUrl ... which was fine since I wasn't a big fan of PostBackUrl to begin with.  I came up with a pretty simple solution to having multiple forms in this master page environment.  The typical master page setup is where a <form runat="server"> tag in the master page surrounds the ContentPlaceHolder.

<body>
    <form id="form1" runat="server">
        <asp:ContentPlaceHolder id="ContentPlaceHolder1" runat="server">
        </asp:ContentPlaceHolder>
    </form>
</body>

Now if you put your own <form> tag in the Content page, you'll end up with a form nested in another form.  Nested forms are not valid.  Here's what I did to avoid nested forms, but still end up with multiple forms.

<body>    
    <form id="form1" runat="server">
        <asp:ContentPlaceHolder id="cphForm" runat="server">
        </asp:ContentPlaceHolder>
    </form>
    
    <asp:ContentPlaceHolder id="cphNoForm" runat="server">
    </asp:ContentPlaceHolder>
</body>

The master page now has 2 content place holders.  One is wrapped in a <form runat="server"> tag, and the other isn't.  The type of controls allowed when not wrapped in a <form runat="server"> tag is limited.  For instance, you cannot have ASP.NET Buttons, TextBoxes and a number of other controls outside a <form runat="server"> tag.  You can however use Literals, PlaceHolders and basically any HTML controls with a runat="server" tag.  Here's the content page markup.

<asp:Content ID="cntForm" ContentPlaceHolderID="cphForm" Runat="Server">
    
    <asp:PlaceHolder ID="phForm" runat="server">
    
        <h2>Select an Item to Purchase</h2>
        <asp:RadioButtonList ID="rblMenu" runat="server">
            <asp:ListItem Text="Item 1" Value="item1" Selected="True"></asp:ListItem>
            <asp:ListItem Text="Item 2" Value="item2"></asp:ListItem>
            <asp:ListItem Text="Item 3" Value="item3"></asp:ListItem>
        </asp:RadioButtonList>
        <asp:Button ID="btnContinue" runat="server"
                    Text="Continue" OnClick="continuePurchase" />

    </asp:PlaceHolder>
    
</asp:Content>

<asp:Content ID="cntNoForm" ContentPlaceHolderID="cphNoForm" Runat="Server">

    <asp:PlaceHolder ID="phConfirmation" runat="server" Visible="false">
    
        <h2>
           Your Selected Item:
           <asp:Literal ID="litSelectedItem" runat="server"></asp:Literal></h2>
    
        <form method="post" action="http://www.example.com/">
            <input type="hidden" name="myId" value="someID" />
            <input type="hidden" name="itemCode" value="<%= itemCode %>" />
            <input type="hidden" name="itemAmount" value="<%= itemAmt %>" />
            <input type="submit" name="payNow" value="Pay Now" />
        </form>
    
    </asp:PlaceHolder>

</asp:Content>

The content page is making use of both content place holders.  The top Content control contains a place holder, which contains a simple order form.  The second Content control contains a place holder with its visibility set to false.  So when the page is first pulled up, nothing in the second Content control is yet visible.

Once an item is selected, and the Continue button is clicked, a postback is done.  During the postback, the placeholder in the first Content control is made invisible, and the placeholder in the second Content control is made visible.  Because the second Content control (and all of its contents) are outside of the <form runat="server"> tag, we've achieved having two different, non-nested <form> tags on the same page.  Here's what the rendered HTML looks like after the person has selected their item and is ready to be sent to the payment processor:

<body>    
   <div>
      <form name="aspnetForm" method="post" action="Content1.aspx" id="aspnetForm">
         <div>
            <input type="hidden" name="__VIEWSTATE"
                   id="__VIEWSTATE" value="some long value" />
         </div>
      </form>
     
      <h2>Your Selected Item: Item 2</h2>
    
      <form method="post" action="http://www.example.com/">
         <input type="hidden" name="myId" value="someID" />
         <input type="hidden" name="itemCode" value="item2" />
         <input type="hidden" name="itemAmount" value="30" />
         <input type="submit" name="payNow" value="Pay Now" />
      </form>
   </div>    
</body>

With this approach, I was able to post the form to the payment processor, and the form contained hidden input fields only relevant to the processor.  This approach will work in most master page situations.  It may be difficult to implement if you have server side controls requiring a <form runat="server"> tag in your master page -- outside of the main content place holder.  In this case, it may still be possible to implement this two ContentPlaceHolder approach, if you do some juggling around of your controls and/or layout.  However, in a lot of situations, this is a practical way to achieve multiple form tags in an ASP.NET site.