Prefixing Input Elements Of Partial Views With ASP.NET MVC

16 commentsWritten on January 24th, 2011 by
Categories: ASP.NET MVC

Suppose we have the following set of classes in an ASP.NET MVC project:

    public class NameModel
    {
        [Display(Name = "First name")]
        [Required]
        public string FirstName { get; set; }

        [Display(Name = "Last name")]
        [Required]
        public string LastName { get; set; }
    }

    public class AddressModel
    {
        [Required]
        public string Street { get; set; }

        [Required]
        public int Number { get; set; }

        [Display(Name = "Zip code")]
        [Required]
        public int ZipCode { get; set; }

        [Required]
        public string City { get; set; }
    }

    public class PersonModel
    {
        public PersonModel()
        {
            Name = new NameModel();
            Address = new AddressModel();
        }

        public NameModel Name { get; private set; }

        public AddressModel Address { get; private set; }

        [Required]
        [MustBeValidEmailAddress(ErrorMessage = "Not a valid email")]
        public string Email { get; set; }
    }

An input form for an instance of PersonModel could look like this:

@using (Html.BeginForm("MyPostAction", "MyController", FormMethod.Post))
{
    @Html.ValidationSummary(true)

    <p>
        @Html.LabelFor(model => model.Name.FirstName)<br />
        @Html.TextBoxFor(model => model.Name.FirstName)
        @Html.ValidationMessageFor(model => model.Name.FirstName)
    </p>

    <p>
        @Html.LabelFor(model => model.Name.LastName)<br />
        @Html.TextBoxFor(model => model.Name.LastName)
        @Html.ValidationMessageFor(model => model.Name.LastName)
    </p>
        
    <p>
        @Html.LabelFor(model => model.Address.Street)<br />
        @Html.TextBoxFor(model => model.Address.Street)
        @Html.ValidationMessageFor(model => model.Address.Street)
    </p>

    <p>
        @Html.LabelFor(model => model.Address.Number)<br />
        @Html.TextBoxFor(model => model.Address.Number)
        @Html.ValidationMessageFor(model => model.Address.Number)
    </p>
    
    <p>
        @Html.LabelFor(model => model.Address.City)<br />
        @Html.TextBoxFor(model => model.Address.City)
        @Html.ValidationMessageFor(model => model.Address.City)
    </p>

    <p>
        @Html.LabelFor(model => model.Address.ZipCode)<br />
        @Html.TextBoxFor(model => model.Address.ZipCode)
        @Html.ValidationMessageFor(model => model.Address.ZipCode)
    </p>
                           
    <p>
        @Html.LabelFor(model => model.Email)<br />
        @Html.TextBoxFor(model => model.Email)
        @Html.ValidationMessageFor(model => model.Email)
    </p>
        
    <input type="submit" value="Confirm" />
}

That would produce HTML like this:

<label for="Name_FirstName">First name</label><br />
        <input data-val="true" data-val-required="The First name field is required." id="Name_FirstName" name="Name.FirstName" type="text" value="" />
        <span class="field-validation-valid" data-valmsg-for="Name.FirstName" data-valmsg-replace="true"></span>
    </p>
    <p>
        <label for="Name_LastName">Last name</label><br />
        <input data-val="true" data-val-required="The Last name field is required." id="Name_LastName" name="Name.LastName" type="text" value="" />
        <span class="field-validation-valid" data-valmsg-for="Name.LastName" data-valmsg-replace="true"></span>
    </p>
    <p>
        <label for="Address_Street">Street</label><br />
        <input data-val="true" data-val-required="The Street field is required." id="Address_Street" name="Address.Street" type="text" value="" />
        <span class="field-validation-valid" data-valmsg-for="Address.Street" data-valmsg-replace="true"></span>
    </p>

Which is perfect, because this means we get client-side validation for free, based on the DataAnnotations that we used on our model classes. Also, pay attention to the values of the name attributes for the input elements. For the Name.FirstName property of an instance of PersonModel, the name attribute of the corresponding element is correctly set to "Name.FirstName". This enables the ModelBinder to correctly bind all values of the submitted form to construct a valid instance of PersonModel. So far, so good.

