Correcting MVC 3 EditorFor Template Field Names When Using Collections

March 25 2011   One 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.

One Response to “Correcting MVC 3 EditorFor Template Field Names When Using Collections”

  1. shawninc says:

    Cutting and pasting that does not work in MVC3. To get the extension to work, I had to create a class file:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;

    namespace incMvcSite.Classes {
    public static class HtmlPrefixScopeExtensions {
    public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix) {
    return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
    }

    private class HtmlFieldPrefixScope : IDisposable {
    private readonly TemplateInfo templateInfo;
    private readonly string previousHtmlFieldPrefix;

    public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix) {
    this.templateInfo = templateInfo;

    previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
    templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
    }

    public void Dispose() {
    templateInfo.HtmlFieldPrefix = previousHtmlFieldPrefix;
    }
    }
    }
    }
    In the Razor (.cshtml) file, I added the following:
    @using incMvcSite.Classes
    @using(Html.BeginHtmlFieldPrefixScope(“Permission”)) {

    Permission

    // The Html.EditorFor’s would go here…

    }
    Notice the using to bring me extension class into scope. That allows the second using line to work.
    Now the problem is that when posting back, the object is not updated. In my controller, I used a second parameter to specify my prefix:
    TryUpdateModel(modelUser.Permission, “Permission”);
    This added the prefix to all field in the HTML, and the TryUpdateModel loaded the object with prefixed control names. Now you can properly namespace your controls for embedded edit lists, and for partial views of models with the same property names.

    Shawn Zernik
    Internetwork Consulting

Leave a Reply

You must be logged in to post a comment.

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