ASP.NET Routing, DataBinding and WebForms

One of the great things about ASP.NET MVC is the automatic databinding support for routing. For example, if you define a route like this:

routes.Add(new Route("{controller}/{action}/{id}", new MvcRouteHandler()) {
	Defaults = new RouteValueDictionary(new { action = "Index", id = (string)null })
});

…and you have a controller like this:

public class HomeController : Controller {
	public ActionResult Show(int id) {
		// do stuff
		return View();
	}
}

…then visiting a url like mysite.com/Home/Show/5 will automatically popuate the id parameter of the Show action with the value 5.

I thought it would be fun to see if I could get something similar working using WebForms. It actually turned out to be quite straightforward.

First, you’ll need to set up ASP.NET Routing to work with WebForms. I’m using Phil Haack’s WebFormRouting library which can be found here.

I’ve added an extension method to RouteCollection that allows me to declare routes like this:

RouteTable.Routes.MapWithAutoBinding("Customer/{CustomerId}").To("~/Customer.aspx");

In the code behind file for my customer.aspx page I can now declare a property like this:

public partial class Customer : Page {
	protected void Page_Load(object sender, EventArgs e) {
		customerNameLabel.Text = CustomerId;
	}

	[DataBind]
	public string CustomerId { get; set; }
}

So when I visit MySite.com/Customer/Jeremy the “CustomerId” property on the Customer.aspx page will automatically be set to “Jeremy”.

How does it work?

Calling RouteTable.MapWithAutoBinding will create a route using the AutoBoundWebFormRouteHandler which inherits from Phil Haack’s WebFormRouteHandler. After GetHttpHandler is called, reflection is used to find any properties on the page that have a DataBindAttribute.

The RequestContext is then passed to the PerformBinding method on the DataBindAttribute, which will find the appropriate value from the RouteData and invoke the property’s setter.

Taking it a stage further: using Linq to Sql

Just for fun, I thought I’d see if it was possible to automatically fetch an entity from the database based on the value in the RouteData in a similar way to Monorail’s ARDataBind attribute. I thought I’d try and use Linq to SQL as I haven’t used this in a project before.

The first stage was to inherit from the DataBindAttribute class and override the PerformBinding method. Now, instead of just passing the value from the RouteData to the property, we have to run a linq query using the value from the route data as the primary key. This is very easy to do thanks to the Dynamic Linq provider that Microsoft ship in the Visual Studio 2008 samples folder.

So, now the property on the Customer.aspx page can look like this:

[LinqBind("CustomerId")]
public Customer Cust { get; set; }

The “CustomerId” being passed into the LinqBind attribute tells the binder the name of the RouteData value it should use as the primary key for the Customer entity. Currently, this only works with single primary keys, although it should be possible to get it work with composite keys too.

Visiting mysite.com/customer/ALFKI will now run a linq query to load a customer from the Customers table using the customer id of “ALFKI” and then set the “Cust” property to be a reference to this entity.

It is also necessary to tell the LinqBindAttribute which DataContext should be used to do the fetching. This can be set in the Global.asax’s Application_Start method using a delegate:

LinqBindAttribute.DataContextFactory = () => new MyDataContext();

I’ve uploaded the code here if anyone wants to play with it.

Disclaimer: The zip file contains Phil Haack’s WebFormRouting.dll and Microsoft’s Dynamic Linq provider from the Visual Studio 2008 samples.

ConventionController Documentation

I’ve just uploaded my first attempt at some documentation for the MVCContrib project’s ConventionController.

If anyone has any suggestions for improvement, or would like something else added, please let me know.

Embedded Resources with MonoRail Applications

I have a number of JavaScript files that I like to share amongst my web projects. Rather than keeping a separate copy of these script files in each application, I prefer to embed them in to an assembly that the various web projects can reference. But how do you reference these JavaScript files in your HTML?

Using ASP.NET WebForms this is easy – you simply add a [assembly: WebResource] attribute to your AssemblyInfo.cs file and then you can call Page.ClientScript.GetWebResourceUrl to generate a URL to an HTTP Handler that will extract the resource from the assembly and output its contents (see this article on 4guys for more information)

I wanted to do a similar thing using MonoRail, so I wrote a Controller and a Helper that perform a similar task.

The first thing I do is create a helper that will look up the names of all the embedded resources in the assembly. To do this, I use a static constructor to ensure that this only happens the first time that the class is used.

