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.
Pingback: Tweets that mention Prefixing Input Elements Of Partial Views With ASP.NET MVC -- Topsy.com