Series Index
- Part 1: Introduction
- Part 2: ModelMetadata
- Part 3: Default Templates
- Part 4: Custom Object Templates
- Part 5: Master Page Templates
Template Resolution
Before we talk about the built-in templates, we need to spend a few minutes understanding how template resolution works, so you’ll know how to override the template properly.
Paths
When a template is resolved, the system iterates over several names, looking for a template which matches. For each of the names, it asks the view engines to find a partial view named “DisplayTemplates/TemplateName” or “EditorTemplates/TemplateName”, depending on whether you’ve asked for a display or editor template.
If you’re using the WebForms view engine, that means it searches for display templates in the following locations:
- ~/Areas/AreaName/Views/ControllerName/DisplayTemplates/TemplateName.aspx & .ascx
- ~/Areas/AreaName/Views/Shared/DisplayTemplates/TemplateName.aspx & .ascx
- ~/Views/ControllerName/DisplayTemplates/TemplateName.aspx & .ascx
- ~/Views/Shared/DisplayTemplates/TemplateName.aspx & .ascx
(Replace DisplayTemplates with EditorTemplates for the search paths for editor templates.)
Template Names
The following template names are tried in order:
- TemplateHint from ModelMetadata
- DataTypeName from ModelMetadata
- The name of the type (see notes below)
- If the object is not complex: “String”
- If the object is complex and an interface: “Object”
- If the object is complex and not an interface: Recurse through the inheritance hiearchy for the type, trying every type name
When searching for the type name, the simple name is used (i.e., Type.Name) without namespace. Also, if the type is Nullable<T>, we search for T (so you’ll get the Boolean template whether you’re using “bool” or “Nullable<bool>”). This means if you’re writing templates for value types, you will need to account for whether the value is nullable or not. You can use the IsNullableValueType property of ModelMetadata to determine if the value is nullable. We’ll see an example of this below with the built-in Boolean template.
The TemplateInfo Class
One last thing we need to talk about before diving into the template implementations is the TemplateInfo class. The TemplateInfo is available off of ViewData, and unlike model metadata, is only populated when you’re inside of a template.
The primary property you will be using from TemplateInfo is FormattedModelValue. The value of this field is either the properly formatted model value as a string (based on the format strings on ModelMetadata), or the original raw model value (if there is no format string specified).
There are a couple other things we also use (TemplateDepth property and Visited method), which will be explained when we encounter them.
Built-In Display Templates
The system has built-in support for 9 display template names: “Boolean”, “Decimal”, “EmailAddress”, “HiddenInput”, “Html”, “Object”, “String”, “Text”, and “Url”. Two of these (“Text” and “String”) have the same implementation. Some of these have counterparts in the editor templates, and some do not.
The default templates in ASP.NET MVC are done in code, but here I’ve replaced their functionality as .ascx files, to illustrate what they do (and give you a starting point for customizing your own versions of all these templates).
DisplayTemplates/String.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %>
There’s nothing really surprising here: we just encode and display the model.
DisplayTemplates/Html.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%= ViewData.TemplateInfo.FormattedModelValue %>
This is even a little simpler yet, since the “Html” type tells us that the content is HTML and therefore should not be encoded. Be careful when marking your data as “Html” if it comes from end-users, since it opens up the possibility for cross-site scripting (XSS) attacks!
DisplayTemplates/EmailAddress.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <a href="mailto:<%= Html.AttributeEncode(Model) %>"><%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %></a>
This template assumes your model is an e-mail address, and uses it automatically create a mailto: link for it. Note how it uses Model for the e-mail address itself, but FormattedModelValue for the display; that lets you place format strings for display purposes, while still preserving the unedited e-mail address. This pattern is fairly common when the data is both mechanically meaningful (as in the e-mail address) but also displayed.
DisplayTemplates/Url.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <a href="<%= Html.AttributeEncode(Model) %>"><%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %></a>
As with EmailAddress above, this will interpret your model as a URL and automatically create a link to it.
DisplayTemplates/HiddenInput.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <% if (!ViewData.ModelMetadata.HideSurroundingHtml) { %> <%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %> <% } %>
This template is intended to be used with the [HiddenInput] attribute (described in Part 2 of this series). It will generate a display value only if the user explicitly asked for one, by consulting HideSurroundingHtml.
DisplayTemplates/Decimal.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <script runat="server"> private object FormattedValue { get { if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelMetadata.Model) { return String.Format(System.Globalization.CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelMetadata.Model); } return ViewData.TemplateInfo.FormattedModelValue; } } </script> <%= Html.Encode(FormattedValue) %>
This template displays decimal values with 2 digits of precision by default, since most users will use decimal values to represent currency. Note that it only does this if you haven’t applied a format string (which is the purpose of the check in the if statement).
DisplayTemplates/Boolean.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <script runat="server"> private bool? ModelValue { get { bool? value = null; if (ViewData.Model != null) { value = Convert.ToBoolean(ViewData.Model, System.Globalization.CultureInfo.InvariantCulture); } return value; } } </script> <% if (ViewData.ModelMetadata.IsNullableValueType) { %> <select class="list-box tri-state" disabled="disabled"> <option value="" <%= ModelValue.HasValue ? "" : "selected='selected'" %>>Not Set</option> <option value="true" <%= ModelValue.HasValue && ModelValue.Value ? "selected='selected'" : "" %>>True</option> <option value="false" <%= ModelValue.HasValue && !ModelValue.Value ? "selected='selected'" : "" %>>False</option> </select> <% } else { %> <input class="check-box" disabled="disabled" type="checkbox" <%= ModelValue.Value ? "checked='checked'" : "" %> /> <% } %>
The Boolean template is an interesting one, because it needs to generate UI that is different for non-nullable booleans vs. nullable booleans. It determines whether or not the model is supposed to be nullable based on IsNullableValueType from ModelMetadata.
The display UI for a non-nullable boolean is a disabled checkbox which is checked or not, depending on the value of the model. The display UI for nullable boolean is a disabled drop-down list with three potential values: “Not Set”, “True”, and "False”.
DisplayTemplates/Object.ascx
It’s worth explaining the logic for this before we look at the code, because the complex object template does a lot of work on your behalf.
The Object template’s primary responsibility is displaying all the properties of a complex object, along with labels for each property. However, it’s also responsible for showing the value of the model’s NullDisplayText if it’s null, and it’s also responsible for ensuring that you only show one level of properties (also known as a “shallow dive” of an object). In the next blog post, we’ll talk about ways to customize this template, including performing “deep dive” operations.
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <% if (Model == null) { %> <%= ViewData.ModelMetadata.NullDisplayText %> <% } else if (ViewData.TemplateInfo.TemplateDepth > 1) { %> <%= ViewData.ModelMetadata.SimpleDisplayText %> <% } else { %> <% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm))) { %> <% if (prop.HideSurroundingHtml) { %> <%= Html.Display(prop.PropertyName) %> <% } else { %> <% if (!String.IsNullOrEmpty(prop.GetDisplayName())) { %> <div class="display-label"><%= prop.GetDisplayName() %></div> <% } %> <div class="display-field"><%= Html.Display(prop.PropertyName) %></div> <% } %> <% } %> <% } %>
Let’s take a look at the source here, line by line, to understand which bit is doing what.
<% if (Model == null) { %> <%= ViewData.ModelMetadata.NullDisplayText %> <% }
This says we only want to print the model’s NullDisplayText if the model is null.
else if (ViewData.TemplateInfo.TemplateDepth > 1) { %> <%= ViewData.ModelMetadata.SimpleDisplayText %> <% }
This limits us to a single level of complex object ("shallow dive"). The TemplateInfo class tracks the depth of templates that you've shown automatically, and the TemplateDepth for the top level template will be 1.
else { %> <% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForDisplay && !ViewData.TemplateInfo.Visited(pm)))
This is the main loop when we're showing the objects properties. We filter the property list to remove anything where the user has said "do not display this". The other filter asks TemplateInfo if we've already rendered this object before; this helps us prevent infinite recursion when doing "deep dive" templates that might come from objects with circular references (like a parent/child relationship where both objects have pointers to one another).
<% if (prop.HideSurroundingHtml) { %> <%= Html.Display(prop.PropertyName) %> <% }
If the user has asked to hide the surrounding HTML, then all we want to do is display the property by itself. We don't want any of the "extra" stuff around it, like labels.
<% if (!String.IsNullOrEmpty(prop.GetDisplayName())) { %> <div class="display-label"><%= prop.GetDisplayName() %></div> <% } %> <div class="display-field"><%= Html.Display(prop.PropertyName) %></div>
This will show the display name of the property, surrounded by a div tag, if the display name is not null or empty. Since the display name is not empty by default (it will be the name of the property), that means the user must request for the display name to be hidden by explicitly setting it empty (using [DisplayName] if you're using the default DataAnnotations metadata provider).
Built-In Editor Templates
The editor templates will be slightly more complicated than the display templates, since they include the ability to edit the values. They are built upon the existing HTML helpers. There are 7 built-in editor templates: “Boolean”, “Decimal”, “HiddenInput”, “MultilineText”, “Object”, “Password”, and “String”.
One thing you’re going to see that’s unusual here is that we will often by passing empty string as the name to the HTML helpers. Normally this isn’t legal, but in the case of templates, we keep a “context” which tracks where we are in the name of things. This is helpful with complex objects inside of complex objects, because we want our names to indicate where we are in the hierarchy of things (f.e., “Contact.HomeAddress.City” vs. “Contact.WorkAddress.City”).
When you pass a name to the HTML helpers, you’re saying things like “give me a textbox to edit the property of this object named ‘City’.” But what happens when your template isn’t for the address (complex object), but instead for the city (a simple string)? Passing an empty string to the HTML helper says “give me a textbox to edit myself.”
EditorTemplates/String.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%= Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { @class = "text-box single-line" }) %>
Again we start with String, because it’s the simplest template to understand. This tells the system we want a text box (to edit myself), and we want it populated initially with the value of the formatted model value. In addition, we want to attach two CSS classes to the text box, “text-box” and “single-line”. With many of the CSS classes that we use here, you will find that we have provided default styles in MVC 2 that make them look slightly better out of the box.
EditorTemplates/Password.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%= Html.Password("", ViewData.TemplateInfo.FormattedModelValue, new { @class = "text-box single-line password" }) %>
The Password template is similar to String, except that it calls Html.Password and it adds a third CSS class ("password") to the rendered control.
EditorTemplates/MultilineText.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <%= Html.TextArea("", ViewData.TemplateInfo.FormattedModelValue.ToString(), 0, 0, new { @class = "text-box multi-line" }) %>
Again, no big surprises here. We call TextArea, and we pass row and column size as 0 (since we style the text area with CSS), and use the CSS class "multi-line" instead of "single-line".
EditorTemplates/HiddenInput.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <script runat="server"> private object ModelValue { get { if (Model is System.Data.Linq.Binary) { return Convert.ToBase64String(((System.Data.Linq.Binary)Model).ToArray()); } if (Model is byte[]) { return Convert.ToBase64String((byte[])Model); } return Model; } } </script> <% if (!ViewData.ModelMetadata.HideSurroundingHtml) { %> <%= Html.Encode(ViewData.TemplateInfo.FormattedModelValue) %> <% } %> <%= Html.Hidden("", ModelValue) %>
This is a lot more complex than the display version, because it has to do a lot more. The ModelValue property determines if the model is a LINQ to SQL Binary object or a byte array, and converts the value into a Base64 encoded value if so; otherwise, the raw model value is placed into the hidden input.
In addition to rendering the hidden input, it also needs to know if it should generate a display of the value.
EditorTemplates/Decimal.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <script runat="server"> private object ModelValue { get { if (ViewData.TemplateInfo.FormattedModelValue == ViewData.ModelMetadata.Model) { return String.Format(System.Globalization.CultureInfo.CurrentCulture, "{0:0.00}", ViewData.ModelMetadata.Model); } return ViewData.TemplateInfo.FormattedModelValue; } } </script> <%= Html.TextBox("", ModelValue, new { @class = "text-box single-line" }) %>
The Decimal editor template is nearly identical to the display template version, except that it ends up generating a text box for editing the value.
EditorTemplates/Boolean.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <script runat="server"> private List<SelectListItem> TriStateValues { get { return new List<SelectListItem> { new SelectListItem { Text = "Not Set", Value = String.Empty, Selected = !Value.HasValue }, new SelectListItem { Text = "True", Value = "true", Selected = Value.HasValue && Value.Value }, new SelectListItem { Text = "False", Value = "false", Selected = Value.HasValue && !Value.Value }, }; } } private bool? Value { get { bool? value = null; if (ViewData.Model != null) { value = Convert.ToBoolean(ViewData.Model, System.Globalization.CultureInfo.InvariantCulture); } return value; } } </script> <% if (ViewData.ModelMetadata.IsNullableValueType) { %> <%= Html.DropDownList("", TriStateValues, new { @class = "list-box tri-state" })%> <% } else { %> <%= Html.CheckBox("", Value ?? false, new { @class = "check-box" })%> <% } %>
The Boolean editor template is similar to the display template, except that it uses the built-in HTML helpers for DropDownList and CheckBox.
EditorTemplates/Object.ascx
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %> <% if (ViewData.TemplateInfo.TemplateDepth > 1) { %> <%= ViewData.ModelMetadata.SimpleDisplayText%> <% } else { %> <% foreach (var prop in ViewData.ModelMetadata.Properties.Where(pm => pm.ShowForEdit && !ViewData.TemplateInfo.Visited(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 Editor template for Object is nearly identical to the Display template, except that now we’ve added a call to ValidationMessage so that our default complex object editor will show error message asterisks.
Wrapping Up
Hopefully this post has helped you understand what the built-in templates are and exactly what each one of them does. You should be able to take these snippets of user controls and adapt them to create your own customized templates now. In the next blog post, I’ll discuss some of the quick (and not-so-quick) customizations you can do to alter the way your templates work.