Reference

Custom Display and Editor Templates with ASP.NET MVC 3 Razor

Much has been written on how to create custom templates for ASP.NET MVC 2 and earlier versions, as well as the default MVC view engine. Customizing the default templates in MVC 3 using the Razor view engine is just as easy, but not exactly obvious if one doesn’t know where to start. This article provides some quick example code for replacing the default custom templates for Object view models.

Tags: csharp mvc programming razor web

Prerequisites

It is assumed that you are already familiar with ASP.NET MVC 3 and Razor, as well as the implementation of view models and their rendering using the Html.DisplayFor and Html.EditorFor or related HTML helper functions.

Introduction

When displaying the details or form for a view model using any of the built-in HTML helper functions, the MVC view engine needs to choose a suitable template for rendering. When no matching template can be found for the given view model, the view engine will use a built-in default template. There are several default templates for commonly used data types, such as Boolean, String and HiddenInput. For everything else, the view engine uses a default template for Objects.

All default templates are compiled into the DLL for the System.Web.Mvc namespace and not directly accessible. However, the full source code for ASP.NET MVC 3 RTM is available on Codeplex, and a quick scan through the archive reveals two interesting files.

The first file, mvc3/src//SystemWebMvc/Mvc/Html/DefaultEditorTemplates.cs contains the implementation for the Object default editor template as follows:

internal static string ObjectTemplate(HtmlHelper html, TemplateHelpers.TemplateHelperDelegate templateHelper) {
   ViewDataDictionary viewData = html.ViewContext.ViewData;
   TemplateInfo templateInfo = viewData.TemplateInfo;
   ModelMetadata modelMetadata = viewData.ModelMetadata;
   StringBuilder builder = new StringBuilder();

   if (templateInfo.TemplateDepth > 1) {    // DDB #224751
       return modelMetadata.Model == null ? modelMetadata.NullDisplayText : modelMetadata.SimpleDisplayText;
   }

   foreach (ModelMetadata propertyMetadata in modelMetadata.Properties.Where(pm => ShouldShow(pm, templateInfo))) {
       if (!propertyMetadata.HideSurroundingHtml) {
           string label = LabelExtensions.LabelHelper(html, propertyMetadata, propertyMetadata.PropertyName).ToHtmlString();
           if (!String.IsNullOrEmpty(label)) {
               builder.AppendFormat(CultureInfo.InvariantCulture, "<div class=\"editor-label\">{0}</div>\r\n", label);
           }

           builder.Append("<div class=\"editor-field\">");
       }

       builder.Append(templateHelper(html, propertyMetadata, propertyMetadata.PropertyName, null /* templateName */, DataBoundControlMode.Edit, null /* additionalViewData */));

       if (!propertyMetadata.HideSurroundingHtml) {
           builder.Append(" ");
           builder.Append(html.ValidationMessage(propertyMetadata.PropertyName));
           builder.Append("</div>\r\n");
       }
   }

   return builder.ToString();
}

The second file, mvc3/src/MvcFuturesFiles/DefaultTemplates/EditorTemplates/Object.ascx, contains the same template as a partial page using the syntax of the default view engine:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<script runat="server">
   bool ShouldShow(ModelMetadata metadata) {
       return metadata.ShowForEdit
           && metadata.ModelType != typeof(System.Data.EntityState)
           && !metadata.IsComplexType
           && !ViewData.TemplateInfo.Visited(metadata);
   }
</script>
<% if (ViewData.TemplateInfo.TemplateDepth > 1) { %>
   <% if (Model == null) { %>
       <%= ViewData.ModelMetadata.NullDisplayText %>
   <% } else { %>
       <%= ViewData.ModelMetadata.SimpleDisplayText %>
   <% } %>
<% } else { %>
   <foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => ShouldShow(pm))) { %>
       <% if (prop.HideSurroundingHtml) { %>
           <%= Html.Editor(prop.PropertyName) %>
       <% } else { %>
           <% if (!String.IsNullOrEmpty(Html.Label(prop.PropertyName).ToHtmlString())) { %>
               <div class="editor-label"><%= Html.Label(prop.PropertyName) %></div>
           <% } %>
           <div class="editor-field"><%= Html.Editor(prop.PropertyName) %> <%= Html.ValidationMessage(prop.PropertyName, "*") %></div>
       <% } %>
   <% } %>
