Correcting MVC 3 EditorFor Template Field Names When Using Collections

March 25 2011   1 Comment   

So, I recently ran into a problem with ASP.NET MVC 3 and editor templates when dealing with models that contain collections. If you handle the collection directly in the view, it works fine:

@For i as integer = 0 To Model.Locations.Count-1
    Dim index As Integer = i

    @<div>
        @Html.EditorFor(Function(model) model.Locations(i).Name)
        @Html.EditorFor(Function(model) model.Locations(i).Description)
    </div>
Next

In the above example, the fields will receive the names “Locations[0].Name” and “Locations[0].Description”, and so on for each field. This is correct, and will result in the model begin parsed correctly by the DefaultModelBinder if it’s a parameter on your POST action.

However, what if you want to make an editor template that always gets used for a specific collection type?  Let’s say, for example, that the collection in the model is of type LocationCollection, and you make an editor template named LocationCollection:

@ModelType LocationCollection
@For i as integer = 0 To Model.Count-1
    Dim index As Integer = i
    @<div>
        @Html.EditorFor(Function(model) model(i).Name)
        @Html.EditorFor(Function(model) model(i).Description)
    </div>
Next

Then you reference it in your view like this:

@Model.EditorFor(Function(model) model.Locations)

In this case, the fields will actually have the incorrect names on them, the will be named “Locations.[0].Name” and “Locations.[0].Description”.  Notice the extra period before the array index specifier.  With these field names, the model binder won’t recognize the fields when they come back in the post.

The solution to this problem is a little cumbersome, due to the way the field names are built.  First, the prefix is passed down to the editor template in ViewData.TemplateInfo.HtmlFieldPrefix.  However, this prefix is passed down WITHOUT the period.  The period is added as a delimiter by the ViewData.TemplateInfo.GetFullHtmlFieldName function, which is used by all of the editor helpers, like Html.TextBox.

This means that we actually can’t fix it in the editor template shown above.  Instead, we need TWO editor templates.  The first one for LocationCollection, and then a second one for Location:

@ModelType LocationCollection
@For i as integer = 0 To Model.Count-1
    Dim index As Integer = i
    @Html.EditorFor(Function(model) model(index))
Next
@ModelType Location
@Code
    ViewData.TemplateInfo.FixCollectionPrefix()
End Code
<div>
    @Html.EditorFor(Function(model) model(i).Name)
    @Html.EditorFor(Function(model) model(i).Description)
</div>

By breaking this up into two editor templates, we can now correct the prefix in the second template.  The first editor template receives an HtmlFieldPrefix of “Locations”, so we can’t do anything with that.  However, the Location editor template receives an HtmlFieldPrefix of “Locations.[0]“, so all we need to do is remove the extra period and the problem is solved.  As you can see in my example above, I’m calling a method on TemplateInfo, FixCollectionPrefix.  This is a simple extension method which corrects prefix, which you just need implement somewhere in an imported namespace:

<Extension()>
Public Sub FixCollectionPrefix(templateInfo As TemplateInfo)
Dim prefix As String = templateInfo.HtmlFieldPrefix
If Not String.IsNullOrEmpty(prefix) Then
templateInfo.HtmlFieldPrefix = Regex.Replace(prefix, ".(\[\d+\])$", "$1")
End If
End Sub

And that’s all there is to it. I certainly hope that the MVC team fixes this problem internally for their next release, but until then we’ll just have to keep working around it.

MVC 3 Unobtrusive AJAX Improvements

January 28 2011   1 Comment   

I started experimenting this week with MVC 3 and the new unobtrustive javascript frameworks, both for AJAX and validation. These are built around jQuery, and frankly are very nice pieces of work. There are, however, a few improvements that can be made to the Javascript files included in the distribution. You can obviously include these changes manually, it’s pretty simple, but hopefully one day Microsoft will roll these up in the next version.

I’m not sure how the unobtrusive stuff is licensed by Microsoft, so I won’t publish the modified code here until I’m certain it’s okay. I will, however, go over the modifications so that you can do them yourselves.

  1. Use fn.delegate instead of fn.live – By default, the unobtrusive AJAX library uses fn.live to monitor for anchor click and form submit events, and if the elements are set to operate using AJAX they are intercepted and an AJAX request is initiated.  However, this can be inefficient because the selector is actually being fired during during document.ready, even though it isn’t used for anything.  This means every single form, submit button, and link on your page is being checked for a data-ajax=”true” attribute during document.ready for no reason.  By using $(document).delegate(selector, eventType, fn) this is completely avoided. Note: This only works using jQuery 1.4.2 or later.
  2. Cache jQuery Objects – While this is a minor efficiency concern, there are several places with the event handlers where $(form) is called multiple times to convert an element into a jQuery object with a single member.  It is much more efficient to cache $(form) in a local variable and reuse it. You can also make finding the form more efficient using .closest(‘form’) instead of .parents(‘form’)[0].
  3. Inefficient Handling Of Null Callbacks – asyncRequest uses getFunction to get a callback function based on data attributes.  This is nifty system, because can take either a function name or inline javascript code.  However, if there is no function specified, it goes through the trouble of using Function.constructor to create an empty function.  This can be avoided by adding “if (!code) return $.noop;” to the beginning of getFunction.
  4. Global AJAX Events – Currently, there is no way to attach global AJAX event handlers that specifically handle only the MVC related AJAX events.  Your only options are to intercept ALL jQuery AJAX events, or to attach events to each specific AJAX request using data attributes.  However, there are definitely use cases for global event handlers that only affect MVC related AJAX events.  Therefore, I recommend updating the “asyncRequest” function to add information to the options passed to $.ajax.  In particular, during the $.extend(options, {…}) I am adding “mvcRequest: true” and “mvcTargetElement: element”.  Now you can attach global jQuery AJAX handlers using $(document).ajaxSuccess, etc, and then test to see if it is an MVC request.
  5. AJAX Updated Forms Validation – In the current implementation of the unobtrusive validation, it can’t handle forms added to the page via an AJAX request.  You have to manually call $.validate.unobtrusive.parse() after the page update before client-side validation resumes.  This can be addressed using a global handler, so long as #3 above is also implemented.  By using the attributes passed in, we restrict the processing so it doesn’t re-parse the entire document, only the updated region. The code for this is below:
