ASP.NET MVC generates invalid ClientIDs

This week I had a joy of spending several hours trying to solve and intermitently happening bug. In short, some 2 times out of 3 the YUI selector utility failed to lookup elements in an XHR loaded HTML fragment. After digging quite deep into the YUI code, I found that it was actually the fault of the way ASP.NET MVC treats partial views.

Summary

  • ASP.NET MVC allows use of ASCX as “partial views”
  • It correctly assumes, that uniqueness of ClientID should be preserved
  • It incorrectly does not prepend an alpha-character to automatically generated GUID based IDs
  • User agents and selector libraries may correctly ignore such invalid IDs
  • Solution: override ViewUserControl.UniqueID

The problem with ASP.NET MVC

ASP.NET MVC allows you to return an ASCX control as a view. This allows using a very nice pattern, of loading only parts of the page using XHR, while still being able to use the same controls when the full page is generated.

If you’ve used ASP.NET enough, then ctl00_something_something must be familliar – it’s an automatically generated value for HTML id attributes. ASP.NET does a pretty good job in ensuring that the requirement of unique IDs is followed, however, that only really works for fully generated pages.

If you are returning a partial view to be injected into some page, the context has already been lost – i.e. the server no longer knows anything about the original counter of controls and unique IDs. To circumvent this problem, MVC uses a GUID as a prefix for the ClientID generation. While, this is an absolutely correct approach, which basically means that ID collision is practically impossible, when you inject your partials into the DOM, the actual implementation has a small bug – the HTML spec says:

ID and NAME tokens must begin with a letter ([A-Za-z]) and may be followed by any number of letters, digits ([0-9]), hyphens (“-”), underscores (“_”), colons (“:”), and periods (“.”).

However, as we all know, the GUIDs may start with a number. In fact, they do start with a number much more often than they do with a letter. Which means that your partial views start having situations where id="11fa1658-d453-42e0-86f1-e640c476f7ac_ctl02_something_something". Naturally, if your client side framework is standards aware, it may ignore the id attribute altogether and fail miserably.

The YUI (non)part of the problem

In general, the advice is to not use ID attributes as a base for your operations – be it selecting elements, or attaching events (use classes and wiser selectors instead). In our case, though, we still have a lot of legacy code, which does not follow this approach. There is also the case of Visual Studio sometimes inserting IDs automatically. Still, it doesn’t matter how IDs end up there – if you’re using YUI selector library, you will get some autogenerated IDs anyways.

The reasoning for that, is that querying the DOM using IDs is very cheap with getElementById(). If you’re supplying a parent element, from which your query should be run, YUI will either re-use the ID that already exists, or will create a new (unique) one for the purposes of expanding your query, i.e. YAHOO.util.Selector.query(".myClass", parentElement) will become "DIV#parentId .myClass" which in turn will be expanded into "DIV[id=parentId] [class=myClass]", which is still the very same selector, but is easier to parse and process.

Now, the only problem is the regex ('\\#(-?[_a-z]+[-\\w]*)': '[id=$1]') which does the expanding of ID and class selectors into the attribute based selectors – it does follow the standard, and only accepts valid values, which means that if your ID starts with a number – it will not be expanded and the whole query will fail.

Solution

While some may say, that YUI should be updated to be more lenient on such errors, I say that there should be no place on the web for invalid code. And there’s also a much simpler solution to the problem – override default behaviour of ViewUserControl.UniqueID:

public class MyViewUserControl : ViewUserControl
{
	public override string UniqueID
	{
		get
		{
			var retval = base.UniqueID;
			if(!string.IsNullOrEmpty(retval)
				&& char.IsDigit(retval[0]))
			{
				retval = "c" + retval;
			}
			return retval;
		}
	}
}

Now… where do I report bugs in ASP.NET MVC?..

Comments are closed.