However, considering that we went through the trouble of creating separate NameModel and AddressModel classes, it would make sense that we use Partial Views to render editable fields for instances of NameModel and AddressModel. Our Partial View for the NameModel could look like this:

@model CarShop.Web.Models.NameModel

    <p>
        @Html.LabelFor(model => model.FirstName)<br />
        @Html.TextBoxFor(model => model.FirstName)
        @Html.ValidationMessageFor(model => model.FirstName)
    </p>

    <p>
        @Html.LabelFor(model => model.LastName)<br />
        @Html.TextBoxFor(model => model.LastName)
        @Html.ValidationMessageFor(model => model.LastName)
    </p>

While our Partial View for our AddressModel would look like this:

@model CarShop.Web.Models.AddressModel

    <p>
        @Html.LabelFor(model => model.Street)<br />
        @Html.TextBoxFor(model => model.Street)
        @Html.ValidationMessageFor(model => model.Street)
    </p>

    <p>
        @Html.LabelFor(model => model.Number)<br />
        @Html.TextBoxFor(model => model.Number)
        @Html.ValidationMessageFor(model => model.Number)
    </p>

    <p>
        @Html.LabelFor(model => model.ZipCode)<br />
        @Html.TextBoxFor(model => model.ZipCode)
        @Html.ValidationMessageFor(model => model.ZipCode)
    </p>
    
    <p>
        @Html.LabelFor(model => model.City)<br />
        @Html.TextBoxFor(model => model.City)
        @Html.ValidationMessageFor(model => model.City)
    </p>

We could then modify our original View so it would look like this:

@using (Html.BeginForm("MyPostAction", "MyController", FormMethod.Post))
{
    @Html.ValidationSummary(true)

    @Html.Partial("NamePartial", Model.Name)
    
    @Html.Partial("AddressPartial", Model.Address)    
                     
    <p>
        @Html.LabelFor(model => model.Email)<br />
        @Html.TextBoxFor(model => model.Email)
        @Html.ValidationMessageFor(model => model.Email)
    </p>
        
    <input type="submit" value="Confirm" />
}

Pretty clean huh? Unfortunately, there's a problem. In the generated HTML, the input fields for Name and Address are (obviously) no longer properly prefixed so the Model Binder would not be able to construct a proper PersonModel instance out of the posted values. Basically, we'd have a PersonModel instance where the Name and Address properties would point to instances whose properties would be null, even when the user filled in the values.

So, how do we set the prefix correctly within the Partial Views? We obviously can't hardcode the prefix within the Partial Views, because that would limit their usability to usage within parent Views where the Model indeed has a property of the correct type and with the expected name. We also really want to keep using the TextBoxFor methods in our Partial Views because that's what gives us the DataAnnotations-based client-side validation for free. Ideally, our Partial Views don't know anything about the prefix and we should be able to pass it in from the parent View. And it would also be nice if we could still use the Partial Views as they are, even when no prefix is required. After some searching, i found a pretty clean way to do this.

When we call the Html.Partial method in the parent View, we can pass in a ViewDataDictionary instance to the Partial View, which contains a TemplateInfo object, and that TemplateInfo object happens to have an HtmlFieldPrefix property. It took me a while to find this, but i'm glad i did. Now i can just change the calls to Html.Partial in the Parent View to this:

    @Html.Partial("NamePartial", Model.Name, new ViewDataDictionary 
    { 
        TemplateInfo = new System.Web.Mvc.TemplateInfo { HtmlFieldPrefix = "Name" } 
    })
    
    @Html.Partial("AddressPartial", Model.Address, new ViewDataDictionary 
    { 
        TemplateInfo = new System.Web.Mvc.TemplateInfo { HtmlFieldPrefix = "Address" } 
    })    

And in the generated HTML, the input elements for the NameModel and AddressModel properties will be properly prefixed. Now, we've already achieved our goal of keeping the Partial Views of having to know anything about a prefix that they may or may not need to include depending on which parent View is using them. But, as i'm sure you can agree, passing in the ViewDataDictionary with an instance of TemplateInfo with the correct HtmlFieldPrefix is kinda cumbersome, not to mention repetitive and even slightly error-prone. Ideally, we should be able to change our parent View to this:

    @Html.EditorForNameModel(model => model.Name);
                                                 
    @Html.EditorForAddressModel(model => model.Address);