<% } %>

These two files provide a pretty good idea as to what happens in the default template and how that functionality can be replicated for Razor based templates.

A Razor based Default Template

To customize the Object default editor template, perform the following steps:

  1. In the MVC project under Views/Shared/ create a new folder EditorTemplates
  2. Right-click the new EditorTemplates folder and select New Item…
  3. Select MVC 3 Partial Page (Razor) for the new item to add and name it Object.cshtml.

The view engine will now automatically be able to locate the new Object template in the project. Since this file is still empty, nothing will be rendered yet if the Html.EditorFor helper function is executed for a view model. The following code is a version of the default template using Razor syntax, and it is based on the two files shown earlier:

@functions
{
   bool ShouldShow (ModelMetadata metadata)
   {
       return metadata.ShowForEdit
           && metadata.ModelType != typeof(System.Data.EntityState)
           && !metadata.IsComplexType
           && !ViewData.TemplateInfo.Visited(metadata);
   }
}

@if (ViewData.TemplateInfo.TemplateDepth > 1)
{
   if (Model == null)
   {
       @ViewData.ModelMetadata.NullDisplayText
   }
   else
   {
       @ViewData.ModelMetadata.SimpleDisplayText
   }
}
else
{
   ViewData.Clear();

   foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => ShouldShow(pm)))
   {
       if (prop.HideSurroundingHtml)
       {
           @Html.Editor(prop.PropertyName)
       }
       else if (!string.IsNullOrEmpty(Html.Label(prop.PropertyName).ToHtmlString()))
       {
           <div class="editor-label">@Html.Label(prop.PropertyName)</div>
       }
       <div class="editor-field">@Html.Editor(prop.PropertyName) @Html.ValidationMessage(prop.PropertyName, "*")</div>
   }
}

The portion at the top contains a helper function ShouldShow that determines whether a particular property in the view model should be rendered. The bottom part contains the code to perform the actual rendering. Both parts can be customized as you see fit, and that’s pretty much it.

Note on ViewData.Clear()

Observant readers may have noticed that in the above example a call to ViewData.Clear has been added right before the foreach loop. The motivation for this is to remove a subtle problem with ViewBag properties being passed into the ViewData‘s dictionary. If the views’s ViewBag contains a property with the same name as a property in the model, EditorFor will populate the field’s value with the value of the ViewBag property instead of the model value.

It is unclear whether this behavior is a bug or desired feature. With the hard-coded default template helper the problem does not exist, because the ViewBag properties appear not to be included in the view data. By clearing the dictionary in our custom template, the model property’s value will be used for the field value, as expected. Since ViewData is a local property of each executed template, clearing the dictionary should have no side effects on other templates or views, and the ViewBag properties will remain available there.

Note on System.Data.EntityState

In the helper function ShouldShow you may have noticed the condition metadata.ModelType != typeof(System.Data.EntityState). This condition is used to filter out properties that were generated by the Entity Framework. Although the EntityState enumeration type is part of the Sytem.Data namespace, it’s implementation is actually contained within the System.Data.Entity.dll assembly instead of the System.Data.dll, as one would expect.

While the System.Data.dll assembly is generally added to MVC projects by default, the System.Data.Entity.dll assembly is not. For the above code to compile at run-time, the following small modification needs to be applied to the application’s main web.config file:

  1. Open the application’s main web.config file (not the web.config in the View folder or the area)
  2. Navigate to configuration → system.web → compilation → assemblies
  3. Add the following line:
    <add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
  4. Save the web.config file.

If you do not intend to use Entity Framework with your MVC application, you may also safely delete that particular condition in the template.