Thursday, April 21, 2011

Display and Editor Templated View Helpers in ASP.NET MVC

Templated view helpers simplify your work greatly by allowing you to specify that rendering is required for display or input via Display and Editor templated Helpers respectively without needing to explicitly specify what Html element to map the properties to, in our model.

For example, given the following controller that is passing a model to the view :

// Get: /Home/Edit/1
public ActionResult Edit(int id)
{
 var p = GetPerson(id);
 return View(p);
}

Now, without resorting to Templated View Helpers, in order to display our model for edit, in the view we can attempt to offload the task of rendering an appropriate Html element for a property in our model to a View Helper :

@model MvcLab.Models.Person

@{
    ViewBag.Title = "Edit a person";
}

@using (Html.BeginForm()) {
    @Html.TextBox(Model.FirstName);
}


in the above example, Html.TextBox will output :

<input id="FirstName" name="FirstName" 
       type="text" value="Alessandro" />



By using the Html.TextBox helper, we were able to specify explicitly what input element we wanted mapped to a property in our model.

This is nice but we've had to explicitly state that we wanted a TextBox mappped to the FirstName field.

The same can be achieved by using Templated View Helpers which are yet a more convenient way to associate an Html element to a property in our model :

@using (Html.BeginForm()) {
    @Html.EditorFor(x => x.FirstName);
}

The above piece of code will also render an Html input element :

<input class="text-box single-line" 
      id="FirstName" name="FirstName" type="text" value="Alessandro" />

The output is pretty much the same, but notice that we didn't explicitly state what element to associate to the FirstName property. How this works is that it bases its assumption on the data type of the property and whatever model attribute meta data decorations set on it.

In ASP.NET MVC 2 onwards, there are 3 Editor Helpers that do the same thing with slight differences in usage. @Html.Editor, @Html.EditorFor and @Html.EditorForModel, the first can take a string containing our property name and the second a strongly typed model to property mapping expression and the third will just use the strongly typed model passed to the view by default.

They each provide some flexibility how the Editor Template View Helper is used. We'll be using @Html.EditorFor and @Html.EditorForModel for the remainder of this post.

The following piece of code in the view uses EditorForModel.

@model MvcLab.Models.Person

@{
    ViewBag.Title = "A person";
}

@using (Html.BeginForm()) {
    @Html.EditorForModel();
<p><input type="submit" value="Save" /></p>
}

<p>
    @Html.ActionLink("Back to List", "Index")
</p>

Something as simple as @Html.EditorForModel(); will output an input element for each property in our model. Notice how we didn't loop nor needed to pass the model. Since we're passing the Model in the view, using the default no arguments overloads works just nicely. The above piece of code in the view will output to screen :


While this renders our model, we can see that the output needs a bit more tweaking, for example the field labels are using the property name, this can be tweaked on the model itself using attribute decorations such as :

[DisplayName("First name")]
public string FirstName { get; set; }

Next, the id field is displaying. We'd rather this was not displayed, as this too can be compensated for by providing another attribute decoration on the property such as :

[HiddenInput(DisplayValue = false)]
public int PersonId { get; set; }

Completed person class after the noted changes :

public class Person
{
 [HiddenInput(DisplayValue = false)]
 public int PersonId { get; set; }
 [DisplayName("First name")]
 public string FirstName { get; set; }
 [DisplayName("Last name")]
 public string LastName { get; set; }
 public Address Residence { get; set; }
}

This renders as below :



That's much better, yet now we can note that our complex property Residence, which is a class with its own set of properties, did not render. In order to fix this we can try the following :

@using (Html.BeginForm()) {
    @Html.EditorForModel();
 @Html.EditorFor(x => x.Residence);
<p><input type="submit" value="Save" /></p>
}

<p>
    @Html.ActionLink("Back to List", "Index")
</p>

While this is all great and works by convention, we do eventually lose fine grained control over how each field is rendered in the end. One option is to go back to rendering each field individually eg: @Html.EditorFor(x => x.FirstName) directly in the view, but then if we need to reuse this model in another view, we keep repeating each field over and over again. The extra code in each view also begins to weigh on us and becomes unmaintainable sooner than later.