$(document).ajaxSuccess(function (event, xhr, settings) {
    if (settings.mvcTargetElement) {
        $(settings.mvcTargetElement.getAttribute("data-ajax-update")).each(function () {
            $.validator.unobtrusive.parse(this);
        });
    }
});

Please let me know if you see any other improvements that I’ve missed. I’m just beginning to dig through things, so I’ll keep updating as I find more as well.

jQuery Smooth Animations In Firefox 4

October 24 2010   Leave a Comment   

jQuery Smooth Animations is an experiment in utilizing the new, proposed animation API found in Firefox 4. It should be noted that this API is just a proposal and is experimental. Using it on live websites isn’t recommended until the API is finalized.

jQuery Smooth Animations works by replacing parts of the standard jQuery API to make use of the new animation API if it is available. It should still be fully compatible with browsers that don’t implement the animation API.

The end result of this plugin is that animations run more smoothly on Firefox 4, by syncing the animations with paint events. In my experimental comparisons between Firefox 4 Beta 6 and Firefox 3.6.12, I noticed a significant improvement in the jerkiness of the animation in the example.

I recognize that this plugin could be slightly less invasive by not replacing any of the API if window.mozRequestAnimationFrame is not found. However, I wanted to demonstrate how the code could function within the jQuery library itself rather than as a plugin. This is based on the theory that at some point, once the API is standardized, support will be integrated into the jQuery library.

Integration into the library would also allow us to eliminate the override of jQuery.now, instead using smarter logic within jQuery.fx.step. I simply didn’t want to replace jQuery.fx.step because of the extensive logic it contains which could be changed in future releases of jQuery.

GitHub Repository

Detecting Original HTML5 Input Types On Unsupported Browsers

April 25 2010   2 Comments   

I’ve been doing some work recently on extending jQuery UI to style form elements using the Themeroller.  One thing that I wanted to implement was detection of the new HTML5 input types and choosing the correct widget to use based upon that.  This would provide progressive enhancement of the input element to support the HTML5 input type, even if that type of input isn’t supported on that browser.

By definition, if a browser can’t support the type of an input, it automatically falls back to a text input.  This is great for backwards compatibility, but it gave me a bit of a problem.  If I set type=”number” in my HTML, testing the type of the element in Javascript returns “text” when the number element isn’t supported.  Useful for determining if support is available, not so useful for determining what the original value is in the HTML.

In most browsers, you can find the original value using the attributes collection.  However, this doesn’t work in IE7 and earlier.  And, as everyone knows, we still have to support IE 6 & 7 for most web sites.  I found an alternative for IE, which is to test the outerHTML of the element using a regular expression.  IE will return this with the original attributes intact.

I’ve written a jQuery filter extension that allows you to test any element for the original input type:

// add HTML5 input type expression (still detects HTML5 input types on browsers that don't support them)
$.extend($.expr[':'], {
	inputtype: function(elem, i, type) {
		function getRawAttr() {
			// IE will return the original value in the outerHTML
			var match = /<input.*?\s+type=(\w+)(\s+|>).*?/i.exec(elem.outerHTML);

			if (match && match.length >= 2 && match[1] !== "text") {
				return match[1];
			}

			// for other browsers, test attributes collection (doesn't work in IE<7)
			var attrs = elem.attributes,
				i;

			for (i=0; i<attrs.length; i++) {
				if (attrs[i].nodeName === "type") {
					return attrs[i].nodeValue;
				}
			}

		}

		if (elem.tagName != "INPUT") return false;
		if (elem.type === "text") {
			// could be unsupported type fallback, so do further testing
			return getRawAttr() === type[3];
		} else {
			return elem.type === type[3];
		}
	}
});

To use this extension, just use the :inputtype(type) filter in your jQuery expression.  For example:


$('input:inputtype(number)).width(100);

if ($(elem).is(':inputtype(number)')) {
...
}

Update: Changed to only run the check if the tag is an input tag, in case you run the filter against non-input elements.

Simplify CSS and Javascript Compression In Visual Studio

February 23 2010   Leave a Comment   

I’ve released a new open source tool that performs design-time compression of your CSS and Javascript files in Visual Studio projects.  This can be a big help, since it allows you to easily do it in your project rather than as part of your build/publish process.  And, since it leaves both the compressed and uncompressed versions in place, you can still use the uncompressed version for debugging.

Read more about this new tool or download it at http://btburnett.com/netcompressor.

 
     
Copyright © 2009 All Rights Reserved. Powered by WordPress 2.7 Subscribe to RSS