It's actually quite easy to do so. First, we'll need the following helper method:

        private static MvcHtmlString GetPartial<TRootModel, TModelForPartial>(
            HtmlHelper<TRootModel> helper, string partialName, Expression<Func<TRootModel, TModelForPartial>> getter)
        {
            var prefix = ExpressionHelper.GetExpressionText(getter);

            return helper.Partial(partialName, getter.Compile().Invoke(helper.ViewData.Model),
                new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = prefix } });
        }

I know, i know... that code is butt-ugly due to the generics usage but hey, that's C# for ya. It does allow us to create the EditorForNameModel and EditorForAddressModel methods like this:

        public static MvcHtmlString EditorForNameModel<TModel>(
            this HtmlHelper<TModel> helper, Expression<Func<TModel, NameModel>> getter)
        {
            return GetPartial(helper, "NamePartial", getter);
        }

        public static MvcHtmlString EditorForAddressModel<TModel>(
            this HtmlHelper<TModel> helper, Expression<Func<TModel, AddressModel>> getter)
        {
            return GetPartial(helper, "AddressPartial", getter);
        }

And there we go. Our Partial Views are clean and completely reusable. And using them in a parent View is nice and clean as well.

  • http://twitter.com/ntcoding Nick

    would it not be easier to create an editor template for each model. And then call Html.EditorForModel().

    Then in the editor template, do this:

    ViewData.TemplateInfo.HtmlFieldPrefix = “PrefixForModel”

    Seems a lot easier. Maybe I didn’t read it properly. I’m in a hurry.

  • Pingback: Tweets that mention Prefixing Input Elements Of Partial Views With ASP.NET MVC -- Topsy.com

  • http://twitter.com/joshuamck Joshua McKinney

    +1, except you don’t even need to do anything in the editor template. It just works.

    In Views/Person/Edit.cshtml:
    @Html.EditorFor(model => model.Name)

    In Views/Person/EditorTemplates/Name.cshtml or Views/Shared/EditorTemplates/Name.cshtml
    @model TestEditModel.Models.Name

    @Html.LabelFor(model => model.FirstName)
    @Html.EditorFor(model => model.FirstName)
    @Html.ValidationMessageFor(model => model.FirstName)

    Produces:
    FirstName

    • http://davybrion.com Davy Brion

      d’oh! wish i had known about those Editor Templates yesterday

      oh well, i’ll consider it a good exercise :)

      • http://twitter.com/ntcoding Nick

        Yeah, always good to see someone new to MVC coming up with their own ideas. Just incase you start forming habits – deffo keep up the posting – keeps us on our toes.

  • Orjan Sjoholm

    This is nice but not so dry, 5 lines over and over for every propp. Maintenace will be huge and the design will be impossible to change.

  • Anton Heryanto

    Any one know how to handle multiple prefix something like

    Reseller.Company.Address

    Company has template which easily handle using this approach, but
    Address has template which prefixed to company but not work as expected

  • http://jasonseney.com Jason

    This is a HUGE help -thank you so much! I’m re-using an Address form across the site in at least 3 places, and this is the perfect solution! :)

  • http://twitter.com/billybraga Billy Braga

    Thanks for the ViewDataDictionary.TemplateInfo.HtmlFieldPrefix tip !!!

  • Jesse van Assen

    I have been stuck with this issue for half a day now, and your post helped! many thanks!

  • Menaheme

    i stumbled across this on SO , i prefer adding a template that to inject all this code
    http://stackoverflow.com/questions/4964519/mvc-3-with-forms-and-lists-default-model-binder-and-editorfor

  • Nilesh hirapra

    You are great! Your solution saved my life. I was finding this solution since last 3 days. This solution is amazing.

  • Just

    This doesn’t work with data annotations when rendering a model that fails validation because this data is in the ViewData dictionary.  None of the validation messages will be sent into the partial, so validation won’t happen.  In fact, you’ll likely wind up with a broken page as a result as the rendering pipeline gets hosed up by the changed view data dictionary.

  • Kevin

    Thank you very much for this