One way to solve this is to move the logic into a Partial view which can then be reused in a single line of code on other views where we'd want to render a Person model. An even nicer approach is to use convention, again designating Editor and Display templates for our Templated View Helpers. Using this approach will result in providing us with fine grained control over the output. Lets try that now.

Instead of creating the rendering for our model in the view directly, lets move it to a partial view. Following convention for Templated View Helpers, the partial view (template) needs to be stored in the following locations :
/Views/Shared/DisplayTemplates/TemplateName.cshtml and /Views/Shared/EditorTemplates/TemplateName.cshtml.

And since our model is a Person object, we will name our template Person.cshtml for both Display and Editor templates. This is to follow convention as that is the template it will look for based on the type name. There is still more flexibility in defining a template by using meta data attributes UIHint and passing a template name there or by passing a template explicitly in the Templated View Helper itself.

To keep the post simple we are going to use only default convention and setup custom EditorTemplates as the procedure is the same for DisplayTemplates. Along with the article I've included a sample application that shows usage for both Display and Editor templates.

/Views/Shared/EditorTemplates/Person.cshtml :

@model MvcLab.Models.Person

@Html.ValidationSummary(true)
<fieldset>
    <legend>Person</legend>

    @Html.HiddenFor(model => model.PersonId)

    <div class="editor-label">
        @Html.LabelFor(model => model.FirstName)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.FirstName)
        @Html.ValidationMessageFor(model => model.FirstName)
    </div>

    <div class="editor-label">
        @Html.LabelFor(model => model.LastName)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.LastName)
        @Html.ValidationMessageFor(model => model.LastName)
    </div>
</fieldset>

<!-- Note that the Residence property is a complex type for whom
 we've defined a template too in : Views\EditorTemplates\Address.cshtml -->
 @Html.EditorFor(model => model.Residence)

and now a Template for Address, though we could have done it in the Person object itself, as this gives us the flexibility of reusing the Address models rendering in any view.

/Views/Shared/EditorTemplates/Address.cshtml

@model MvcLab.Models.Address

    <fieldset>
        <legend>Address</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.City)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.City)
            @Html.ValidationMessageFor(model => model.City)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Country)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Country)
            @Html.ValidationMessageFor(model => model.Country)
        </div>
    </fieldset>

Note that, upto this point, we have not written a single line of code. What we see so far is all auto generated scaffolding when electing to set a strongly typed class on a partial view in Visual Studio itself. This is much quicker and we're left with remodeling the markup to fit our design requirements.

Below is a screenshot of the Add View dialog that we've used to autogenerate most of the code above.



Next, we are finally ready to reuse these templates in our views :

/Views/Home/edit.csHtml

@model MvcLab.Models.Person

@{
    ViewBag.Title = "Edit a person";
}

@using (Html.BeginForm()) {
    @Html.EditorForModel();
<p><input type="submit" value="Save" /></p>
}

<p>
    @Html.ActionLink("Back to List", "Index")
</p>


Note that the code in our view and every other view that wants to reuse the Person model has gone down to a single method call : @Html.EditorForModel()

and the output :




Final conclusions :
As we have seen, by using Templated View Helpers, we were able to promote reusability of our models rendering and in the process we were not limited by typical black box solutions as we were able to gain fine grained control by defining custom templates for our models.

Best yet, we followed convention over configuration and ended up doing little work with improved productivity and maintainability of our code. Following the same procedure here on this post, we can define custom templates for Display View Helpers too in the same way we provided templates for Editors.

The sample application provided along with this post defines templates for both Display and Editor Helpers.

This is what it all looks like in solution explorer :

Sample application : download
Reference material :
http://msdn.microsoft.com/en-us/library/ee402949.aspx
http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.uihintattribute.aspx

2 comments:

  1. After hours of searching I found this post and I must say it's right on point with what I needed. Excellent article!

    I am confused, however: you mention you split out Address for the potential to use it separately outside of what you've done here? How is that accomplished?

    ReplyDelete
  2. Thanks for a well-written useful article :)

    ReplyDelete