public class ScriptHelper : AbstractHelper
{
    internal const string ROOT_NAMESPACE = "Cristal.Common.Resources";
    private static readonly List<string> _resources = new List<string>();
    private static readonly string _version;

    static ScriptHelper()
    {
        _version = Assembly.GetExecutingAssembly().GetName().Version.ToString().Replace(".", "");
        foreach (string resource in typeof(ScriptHelper).Assembly.GetManifestResourceNames())
        {
            _resources.Add(resource);
        }
    }
}

The static constructor finds all of the assembly-level resources and adds their names to a cache. I also look up the version of the currently executing assembly – more on this later. The constant ROOT_NAMESPACE variable simply stores the namespace where the embedded resources are stored.

Finally, I have a public method called “Install” that can be called from my Views to reference one of these embedded scripts:

public string Install(string name)
{
    NameValueCollection querystring = new NameValueCollection(2);
    querystring.Add("n", name); //name of script
    querystring.Add("v", _version); //version
    string url = Controller.UrlBuilder.BuildUrl(Controller.Context.UrlInfo,
                                                string.Empty,
                                                "Resources",
                                                "Scripts",
                                                querystring);

    //cut off the final ampersand which monorail irritatingly appends.
    url = url.Substring(0, url.Length - 1);
    url = HttpUtility.HtmlEncode(url);
    return string.Format("<script type=\"text/javascript\" src=\"{0}\"></script>", url);
}

This method generates a URL to a pretend controller with the name of the script (and the assembly version) as querystring parameters. I can then declare this helper on my controller, and reference it in the view:

Controller:

[Helper(typeof(ScriptHelper), "Script")]
public class MyController : Controller
{
	public void Index() {}
}

And the view:

${Script.Install("SomeScriptName")}

Note that the name of the script is not the full web resource name (eg Cristal.Common.Resources.SomeScriptName.js)

In the HTML, this will generate some thing like:

<script type="text/javascript" src="/Resources/Scripts.rails?n=SomeScriptName&v=1000"></script>

The “n” querystring parameter is the name of the script and the “v” is the assembly version.
Of course, this won’t actually work at the moment as there isn’t a Resources controller to handle the request. So let’s create one:

public class ResourcesController : Controller
{

    //n stands for "name", v stands for "version"
    [Cache(HttpCacheability.Public, Duration = 86400, VaryByParams = "v,n")]
    public void Scripts()
    {
    	if (Request.QueryString["n"] == null)
            throw new Exception("Invalid resource.");
	    Response.ContentType = "text/javascript";
        RenderText(ScriptHelper.GetResource(HttpUtility.HtmlEncode(Request.QueryString["n"])));
    }
}

The “Scripts” action is very simple – it ensures that a script name is present in the query string, then passes it to the GetResource method on our scripthelper (below). Also notice the [Cache] attribute at the top of the action – once the script has been loaded from the assembly, the response is cached. However, to ensure that each script is cached separately, we specify the “n” query string parameter (the script name) in the VaryByParams option.

“v” is also in VaryByParams so that I can force the cache to expire by incrementing the version number.

Here’s the GetResource method on ScriptHelper:

internal static string GetResource(string name)
{
    name = BuildResourceName(name);

    string toReturn = string.Empty;
    if (name != null)
    {
     	Assembly asm = typeof(ScriptHelper).Assembly;
        using (StreamReader reader = new StreamReader(asm.GetManifestResourceStream(name)))
        {
        	toReturn = reader.ReadToEnd();
        }
    }
    return toReturn;
}

private static string BuildResourceName(string name)
{
    name = string.Format("{0}.{1}.js", ROOT_NAMESPACE, name);
    bool hasResource = false;
    foreach (string resource in _resources)
    {
    	if (name.Equals(resource, StringComparison.InvariantCultureIgnoreCase))
        {
            hasResource = true;
            name = resource;
            break;
        }
    }
    if (!hasResource)
    	return null;

    return name;
}

Firstly, BuildResourceName is called to convert the script name (eg “SomeScriptName”) in to the fully qualified Web-resource name (eg “Cristal.Common.Resources.SomeScriptName.js”). Then, we use a streamreader to extract the contents of the JavaScript file from the embedded resource, convert it to a string and return it to the controller.

I’ve implemented this in the next version of CristalWeb (version 7).

AJAX Control Toolkit Updated

Microsoft has released a new version of the AJAX Control Toolkit. As well as a number of bug fixes there are two new controls in this build: SlideShowExtender and ListSearchExtender.

The updated version can be downloaded from here