Over the last few chapters, you've learned in some detail about how ASP.NET MVC lets you separate your HTML generation and application logic concerns using views and controllers. But most web applications ultimately revolve around data. Even if the user interface is very sophisticated and customized, it's typically all about letting users browse and edit information. In MVC architecture, we use the term models for the data objects being passed between controllers and views. These models can be sophisticated domain models that encapsulate business logic and are persisted as entities in a database, or they can be just simple view models that are never saved and merely hold a set of properties that a view can render.
ASP.NET MVC tries not to have strong opinions about how your models should work. Unlike Ruby on Rails, for example, ASP.NET MVC doesn't expect you to inherit model classes from a certain base class or use a particular database access technology. Instead, its goal is to leave you in control so you can use anything from the extensive .NET ecosystem.
But if the MVC Framework doesn't know anything about your models, how can it automatically handle any of the tedious work related to data entry and display? In this chapter, you'll learn how the framework uses conventions to cope with most situations simply by inspecting the property names and types on your models, and offers extensibility mechanisms to let you provide extra information or alternative conventions, in each of the following use cases:
Using templated view helpers to generate portions of user interface directly from your models and their properties
Using model binding to parse HTML form submissions automatically
Integrating validation into the request-handling pipeline and generating client-side validation scripts
First, it's useful to understand how the MVC Framework's built-in conventions mesh together to help you handle data entry. Figure 12-1 illustrates how the framework provides two-way mapping between server-side .NET model objects and client-side HTML forms.
When you use HTML helpers, they render HTML form elements with names and values corresponding to properties on your model. In a moment you'll learn how to push this process further using templated view helpers—a more powerful conventions-based approach to generating HTML.
The cycle is completed later, when the user posts the HTML form back to your server. The model binding system uses equal and opposite conventions to map the incoming data back on to the same model object types. This symmetry lets your controllers work purely in terms of .NET objects without manually needing to parse incoming data, and the whole cycle creates a lightweight kind of statefulness that means your form elements effectively retain their contents after a validation failure.
This is totally unrelated to how ASP.NET Web Forms does postbacks and maintains control state. ASP.NET MVC's controllers don't follow the Web Forms page life cycle, and its views don't maintain control state using any hidden form field similar to ViewState. Instead, ASP.NET MVC works using rendering and binding conventions that are described in this chapter, all of which you can extend, replace, or ignore if you wish.
All the HTML helpers that you saw in the previous chapter—such as Html.TextBoxFor()
and Html.ActionLink()
—specify explicitly which type of HTML element they should render. A new feature added in ASP.NET MVC 2, templated view helpers, gives you the further option of saying you want to render a display or an editor for a model object or one of its properties, but without saying what HTML elements it should render. The framework will then choose and render a template according to the type of the model object or property, selecting either a custom template or one of its default built-in templates.
As a simple example, your model object might have a property called Approved
. If this property is of type bool
, then Html.EditorFor(x => x.Approved)
will by default generate a check box with the label "Approved." Or, if Approved
is a string
, then it will generate a text box with the same label. Or, if Approved
is some custom model type, then it will use reflection and iterate over Approved
's properties, generating a suitable input element for each property. In each case, the input elements will have names matching the framework's conventions so that the form values can later be mapped back to the same model type.
This sounds very clever, but don't be misled into thinking you can always generate satisfactory user interfaces purely by automatic conventions. When crafting quality software, you'll want to finely tailor each of your application's UIs to aim for the optimal user experience, accounting for subtleties such as design aesthetics and user psychology. If you want to exercise such fine-grained control over the way you transform model data into HTML, views already give you all the power and flexibility you need.
So, if templated view helpers aren't intended to replace your manual control, what are they good for? They're generally used as smarter versions of partial views—rendering fragments within a larger view—and are especially useful in the following situations:
Displaying and editing custom types: For example, if your application deals with geospatial data, you might have a custom type called LatLong
. Whenever you want to render a display or editor for a model property called Location
of the type LatLong
, you can call Html.DisplayFor(x => x.Location)
or Html.EditorFor(x => x.Location)
, having already created suitable templates for displaying and editing that data type. This is just a convention-based alternative to manually invoking something like Html.Partial("LatLongEditor.ascx", Model.Location)
. Later in this chapter, you'll see an example of establishing a convention that DateTime
properties should be edited using a certain date picker widget.
Editing hierarchical models: If your model objects have properties with subproperties and sub-subproperties, then you need to name all of your HTML form elements according to model binding conventions if you want the user's input to be mapped back to your model automatically. Templated view helpers automatically observe this naming convention, and if you explicitly provide a template for each model type, you can remain in total control over the HTML output.
Scaffolding: This term refers to the framework's ability to generate entire display and data entry UIs directly from your model types. Similar to real-world scaffolding that assists builders during construction, these UIs are mainly useful as a temporary solution during development until you have time to replace them with properly customized UIs.
The MVC Framework includes the templated view helpers listed in Table 12-1. You'll find further details about each of them in this chapter.
Table 12-1. Built-In Templated View Helpers
Example | Purpose | |
---|---|---|
|
| Renders a read-only view of the specified model property, choosing a template according to the property's type and any metadata associated with it. |
|
| Strongly typed version of the previous helper. |
|
| Shorthand way of writing |
|
| Renders an edit control for the specified model property, choosing a template according to the property's type and any metadata associated with it. |
|
| Strongly typed version of the previous helper. |
|
| Shorthand way of writing |
|
| Renders an HTML |
|
| Strongly typed version of the previous helper. |
|
| Shorthand way of writing |
|
| Bypasses all templates and renders a simple string representation of the specified model property. |
|
| Strongly typed version of the previous helper. |
Most developers prefer to use the strongly typed (lambda expression-based) versions of these helpers, because they have the advantage of providing IntelliSense. Plus, if you later rename a model property, then a refactoring tool can automatically update all lambda expressions that reference it.
Let's begin our exploration by trying out the most dramatic of these helpers, Html.EditorForModel()
. We'll start by defining some model classes, Person
and Address
.
public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; } public bool IsApproved { get; set; } } public class Address { public string Line1 { get; set; } public string Line2 { get; set; } public string City { get; set; } public string PostalCode { get; set; } public string Country { get; set; } }
As you learned in the previous chapter, an action method could render an instance of Person
by invoking a strongly typed view inherited from System.Web.Mvc.ViewPage<Person>
. If the view contained the following:
<h2>Edit this person</h2>
<% using(Html.BeginForm()) { %>
<%: Html.EditorForModel() %>
<p><input type="submit" value="Save" /></p>
<% } %>
then it would render as a complete user interface, as shown in Figure 12-2.
The UI was generated by the framework's built-in default editor template for an arbitrary object. This template uses reflection and iterates over each of the object's simple properties,[72] and for each one it renders a label and a suitable editor for that property. Each of the form fields is named to match the corresponding model property, so when the user posts this form back to your server, the model binding system will automatically reconstitute the data as a Person
instance.
You'll learn more about both model binding and the default object editor template shortly. First, let's consider how you could customize and improve this UI.
.NET classes provide only a limited amount of information about the data they contain. They specify the .NET types and names of a set of properties, but by default, that's all they say. They don't give user-friendly names for the properties, they don't say whether a property is only for the software's internal use or whether an end user would be interested in the value, and they don't declare which set of values would be considered valid.
To bridge this gap, ASP.NET MVC uses model metadata to let you provide extra information about the meaning of your models. Model metadata influences how templated view helpers produce displays and editors, and it influences how the framework validates incoming data.
Out of the box, ASP.NET MVC will detect and respond to metadata attributes from the standard .NET System.ComponentModel
namespace. If these don't meet your needs, you can create custom metadata attributes, or you can make your own metadata provider by inheriting from the ModelMetadataProvider
base class.
As a simple example, you could improve the UI shown in Figure 12-2 by applying metadata attributes to the Person
class as follows:
public class Person {[HiddenInput (DisplayValue = false)] // Don't want the user to see or edit this
public int PersonId { get; set; }// [DisplayName] specifies user-friendly names for the properties
[DisplayName("First name")]
public string FirstName { get; set; }[DisplayName("Last name")]
public string LastName { get; set; }[DataType(DataType.Date)] // Show only the date, ignoring any time data
[DisplayName("Born")] public DateTime BirthDate { get; set; } public Address HomeAddress { get; set; }[DisplayName("May log in")]
public bool IsApproved { get; set; } }
With this metadata in place, the preceding view would render as shown in Figure 12-3. You'll learn about other standard metadata attributes shortly.
Right now, if you were to look at the generated HTML, you'd find it references a useful set of CSS classes, so you could style it easily. Here's what the default object template renders for the FirstName
model property (reformatted for readability):
<div class="editor-label
"> <label for="FirstName">First name</label> </div> <div class="editor-field
"> <input class="text-box single-line
" id="FirstName" name="FirstName" type="text" value="Blaise" /> </div>
For example, you could use CSS to make the labels and the input controls appear on the same lines instead of having the labels appear above the input controls.
Instead of using Html.EditorForModel()
to render an editor for an entire model object, it's also common to use Html.Editor()
or Html.EditorFor()
to place editors for individual properties at different locations in a view. This gives you much more control over the finished output, but still uses model metadata and templating conventions to influence the result.
For example, you could express the previous view as follows:
<% using(Html.BeginForm()) { %> <fieldset> <legend>Person</legend> <div class="field"> <label>Name:</label><%: Html.EditorFor(x => x.FirstName) %>
<%: Html.EditorFor(x => x.LastName) %>
</div> <div class="field"> <label>Born:</label><%: Html.EditorFor(x => x.BirthDate) %>
</div> <div align="center"><%: Html.EditorFor(x => x.IsApproved) %>
May log in</div> <fieldset> <legend>Home address</legend> <div class="addressEditor"><%: Html.EditorFor(x => x.HomeAddress) %>
</div> </fieldset> </fieldset> <p><input type="submit" value="Save" /></p> <% } %>
Here, we're using the strongly typed Html.EditorFor()
helper, which is only available in strongly typed views. You could get the same result by calling the string-based alternative (i.e., Html.Editor("FirstName")
), which is available in loosely typed views, but doesn't have the benefits of IntelliSense and refactoring support.
This would render as shown in Figure 12-4.
This is now very similar to using simple HTML helpers such as Html.TextBoxFor()
and Html.CheckBoxFor()
, but with a few advantages:
You don't have to specify which input controls should be used. In this example, you can see the framework automatically chooses to use a check box for the bool
property.
Model metadata is respected. As you can see, the birth date is shown without an associated time because of the [DataType(DataType.Date)]
metadata attribute.
You can get scaffolding for child properties without extra work. In this example, we've made a basic address editor simply by writing Html.EditorFor(x => x.HomeAddress)
. Later, you'll see how to supply custom templates to replace the scaffolding.
If you prefer to generate the input control labels directly from model metadata, you can use Html.Label()
or Html.LabelFor()
. For example, you could render the text label for the IsApproved
property as follows:
<%: Html.LabelFor(x => x.IsApproved) %>
This will render as follows:
<label for="IsApproved">May log in</label>
This time, the phrase "May log in" is taken from the [DisplayName("May log in")]
metadata attribute instead of being specified in the view. If there was no such metadata, it would fall back on displaying the actual property name.
This isn't necessarily any better than writing the label text directly into a view. After all, views are the normal place in MVC architecture for UI details such as label text. The most likely reason to use Html.Label()
or Html.LabelFor()
is if you're using the same model class on multiple data entry views—you may want to define the label texts in one central place (i.e., in a [DisplayName]
attribute on the model) so it's consistent across all the views.
You'll learn in Chapter 17 how to display fragments of text from a RESX resource file. This gives you an alternative way to reference text resources defined in a central location, and makes it very easy to support translation into multiple languages. This is usually better than [DisplayName]
for large or international applications, since [DisplayName]
doesn't directly support any kind of localization.
When you use Html.EditorFor()
or a similar helper, ASP.NET MVC needs to choose a suitable template to render. It does so by considering each of the following template names, in this order of priority:
Any template name explicitly specified in your call to the HTML helper—for example, if you call Html.EditorFor(x => x.SomeProperty, "MyTemplate")
.
Any template name specified by model metadata—for example, if you attach an attribute such as [UIHint("MyTemplate")]
to the model property. You'll learn more about [UIHint]
and the ModelMetadata.TemplateHint
property shortly.
The data type name specified by model metadata—for example, if you attach an attribute such as [DataType("MyDataType")]
or [DataType(DataType.EmailAddress)]
to the model property. You'll learn more about [DataType]
and the ModelMetadata.DataTypeName
property shortly.
The actual .NET type name for the chosen model object or property. Note that it unwraps nullable types automatically, so properties of type int, int?
, or System.Nullable<System.Int32>
will all be mapped to the template name Int32
.
If the chosen model object or property is of a "simple" type (i.e., it can be converted from a string
using TypeDescriptor.GetConverter
, which includes common types such as int
and DateTime
), then it uses the built-in template name String
.
If the chosen model object or property is not an interface, then it tries the names of all base types, up to but excluding Object
.
If the chosen model object or property implements IEnumerable
, then it uses the built-in template name Collection
.
Finally, it falls back on using the built-in template name Object
.
For each of these possible template names, the framework will first try to find a template by asking your active view engine for a partial called EditorTemplates/
templateName
. If one is found, it gets invoked, and the process is complete. You learned in Chapter 9 how the view engine searches for views and partials in the folders /Areas/
areaName
/Views/
controllerName
, /Areas/
areaName
/Views/Shared, /Views/
controllerName
, and /Views/Shared
. Note that these view lookups are cached within each HTTP request, so the scanning process is not as expensive as you might imagine.
If the view engine can't find any partial matching the requested name, the framework will consider using a built-in default editor template. Table 12-2 lists all of the built-in editor templates.
Table 12-2. The Built-In Editor Templates
Template Name | Behavior |
---|---|
| For regular |
| For |
| Renders a text box by called |
| Renders a hidden input control by calling |
| Renders an HTML |
See the description just after the end of this table. | |
| Renders a password input control by calling |
| Renders a text box by calling |
| Behaves exactly like the |
You can override any of these default templates by creating a custom editor template partial with the same name. For example, you can override the editor template for Boolean
by creating a partial view at /Views/Shared/EditorTemplates/Boolean.ascx
. You'll learn more about custom templates later in the chapter.
The Object
template deserves special attention because it's a little more sophisticated. This is the ultimate fallback template, and is typically what's used during scaffolding, because you'll call Html.EditorFor(x => x.SomeComplexProperty)
or Html.EditorForModel()
without having defined an editor template for your property or model type.
The Object
template iterates over each of your type's properties, rendering a label, an editor, and a validation message for each one (you'll learn more about validation later in this chapter). For each property, it produces HTML of the following form:
<div class="editor-label"><label for="propertyName
">property label
</label></div> <div class="editor-field">propertyeditor template goes here
</div>
What might surprise you is that first, the default Object
template will skip any properties that are not simple (again, this is defined as being convertible from a string
using TypeDescriptor.GetConvertor
). Otherwise, it might cause unwanted side effects such as triggering lazy-loading.
Second, it can't be invoked from any other template: if you try to invoke the Object
template from inside a different template, it will merely display the model property as a simple read-only string and won't iterate through its subproperties. In summary, scaffolding is not recursive.
So far, we've only considered the templated view helpers for editing data: Html.Editor(), Html.EditorFor()
, and Html.EditorForModel()
. Those are the most commonly used, but there are also read-only display equivalents: Html.Display(), Html.DisplayFor()
, and Html.DisplayForModel()
. The display helpers behave exactly like the editor helpers, except they use a different set of default templates, and they look for template partials in a different folder (they look for partials called DisplayTemplates/
templateName
instead of EditorTemplates/
templateName
).
The framework's built-in default display templates are listed in Table 12-3.
Table 12-3. The Built-In Display Templates
Behavior | |
---|---|
| Just like the equivalent editor template, renders a check box or a drop-down list. The difference is that the control is disabled by adding the HTML attribute |
| For |
| Displays the value with two decimal places. |
| Renders an HTML |
| Renders a read-only string display of the property value, unless the model metadata's |
| Renders the property value without HTML-encoding it. This is useful if the property value contains HTML tags that you know are safe to display. |
| See the explanation that follows this table. |
| Renders the property value after HTML-encoding it. |
| Behaves exactly like the |
| Renders an HTML link tag—for example, |
The built-in Object
display template works almost exactly like the equivalent editor template, in that it iterates over simple properties and produces a display for each one. For each property, it generates HTML of the following form:
<div class="display-label">property label
</div> <div class="display-field">property value
</div>
Again, it isn't recursive: it won't iterate over your properties if you call it from inside another template.
Continuing the previous example, an action method could render an instance of Person
by invoking a strongly typed view inherited from System.Web.Mvc.ViewPage<Person>
. If the view contained the following:
<h2>Examine this person</h2>
<%: Html.DisplayForModel() %>
then it would render each of the properties as shown in Figure 12-5.
Clearly, this is not a very attractive user interface. You could improve it by applying CSS styles, or by calling Html.DisplayFor()
for each property within a more well-structured page instead of calling Html.DisplayForModel()
, just like we did with Html.EditorFor()
.
A further option is to create a custom template for the Person
type. Let's move on and consider how to do that.
If you're making heavy use of templated view helpers, you'll probably want to go beyond the built-in default editor and display templates (e.g., String, Object, Collection
, as listed in Tables 12-3 and 12-4) and create custom templates. Custom templates are nothing more than partial views named and placed in a particular way so the framework can find them as part of the template selection process.
You've already learned how ASP.NET MVC asks your view engine for a partial called EditorTemplates/
templateName
or DisplayTemplates/
templateName
, where templateName
is an explicitly provided name, a template name given by model metadata, the name of the .NET type, or a built-in default template name. So, if you want to change how Person
instances are displayed, you could create a partial at /Views/Shared/DisplayTemplates/Person.ascx
, strongly typed with model class Person
, containing the following:
<%@ Control Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<Namespace
.Person>" %>
<div class="person">
<img class="person-icon" src="/content/person-icon.png" />
<%: Html.ActionLink(Model.FirstName + " " + Model.LastName,
"Edit", new { Model.PersonId }) %>
(born <%: Model.BirthDate.ToString("MMMM dd, yyyy") %>)
<div class="address">
<%: Html.DisplayFor(x => x.HomeAddress, "AddressSingleLine") %>
</div>
</div>
This partial explicitly references another display template called AddressSingleLine
, so you'll need to create another partial in the same folder called AddressSingleLine.ascx
, perhaps containing the following:
<%@ Control Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<Namespace
.Address>" %>
<%: string.Join(", ",
new[] {
Model.Line1, Model.Line2, Model.City, Model.PostalCode, Model.Country
}.Where(x => !string.IsNullOrEmpty(x)).ToArray()
) %>
Now, without any further changes to the original view, the Person
instance would be rendered as shown in Figure 12-6 (CSS styles added).
This is not a unique achievement. You could easily set up a similar structure of views and partials without using the templating system, rendering your partials using Html.Partial()
or Html.RenderPartial()
as described in the previous chapter. The added benefit of using templates for display is that you can establish standard conventions about how certain model types should be rendered, and then you don't have to remember which partials to invoke. For example, you could now render an enumerable collection of Person
instances with a single line of view markup—for example:
<%: Html.DisplayFor(x => x.MyPersonCollection) %>
This would render the Person.ascx
partial once for each item in the collection. You need to judge whether, for your own application, conventions like this will save you time or merely make it harder to guess what your code will do at runtime.
You're not limited to defining templates for your own custom model types; you can also define templates for standard .NET types or templates that overwrite the built-in defaults that you saw in Tables 12-3 and 12-4.
For example, you could create a standard editor template for DateTime
properties by creating a partial at /Views/Shared/EditorTemplates/DateTime.ascx
, containing the following:
<%@ Control Language="C#" Inherits="ViewUserControl<DateTime?>" %> <%: Html.TextBox("", /* Name suffix */ ViewData.TemplateInfo.FormattedModelValue, /* Initial value */ new { @class = "date-picker" } /* HTML attributes */ ) %>
When creating templates for value types such as int
and DateTime
, it's wise to set your model type to the nullable equivalent (in this example, DateTime?
) because ASP.NET MVC will expect your template to handle both nullable and nonnullable versions of the type. If you don't do this, then if the model property is nullable and holds null
, you'll simply get an error message.
Now that this partial exists, all DateTime
and DateTime?
property editors (everywhere in your application, except where overridden using an explicit template name or a [UIHint]
attribute ) will be rendered as text boxes with the CSS class date-picker
. We're passing an empty string for the name
parameter because the framework will automatically prefix this with the field name corresponding to the model item being rendered; you only need to specify a nonempty value if your editor is for a child property (HTML field prefixes are covered in the next section).
The resulting HTML will be similar to the following:
<input class="date-picker" id="PropertyName
" name="PropertyName
" type="text" value="PropertyValue
" />
You could then associate all such elements with client-side date picker widgets using a client-side toolkit such as jQuery UI by putting the following script into a site-wide master page:
<script type="text/javascript"> $(function() { $(".date-picker").datepicker(); // Turns matching elements into date pickers }); </script>
See jQuery UI's web site at http://jqueryui.com/
for more details about how to install and use it.
Since custom display or editor templates are usually implemented as strongly typed partial views, you can access the view's Model
property to obtain the value of the current model item. But when you want to render that model item as a string, don't directly call Model.ToString()
, as that would bypass any formatting metadata on the model. Instead, notice how the preceding sample code for DateTime.ascx
represents the model item as a string using ViewData.TemplateInfo.FormattedModelValue
—this respects formatting metadata, so this custom template behaves correctly when combined with [DataType(DataType.Date)]
or the [DisplayFormat]
attribute .
As a convenient simplification, ASP.NET MVC offers a specialized base class for display and editor templates. If you edit a partial view's Inherits
directive so that the partial inherits from ViewTemplateUserControl<
modelType
>, then you'll have access to a new property, FormattedModelValue
, which is shorthand for ViewData.TemplateInfo.FormattedModelValue
.
For example, the preceding DateTime.ascx
sample could be rewritten as follows:
<%@ Control Language="C#" Inherits="ViewTemplateUserControl<DateTime?>
" %> <%: Html.TextBox("", /* Name suffix */FormattedModelValue
, /* Initial value */ new { @class = "date-picker" } /* HTML attributes */ ) %>
Sometimes you might want to pass more than just the model to your custom template; you might also want to pass some additional parameters that influence how it should render the model.
This is easy—all the template-rendering helper methods (Editor, EditorFor, EditorForModel, Display, DisplayFor, DisplayForModel
) have overloads that let you pass a parameter called additionalViewData
. If you pass an anonymously typed object for this parameter, the framework will extract your object's properties and use them to populate your custom template's ViewData
dictionary.
For example, you might have a special date picker that handles time zones in some way. You could render a date editor as follows:
<%: Html.EditorFor(x => x.BirthDate, new { timezone = "PST" }
) %>
and then your custom DateTime.ascx
template would be able to receive the time zone parameter by reading it from ViewData["timezone"]
.
When editing data (as opposed to displaying a read-only view of it), there is a further benefit that comes with using templates rather than merely invoking partials directly. Templates introduce a notion of HTML field prefixes. When you render a hierarchy of templates nested inside other templates, the framework automatically builds up a string expression that uniquely describes your current location in the hierarchy. This provides two main benefits:
All of the built-in HTML helpers (e.g., Html.TextBox()
) respect this, prefixing the current value before any name
or id
attribute they render. This makes it much easier to keep all of your input elements' IDs unique, as required by the HTML specification.
The generated string expression describes the referenced property's location in the model object graph. For example, if you call <%: Html.EditorFor(x => x. MyPersonCollection[2]) %>
, which in turn calls <%: Html.EditorFor(x => x.HomeAddress) %>
, then the address template will prefix its input element names with MyPersonCollection[2].HomeAddress
. This exactly matches model binding conventions, so all the posted data can be reconstituted as a collection of People
instances. In a sense, this algorithm is the inverse of ViewData.Eval()
—it works out what string expression is necessary to reach each property.
With ASP.NET MVC 1.0, you had to manage HTML field prefixes manually. With the new templating system, it happens automatically. As a basic example of this feature at work, refer back a few pages to Figure 12-4, which shows a Person
editor that contains an Address
editor. The text box labeled "City" was rendered as the following HTML:
<input class="text-box single-line" id="HomeAddress_City
name="HomeAddress.City
" type="text" value="Paris" />
As you can see, the framework has prefixed the element's name
with HomeAddress
. The same applies to its id
, except that to satisfy the HTML 4.01 specification, it replaces characters other than letters, digits, dashes, underscores, and colons with HtmlHelper.IdAttributeDotReplacement
, which equals underscore (_
) unless you overwrite it.
When you're creating custom templates, you may want to follow these element name prefixing conventions. You can do that easily by using the properties and methods on ViewData.TemplateInfo
, as listed in Table 12-4.
Table 12-4. Properties and Methods on ViewData.TemplateInfo
Name | Purpose |
---|---|
| Returns a simple |
| Generates a valid HTML element ID value based on the current field prefix and a supplied ID suffix. For example, |
| Generates an HTML element name value based on the current field prefix and a supplied name suffix. For example, |
| Returns the current field prefix. |
| Returns an |
| Returns a |
When rendering ASP.NET MVC's built-in HTML helpers (such as Html.TextBox()
) from custom templates, you don't need pass prefix information to them. They automatically obtain and render the correct prefixes using ViewData.TemplateInfo
. You only need to handle this manually if you're constructing HTML tags manually.
As you've already seen in this chapter, ASP.NET MVC allows model objects to declare metadata that will influence how the framework displays and edits those model objects. For example, you can attach the attribute [DisplayName("First name")]
to a model property, and then the templated view helpers will use your supplied text whenever they render a label for that property.
Just like most of ASP.NET MVC's core mechanisms, the model metadata system is completely pluggable. The framework includes a comprehensive implementation, but also allows you to extend or completely replace it if you wish. Figure 12-7shows how this extensibility works.
The templated view helpers don't know anything about Data Annotations or any other specific metadata source. They only understand ModelMetadata
objects, which are ASP.NET MVC's standard way to describe metadata. Over the next few pages, you'll learn
Which Data Annotations attributes make a difference to ASP.NET MVC
How DataAnnotationaModelMetadataProvider
, the default metadata provider, maps these attributes onto ModelMetadata
properties
How you can extend or replace DataAnnotationaModelMetadataProvider
with your own custom metadata provider
How ModelMetadata
properties affect the templated view helpers
Using the .NET Framework's standard Data Annotations attributes in the System.ComponentModel
namespace and the System.ComponentModel.DataAnnotations.dll
assembly isn't the only way of defining model metadata, but it's the easiest because that's what the MVC Framework supports without any extra work on your part.
Table 12-5 shows which Data Annotations attributes make a difference to DataAnnotationsModelMetadataProvider
, how it maps them onto ModelMetadata
properties, and what effects these properties trigger.
Table 12-5. System.ComponentModel Attributes Recognized by DataAnnotationsModelMetadataProvider
Data Annotations Attribute | Effect |
---|---|
| Determines which child property |
| Affects template selection when rendering displays or editors for this item. Maps to |
| Affects template selection and how the built-in HTML helpers format the model value as text. Maps to |
| Maps to |
| Affects how the built-in HTML helpers represent the model value as text. For example, the format string |
| Controls whether the built-in |
| Affects all built-in helpers that render property labels, including |
| Causes the built-in |
In addition, ASP.NET MVC includes its own extra metadata attribute, [HiddenInput]
, that affects HideSurroundingHtml
and TemplateHint
, as described in Table 12-6. This extra attribute is needed because no equivalent metadata attribute exists in System.ComponentModel
.
If Data Annotations attributes and DataAnnotationsModelMetadataProvider
don't meet your needs, you can create a custom metadata provider by creating a class that inherits from one of the following base classes:
ModelMetadataProvider
: The abstract base class for all metadata providers.
AssociatedMetadataProvider
: Usually a better choice of base class for a custom metadata provider. It deals with much of the tricky work related to obtaining the list of attributes associated with each model property, and transparently fetches these attributes from "buddy" classes configured via [MetadataType]
. All you have to do is override a single method, CreateMetadata()
, and return a ModelMetadata
instance based on a supplied set of attributes, a model type, a property name, and so on.
DataAnnotationsModelMetadataProvider
: The default metadata provider. By inheriting from this, you can retain support for standard System.ComponentModel
Data Annotation attributes. Again, you only need to override its CreateMetadata()
method.
As an example, let's consider enhancing the default metadata provider so that it recognizes some extra naming conventions. You may have model classes similar to the following:
public class StockTradingRecord { public string SymbolName { get; set; } public DateTime TradingDate { get; set; } public decimal ClosingPrice { get; set; } public decimal HighPrice { get; set; } public decimal LowPrice { get; set; } }
and wish to set up conventions so that any property whose name ends with Date
will be displayed and edited as a date (ignoring any time component of the DateTime
value ), and any property whose name ends with Price
will be displayed as a currency value.
Since ASP.NET MVC only lets you enable one metadata provider at a time, the easiest way to add extra behavior without losing existing behavior is to inherit from an existing provider. Here's a custom provider that inherits from DataAnnotationsModelMetadataProvider
:
public class ConventionsMetadataProvider: DataAnnotationsModelMetadataProvider { protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attribs, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName) { var metadata = base.CreateMetadata(attribs, containerType, modelAccessor, modelType, propertyName); if (propertyName != null) { if (propertyName.EndsWith("Date"))
metadata.DisplayFormatString = "{0:d}"; // Date format else if (propertyName.EndsWith("Price")) metadata.DisplayFormatString = "{0:c}"; // Currency format } return metadata; } }
To activate this custom metadata provider, assign an instance of it to the static ModelMetadataProviders.Current
property. For example, alter Application_Start()
in Global.asax.cs
as follows:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelMetadataProviders.Current = new ConventionsMetadataProvider();
}
Now our new conventions will apply. For example, StockTradingRecord
's ClosingPrice
property will automatically be displayed as a currency value (i.e., with two decimal places and a currency symbol) whenever rendered by Html.DisplayFor()
.
Alternatively, you could create a metadata provider that detects and responds to custom metadata attributes, or loads metadata settings from a configuration file, or recognizes a range of custom model types and uses alternative logic to associate matching properties with particular editor or display templates (you can override template selection by assigning a value to ModelMetadata
's TemplateHint
property). Let's now consider the full range of ModelMetadata
properties that you can set using a custom metadata provider.
At the risk of duplicating information that you could find online on MSDN, Table 12-6 gives a complete list of the writable properties you can set on a ModelMetadata
instance to influence the templating system. You can write to these properties either from a custom metadata provider, as shown in the previous example, or in many cases using built-in System.ComponentModel
attributes, as described in the following table.
The reason I include this complete list is that, at the time of writing, MSDN does not provide such clear information about how each property affects ASP.NET MVC's behavior. As you will see, not all of these properties are actively used by ASP.NET MVC 2.
Table 12-6. Public Writable Properties on ModelMetadata
Member | Meaning | Affects/Affected By |
---|---|---|
| Dictionary of arbitrary extra metadata values for use by custom providers. | Not set or used by any built-in part of ASP.NET MVC, but as explained later, can be used by custom metadata providers and HTML helpers. |
| Boolean value; | Can be set using |
Provides further information about the intended meaning of the property—for example, specifying whether it's a string that holds an e-mail address ( | Can be set using | |
| Holds a human-readable description of the model item. | Not set or used by any built-in part of ASP.NET MVC. |
| Holds a formatting string (e.g., | Can be set using |
| Provides a human-readable name for the property. | Can be set using |
| Just like | Can be set using |
| Describes whether scaffolding should render a label alongside this property's display or editor. | Can be set to |
| Specifies whether the item should be treated as read-only. | Can be set using |
Specifies whether the item should be validated as a required field. By default, this is | Can be set to | |
| Gets or sets the model object or property value that this | Automatically set when rendering any template, and used by most HTML helpers to determine an initial value. |
| Specifies the string to be displayed in place of a | Can be set using |
| Gets or sets the | Set automatically by the |
| A shorter human-readable description of the model item. | Not set or used by any built-in part of ASP.NET MVC. |
| Specifies whether the item should be included in scaffolded displays. | Can be set using |
| Specifies whether the item should be included in scaffolded editors. | Can be set using |
| Provides a simple string representation of the model item. Takes its default value from | You can control which child property value is used as the default |
Specifies the name of the template that should be used when rendering displays or editors for this item. | Can be set to an arbitrary value using | |
| Specifies text that could be overlaid onto empty input controls to act as a prompt for the user. | Not set or used by any built-in part of ASP.NET MVC. |
Any HTML helper can access the metadata associated with the model object or property that it is currently rendering. ModelMetadata
has two static methods, FromStringExpression()
and FromLambdaExpression()
, that retrieve the desired ModelMetadata
instance from ViewData
.
For example, you might want the IsReadOnly
metadata flag to cause text boxes to render in a disabled state (so the user can read but not edit the value). You could create your own wrapper around Html.TextBoxFor()
that does this.
public static class EnhancedTextBoxExtensions { public static MvcHtmlString TextBoxForEx<T, TProp>(this HtmlHelper<T> html, Expression<Func<T, TProp>> expr) { var metadata = ModelMetadata.FromLambdaExpression(expr, html.ViewData); bool isReadOnly = metadata.IsReadOnly; var htmlAttributes = isReadOnly ? new { disabled = "disabled" } : null; return html.TextBoxFor(expr, htmlAttributes); } }
In your views, you can now write Html.TextBoxForEx(x => x
.SomeProperty
), and the resulting text box will have the HTML attribute disabled="disabled"
if the referenced property has a [ReadOnly]
attribute.
Html.TextBoxForEx()
is an extension method, so it will only be available in views once you've referenced its namespace. See Chapter 11's coverage of custom HTML helpers for more details.
So far, our approach to defining metadata has relied mainly on Data Annotations attributes. This usually works well, but there's a complication if you can't edit your model classes' source code, perhaps because they're automatically generated by a tool such as the LINQ to SQL or Entity Framework designer. If you can't edit the model's source code, how can you add attributes to its properties?
The solution is to define its metadata on a separate class, known as a buddy class. This buddy class has no behavior and is used only to define metadata. The only requirement is that your real model class has to be defined as a partial class (fortunately, the LINQ to SQL and Entity Framework code generators do mark their entity classes as partial).
Continuing the earlier example with the Person
class, you could remove all the metadata attributes, defining it simply as follows:
public partial
class Person
{
public int PersonId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
public Address HomeAddress { get; set; }
public bool IsApproved { get; set; }
}
Notice, of course, that it's now marked as partial
. Next, you can separately declare a buddy class as follows, using the [MetadataType]
attribute to indicate that it should merge in metadata from properties on another class.
// Note: Must be in the same namespace as the other part of the partial definition [MetadataType(typeof(PersonMetadata))] public partial class Person { // This class is only used as a source of metadata private class PersonMetadata { [HiddenInput(DisplayValue = false)] public int PersonId { get; set; } [DisplayName("First name")] public string FirstName { get; set; } [DisplayName("Last name")] public string LastName { get; set; } // Also add any other properties for which you want to supply metadata } }
At runtime, any metadata provider that inherits from AssociatedMetadataProvider
(and this includes ASP.NET MVC's built-in default metadata provider) will recognize and respect the [MetadataAttribute]
on the partial class, and will use metadata from any properties with matching names on the nominated buddy class (i.e., PersonMetadata
). This means your real model class doesn't have to know anything about metadata, and it can safely be regenerated by a tool without losing your attributes.
Each time your site visitors submit an HTML form, your application receives an HTTP request containing the form's data as a set of name/value pairs. You could manually pick out each data item that you wish to receive (e.g., retrieving Request.Form["phoneNumber"]
), but this is labor intensive, especially if an action method needs to receive many data items and use them to construct or update a model object.
Model binding is ASP.NET MVC's mechanism for mapping HTTP request data directly into action method parameters and custom .NET objects (including collections). As you'd expect from ASP.NET MVC, it defines certain naming conventions to let you quickly map complex data structures without having to specify all the mapping rules manually.
You've already been using the framework's model binding feature, every time your action methods have taken parameters—for example:
public ActionResult RegisterMember(string email, DateTime dateOfBirth) { // ... }
To execute this action method, the MVC Framework's built-in ControllerActionInvoker
uses a component called DefaultModelBinder
and several implementations of IValueProvider
to convert incoming request data into a suitable .NET object for each action method parameter. Over the next few pages, you'll learn in detail how this works.
An IValueProvider
represents a supply of raw data arriving with an HTTP request. By default, the framework is configured to use the four value providers listed in Table 12-7, in the order of priority shown.
Table 12-7. Where Model Binding by Default Gets Its Raw Incoming Data (in Priority Order)
Value Provider | Retrieves Data From | How It Interprets String Values |
---|---|---|
|
| Culture sensitive ( |
|
| Culture insensitive ( |
|
| Culture insensitive ( |
|
| n/a |
So, the previous example's email
parameter would be populated from
Request.Form["email"]
, if it exists
Otherwise, RouteData.Values["email"]
, if it exists
Otherwise, Request.QueryString["email"]
, if it exists
Otherwise, Request.Files["email"]
, if it exists (although you would need to change the parameter type from string
to HttpPostedFileBase
in order to receive the uploaded file)
Otherwise, null
The equivalent is true for the dateOfBirth
parameter, but with two differences:
A
DateTime
value can't benull
, so if locations 1 through 4 were all empty, the framework would just throw anInvalidOperationException
saying, "The parameters dictionary contains a null entry for parameter 'dateOfBirth' of nonnullable type 'System.DateTime'."If
dateOfBirth
were populated from the request URL (locations 2 or 3), then it would be marked for culture-insensitive parsing, so you should use the universal date formatyyyy-mm-dd
. If it were populated from the form POST data (location 1), then it would be marked for culture-sensitive parsing, leading to different interpretations depending on server settings. A thread in US culture mode would accept the date formatmm-dd-yyyy
, whereas a thread in UK culture mode would assumedd-mm-yyyy
(both would still work fine withyyyy-mm-dd
).[73] The reason for this difference of behavior is that it makes sense to interpret user-supplied data culture-sensitively, and form fields are often used to accept such user-supplied data. However, by definition, query string and routing parameters in a universal resource locator (URL) should not contain culture-specific formatting.
The framework's DefaultModelBinder
takes these supplies of raw data, most of which are simply string
values from the HTTP request, and converts them into whatever .NET objects are required as action method parameters. It uses .NET's type converter facility to deal with converting to simple types such as int
and DateTime
. But for collections and custom types, something more sophisticated is required.
You can simplify some action methods tremendously by receiving custom types as parameters, rather than instantiating and populating them manually.
First, let's define a new simple model class as follows:
public class Person { public string Name { get; set; } public string Email { get; set; } public DateTime DateOfBirth { get; set; } }
Next, consider the following view, which renders a basic user registration form:
<% using(Html.BeginForm("RegisterMember", "Home")) { %> <div>Name: <%: Html.TextBox("myperson.Name") %></div> <div>Email address: <%: Html.TextBox("myperson.Email") %></div>
<div>Date of birth: <%: Html.TextBox("myperson.DateOfBirth") %></div> <input type="submit" /> <% } %>
This form might post to the following action method, which uses no model binding at all:
public ActionResult RegisterMember() { var myperson = new Person(); myperson.Name = Request["myperson.Name"]; myperson.Email = Request["myperson.Email"]; myperson.DateOfBirth = DateTime.Parse(Request["myperson.DateOfBirth"]); // ... now do something with myperson }
There's a lot of tedious plumbing in there, but you can eliminate it as follows:
public ActionResult RegisterMember(Person myperson
)
{
// ... now do something with myperson
}
When DefaultModelBinder
is asked to supply an object of some custom .NET type rather than just a simple type like string
or int
, it uses reflection to determine what public properties are exposed by that custom type. Then it calls itself recursively to obtain a value for that property. This recursion makes it possible to populate an entire custom object graph in one shot.
Notice the naming convention used to match request items with object properties: by default, it looks for values called nameOfParameter
.nameOfProperty
(e.g., myperson.Email
). That ensures it can assign incoming data to the correct parameter object. As recursion continues, the binder would look for nameOfParameter
.nameOfProperty
.nameOfSubProperty
, and so on. This is the same naming convention that templated view helpers use when giving names to the HTML fields they render, so in effect you get two-way binding between model objects and HTML forms.
When DefaultModelBinder
needs to instantiate custom object types (e.g., Person
in the previous example), it uses .NET's Activator.CreateInstance()
method, which relies on those types having public parameterless constructors. If your types don't have parameterless constructors, or if you want to instantiate them using a DI container, then you can derive a subclass of DefaultModelBinder
, override its virtual method CreateModel()
, and then assign an instance of your custom binder to ModelBinders.Binders.DefaultBinder
. Alternatively, you can implement a custom binder just for that specific type. An example of a custom binder follows shortly.
Now let's consider some ways in which this binding algorithm can be customized.
In the previous example, the default binder expected to populate the myperson
parameter by asking the value provider for myperson.Name, myperson.Email
, and myperson.DateOfBirth
(which in turn requests data from the value providers listed in Table 12-7). As you can guess, the prefix myperson
is determined by the name of the action method parameter.
If you wish, you can specify an alternative prefix using the [Bind]
attribute —for example:
public ActionResult RegisterMember([Bind(Prefix = "newuser")]
Person myperson)
{
// ...
}
Now the value provider will be asked for newuser.Name, newuser.Email
, and newuser.DateOfBirth
. This facility is mainly useful if you don't want your HTML element names to be constrained by what's appropriate for C# method parameter names.
If you prefer, you can avoid using prefixes altogether. In other words, simplify your view markup by removing the myperson
. prefix from each text box name, or if you're using a strongly typed view, use the strongly typed Html.TextBoxFor()
helper instead:
<% using(Html.BeginForm("RegisterMember", "Home")) { %> <div>Name: <%: Html.TextBoxFor(x => x.Name) %></div> <div>Email address: <%: Html.TextBoxFor(x => x.Email) %></div> <div>Date of birth: <%: Html.TextBoxFor(x => x.DateOfBirth) %></div> <input type="submit" /> <% } %>
The e-mail input text box will now be named Email
rather than myperson.Email
(and likewise for the other input controls). The incoming values will successfully bind against an action method defined as follows:
public ActionResult RegisterMember(Person myperson) { // ... }
This works because DefaultModelBinder
first looks for values with prefixes inferred from the method parameter name (or from any [Bind]
attribute, if present). In this example, that means it will look for incoming key/value pairs whose key is prefixed by myperson
. If no such incoming values can be found—and in this example they won't be—then it will try looking for incoming values again, but this time without using any prefix at all.
Imagine that the Person
class, as used in the last few examples, had a bool
property called IsAdmin
. You might want to protect this property from unwanted interference. However, if your action method uses model binding to receive a parameter of type Person
, then a malicious user could simply append ?IsAdmin=true
to the URL used when submitting the member registration form, and the framework would happily apply this property value to the new Person
object created.
Clearly, that would be a bad situation. And besides security, there are plenty of other reasons why you might want to control exactly which subset of properties are subject to model binding. There are two main ways to do this.
First, you can specify a list of properties to include in binding by using a [Bind]
attribute on your action method parameter—for example:
public ActionResult RegisterMember([Bind(Include = "Name, Email")]
Person myperson)
{
// ...
}
Or you can specify a list of properties to exclude from binding:
public ActionResult RegisterMember([Bind(Exclude = "DateOfBirth")]
Person myperson)
{
// ...
}
Second, you can apply a [Bind]
attribute to the target type itself. This rule will then apply globally, across all your action methods, whenever that type is model bound—for example:
[Bind(Include = "Email, DateOfBirth")]
public class Person
{
public string Name { get; set; }
public string Email { get; set; }
public DateTime DateOfBirth { get; set; }
}
Which of these strategies you use will depend on whether you're establishing a global rule or a rule that applies just to one particular model binding occasion.
In either case, using an Include
rule sets up a whitelist: only the specified properties will be bound. Using an Exclude
rule sets up a blacklist: all properties will be bound, except those specifically excluded. It rarely makes sense to specify both Include
and Exclude
, but if you do, properties will be bound only if they are present in the include list and are not present in the exclude list.
If you use [Bind]
on both the action method parameter and the target type itself, properties will be bound only if they're allowed by both filters. So, if you exclude IsAdmin
on the target type, that can't be overridden by any action method. Phew!
You've seen how model binding happens automatically when your action method accepts parameters. It's also possible to run model binding manually. This gives you more explicit control over how model objects are instantiated, where incoming data is retrieved from, and how parsing errors are handled.
For example, you could rewrite the previous example's RegisterMember()
action, invoking model binding manually by calling the controller base class's UpdateModel()
method as follows:
public ActionResult RegisterMember() { var person = new Person();UpdateModel(person);
// Or if you're using a prefix: UpdateModel(person, "myperson");
// ... now do something with person }
This approach is beneficial if you need to control exactly how your model objects are instantiated. Here, you're supplying a Person
instance to be updated (which you might have just loaded from a database) instead of letting the framework always create a new Person
.
UpdateModel()
accepts various parameters to let you choose the incoming data key prefix, which parameters should be included in or excluded from binding, and which value provider supplies incoming data. For example, instead of accepting data from all your registered value providers (which by default are those listed in Table 12-7), you could use the special FormCollection
value provider, which gets its data only from Request.Form
. Here's how:
public ActionResult RegisterMember(FormCollection form
{ var person = new Person(); UpdateModel(person,form
); // ... now do something with person }
This permits an elegant way of unit testing your model binding. Unit tests can run the action method, supplying a FormCollection
containing test data, with no need to supply a mock or fake request context. It's a pleasingly "functional" style of code, meaning that the method acts only on its parameters and doesn't touch external context objects.
Sometimes users will supply values that can't be assigned to the corresponding model properties, such as invalid dates, or text for int
properties. To understand how the MVC Framework deals with such errors, consider the following design goals:
User-supplied data should never be discarded outright, even if it is invalid. The attempted value should be retained so that it can reappear as part of a validation error.
When there are multiple errors, the system should give feedback about as many errors as it can. This means that model binding cannot bail out when it hits the first problem.
Binding errors should not be ignored. The programmer should be guided to recognize when they've happened and provide recovery code.
To comply with the first principle, the framework needs a temporary storage area for invalid attempted values. Otherwise, since invalid dates can't be assigned to a .NET DateTime
property, invalid attempted values would be lost. This is why the framework has a temporary storage area known as ModelState. ModelState
also helps to comply with the second principle: each time the model binder tries to apply a value to a property, it records the name of the property, the incoming attempted value (always as a string
), and any errors caused by the assignment. Finally, to comply with the third principle, if ModelState
has recorded any errors, then UpdateModel()
finishes by throwing an InvalidOperationException
saying, "The model of type typename could not be updated."
So, if binding errors are a possibility, you should catch and deal with the exception—for example:
public ActionResult RegisterMember() { var person = new Person(); try { UpdateModel(person);
// ... now do something with person } catch (InvalidOperationException ex) { // To do: Provide some UI feedback based on ModelState } }
This is a fairly sensible use of exceptions. In .NET, exceptions are the standard way to signal the inability to complete an operation (and are not reserved for critical, infrequent, or "exceptional" events, whatever that might mean[74]). However, if you prefer not to deal with an exception, you can use TryUpdateModel()
instead. It doesn't throw an exception, but returns a bool
status code—for example:
public ActionResult RegisterMember() { var person = new Person(); if (TryUpdateModel(person)) { // ... now do something with person } else { // To do: Provide some UI feedback based on ModelState } }
You'll learn how to provide suitable UI feedback in the "Validation" section later in this chapter.
When a certain model property can't be bound because the incoming data is invalid, that doesn't stop DefaultModelBinder
from trying to bind the other properties. It will still try to bind the rest, which means that you'll get back a partially updated model object.
When you use model binding implicitly—that is, receiving model objects as method parameters rather than using UpdateModel()
or TryUpdateModel()
—then it will go through the same process, but it won't signal problems by throwing an InvalidOperationException
. You can check ModelState.IsValid
to determine whether there were any binding problems, as I'll explain in more detail shortly.
One of the best things about model binding is how elegantly it lets you receive multiple data items at once. For example, consider a view that renders multiple text box helpers with the same name:
Enter three of your favorite movies: <br /> <%: Html.TextBox("movies") %><br /> <%: Html.TextBox("movies") %><br /> <%: Html.TextBox("movies") %>
Now, if this markup is in a form that posts to the following action method:
public ActionResult DoSomething(List<string> movies) { // ... }
then the movies
parameter will contain one entry for each corresponding form field. Instead of List<string>
, you can also choose to receive the data as a string[]
or even an IList<string>
—the model binder is smart enough to work it out. If all of the text boxes were called myperson.Movies
, then the data would automatically be used to populate a Movies
collection property on an action method parameter called myperson
.
So far, so good. But what about when you want to bind an array or collection of some custom type that has multiple properties? For this, you'll need some way of putting clusters of related input controls into groups—one group for each collection entry. DefaultModelBinder
expects you to follow a certain naming convention that is best understood through an example.
Consider the following view markup:
<% using(Html.BeginForm()) { %> <h2>First person</h2> <div>Name: <%: Html.TextBox("people[0].Name
") %></div> <div>Email address: <%: Html.TextBox("people[0].Email
")%></div> <div>Date of birth: <%: Html.TextBox("people[0].DateOfBirth
")%></div> <h2>Second person</h2> <div>Name: <%: Html.TextBox("people[1].Name
")%></div> <div>Email address: <%: Html.TextBox("people[1].Email
")%></div> <div>Date of birth: <%: Html.TextBox("people[1].DateOfBirth
")%></div> ... <input type="submit" /> <% } %>
Check out the input control names. The first group of input controls all have a [0]
index in their name; the second all have [1]
. To receive this data, simply bind to a collection or array of Person
objects, using the parameter name people
—for example:
[HttpPost] public ActionResult RegisterPersons(IList<Person> people) { // ... }
Because you're binding to a collection type, DefaultModelBinder
will go looking for groups of incoming values prefixed by people[0], people[1], people[2]
, and so on, stopping when it reaches some index that doesn't correspond to any incoming value. In this example, people
will be populated with two Person
instances bound to the incoming data.
An easier way of generating input controls with correctly indexed names is to use a for
loop and lambda-based HTML helpers. For example, if your Model
object has a property called People
of type IList<People>
, you can render a series of input control groups as follows:
<% for (var i = 0; i < Model.People.Count; i++) { %> <h2>Some person</h2> <div>Name: <%: Html.TextBoxFor(x => x.People[i].Name)%></div> <div>Email address: <%: Html.TextBoxFor(x => x.People[i].Email)%></div> <div>Date of birth: <%: Html.TextBoxFor(x => x.People[i].DateOfBirth)%></div> <% } %>
If you want an even easier way to do it, you can use templated view helpers. The built-in Collection
template automatically iterates over collections and renders a suitably indexed display or editor for each item. If you define a partial at /Views/
controllerName
/EditorTemplates/Person.ascx
containing the following:
<%@ Control Language="C#"
Inherits="System.Web.Mvc.ViewUserControl<Namespace
.Person>" %>
<h2>Some person</h2>
<div>Name: <%: Html.TextBoxFor(x => x.Name)%></div>
<div>Email address: <%: Html.TextBoxFor(x => x.Email)%></div>
<div>Date of birth: <%: Html.TextBoxFor(x => x.DateOfBirth)%></div>
then you can render a correctly indexed series of editors with a single line:
<%: Html.EditorFor(x => x.People) %>
You've just seen how to use sequential, zero-based indexes (i.e., 0, 1, 2, etc.) to define collection items. A more flexible option is to use arbitrary string keys to define collection items. This can be beneficial if you might dynamically add or remove groups of controls using JavaScript on the client, and don't want to worry about keeping the indexes sequential.[75]
To use this option, each collection item needs to declare a special extra value called index
that specifies the arbitrary string key you've chosen. For example, you could rewrite the previous example's view markup as follows:
<% using(Html.BeginForm()) { %> <h2>First person</h2> <input type="hidden" name="people.index
" value="someKey
" /> <div>Name: <%: Html.TextBox("people[someKey
].Name")%></div> <div>Email address: <%: Html.TextBox("people[someKey
].Email")%></div> <div>Date of birth: <%: Html.TextBox("people[someKey
].DateOfBirth")%></div> <h2>Second person</h2> <input type="hidden" name="people.index
" value="anotherKey
" />
<div>Name: <%: Html.TextBox("people[anotherKey
].Name")%></div> <div>Email address: <%: Html.TextBox("people[anotherKey
].Email")%></div> <div>Date of birth: <%: Html.TextBox("people[anotherKey
].DateOfBirth")%></div> ... <input type="submit" /> <% } %>
There are multiple hidden fields called people.index
, so ASP.NET will receive all their values combined into a single array. DefaultModelBinder
will then use this as a hint for what indexes it should expect when binding to a collection called people
.
If for some reason you'd like your action method to receive a dictionary rather than an array or a list, then you have to follow a modified naming convention that's more explicit about keys and values—for example:
<% using(Html.BeginForm()) { %> <h2>First person</h2> <input type="hidden" name="people[0].key
" value="firstKey
" /> <div>Name: <%: Html.TextBox("people[0].value.Name
")%></div> <div>Email address: <%: Html.TextBox("people[0].value.Email
")%></div> <div>Date of birth: <%: Html.TextBox("people[0].value.DateOfBirth
")%></div> <h2>Second person</h2> <input type="hidden" name="people[1].key
" value="secondKey
" /> <div>Name: <%: Html.TextBox("people[1].value.Name
")%></div> <div>Email address: <%: Html.TextBox("people[1].value.Email
")%></div> <div>Date of birth: <%: Html.TextBox("people[1].value.DateOfBirth
")%></div> ... <input type="submit" /> <% } %>
When bound to a Dictionary<string, Person>
or IDictionary<string, Person>
, this form data will yield two entries, under the keys firstKey
and secondKey
, respectively. You could receive the data as follows:
public ActionResult RegisterPersons(IDictionary<string, Person> people) { // ... }
If you want to supply extra data items to the model binding system, you can do so by creating your own value provider. This technique is relevant if your application must obtain request-specific values from HTTP headers, cookies, or elsewhere, and you'd like those values to be easily accessible as action method parameters, just like query string or form values.
To create a custom value provider, it isn't enough just to implement IValueProvider
. You must also create a factory class (inherited from ValueProviderFactory
) so the framework can create a separate instance of your value provider for each HTTP request. Here's an example of a value provider and its associated factory rolled into one.
public class CurrentTimeValueProviderFactory : ValueProviderFactory { public override IValueProvider GetValueProvider(ControllerContext ctx) { return new CurrentTimeValueProvider(); } private class CurrentTimeValueProvider : IValueProvider { public bool ContainsPrefix(string prefix) { // Claim only to contain a single value called "currentTime" return "currentTime".Equals(prefix, StringComparison.OrdinalIgnoreCase); } public ValueProviderResult GetValue(string key) { return ContainsPrefix(key) ? new ValueProviderResult(DateTime.Now, null, CultureInfo.CurrentCulture) : null; } } }
Whenever ASP.NET MVC asks this value provider for an item called currentTime
, the value provider will return DateTime.Now
. This allows your action methods to receive the current time simply by declaring a parameter called currentTime
—for example:
public ActionResult Clock(DateTime currentTime) { return Content("The time is " + currentTime.ToLongTimeString()); }
This is beneficial for unit testing: a unit test could call Clock()
, supplying any DateTime
value to act as the current time, which might be important if you need to test for some behavior that only occurs on weekends. Even if you aren't unit testing your action methods, it still helps to simplify your controllers if you perform storage and retrieval of custom context objects (e.g., SportsStore's Cart
objects that are held in Session
) using a value provider, because it means that actions can receive these context objects without needing to know or care where they come from.
To make this work at runtime, however, you need to tell ASP.NET MVC to use your new value provider. Add a line similar to the following to Application_Start()
in Global.asax.cs
:
ValueProviderFactories.Factories.Add(new CurrentTimeValueProviderFactory());
Or, if you want your custom value provider to be at the top of the priority list (so that the framework will use its values in preference to those from Request.Form, Request.QueryString
, and so on), register it as follows:
ValueProviderFactories.Factories.Insert(0, new CurrentTimeValueProviderFactory());
You've learned about the rules and conventions that DefaultModelBinder
uses to populate arbitrary .NET types according to incoming data. Sometimes, though, you might want to bypass all that and set up a totally different way of using incoming data to populate a particular object type. To do this, implement the IModelBinder
interface.
For example, if you want to receive an XDocument
object populated using XML data from a hidden form field, you need a very different binding strategy. It wouldn't make sense to let DefaultModelBinder
create a blank XDocument
, and then try to bind each of its properties, such as FirstNode, LastNode, Parent
, and so on. Instead, you'd want to call XDocument
's Parse()
method to interpret an incoming XML string. You could implement that behavior using the following class, which can be put anywhere in your ASP.NET MVC project:
public class XDocumentBinder : IModelBinder { public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {// Get the raw attempted value from the value provider
string key = bindingContext.ModelName; ValueProviderResult val = bindingContext.ValueProvider.GetValue(key); if ((val != null) && !string.IsNullOrEmpty(val.AttemptedValue)) {// Follow convention by stashing attempted value in ModelState
bindingContext.ModelState.SetModelValue(key, val);// Try to parse incoming data
string incomingString = ((string[])val.RawValue)[0]; XDocument parsedXml; try { parsedXml = XDocument.Parse(incomingString); } catch (XmlException) { bindingContext.ModelState.AddModelError(key, "Not valid XML"); return null; }// Update any existing model, or just return the parsed XML
var existingModel = (XDocument)bindingContext.Model; if (existingModel != null) { if (existingModel.Root != null) existingModel.Root.ReplaceWith(parsedXml.Root); else existingModel.Add(parsedXml.Root); return existingModel; } else return parsedXml; }// No value was found in the request
return null; } }
This isn't as complex as it initially appears. All that a custom binder needs to do is accept a ModelBindingContext
, which provides both the ModelName
(the name of the parameter or prefix being bound) and a ValueProvider
from which you can receive incoming data. The binder should ask the value provider for the raw incoming data, and can then attempt to parse the data. If the binding context provides an existing model object, then you should update that instance; otherwise, return a new instance.
You might be wondering how custom value providers differ from custom model binders. A value provider can only provide incoming objects with particular string keys; it doesn't know what .NET type or object graph the model binder is trying to construct. A model binder is more complicated to implement, but it hooks into the process at a lower level and gives you more control. The preceding XDocument
example needs this extra control—a mere value provider wouldn't know whether you wanted to receive a certain incoming value as a parsed XDocument
instance or just as a string
.
The MVC Framework won't use your new custom model binder unless you tell it to do so. If you own the source code to XDocument
, you could associate your binder with the XDocument
type by applying an attribute as follows:
[ModelBinder(typeof(XDocumentBinder))] public class XDocument { // ... }
This attribute tells the MVC Framework that whenever it needs to bind an XDocument
, it should use your custom binder class, XDocumentBinder
. However, you probably can't change the source code to XDocument
, so you need to use one of the following two alternative configuration mechanisms instead.
The first option is to register your binder with ModelBinders.Binders
. You only need to do this once, during application initialization. For example, in Global.asax.cs
, add the following:
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.Add(typeof(XDocument), new XDocumentBinder());
}
The second option is to specify which model binder to use on a case-by-case basis. When binding action method parameters, you can use [ModelBinder]
, as follows:
public ActionResult MyAction([ModelBinder(typeof(XDocumentBinder))]
XDocument xml)
{
// ...
}
Unfortunately, if you're invoking model binding explicitly, it's somewhat messier to specify a particular model binder, because for some reason UpdateModel()
has no overload to let you do so. Here's a utility method that you might want to add to your controller:
private void UpdateModelWithCustomBinder<TModel>(TModel model, string prefix, IModelBinder binder, string include, string exclude)
{ var modelType = typeof(TModel); var bindAttribute = new BindAttribute { Include = include, Exclude = exclude }; var metadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, modelType); var bindingContext = new ModelBindingContext { ModelMetadata = metadata, ModelName = prefix, ModelState = ModelState, ValueProvider = ValueProvider, PropertyFilter = bindAttribute.IsPropertyAllowed }; binder.BindModel(ControllerContext, bindingContext); if (!ModelState.IsValid) throw new InvalidOperationException("Error binding " + modelType.FullName); }
With this, you can now easily invoke your custom binder, as follows:
public ActionResult MyAction() { var doc = new XDocument(); UpdateModelWithCustomBinder(doc, "xml", new XDocumentBinder(), null, null); // ... }
So, there are several ways of nominating a model binder. How does the framework resolve conflicting settings? It selects model binders according to the following priority order:
The binder explicitly specified for this binding occasion (e.g., if you're using a [ModelBinder]
attribute on an action method parameter).
The binder registered in ModelBinders.Binders
for the target type.
The binder assigned using a [ModelBinder]
attribute on the target type itself.
The default model binder. Usually, this is DefaultModelBinder
, but you can change that by assigning an IModelBinder
instance to ModelBinders.Binders.DefaultBinder
. Configure this during application initialization—for example, in Global.asax.cs
's Application_Start()
method.
Specifying a model binder on a case-by-case basis (i.e., option 1) makes most sense when you're more concerned about the incoming data format than about what .NET type it needs to map to. For example, you might sometimes receive data in JSON format, in which case it makes sense to create a JSON binder that can construct .NET objects of arbitrary type. You wouldn't register that binder globally for any particular model type, but would just nominate it for certain binding occasions.
Back in Table 12-7, you saw that one of ASP.NET MVC's built-in value providers is HttpFileCollectionValueProvider
. This gives you an easy way to receive uploaded files. All you have to do is accept a method parameter of type HttpPostedFileBase
, and ASP.NET MVC will populate it (where possible) with data corresponding to an uploaded file.[76]
For example, to let the user upload a file, add to one of your views a <form>
like this:
<form action="<%: Url.Action("UploadPhoto") %>" method="post" enctype="multipart/form-data"> Upload a photo: <input type="file" name="photo" /> <input type="submit" /> </form>
You can then retrieve and work with the uploaded file in the action method:
public ActionResult UploadPhoto(HttpPostedFileBase photo) { // Save the file to disk on the server string filename = // ... pick a filename photo.SaveAs(filename); // ... or work with the data directly byte[] uploadedBytes = new byte[photo.ContentLength]; photo.InputStream.Read(uploadedBytes, 0, photo.ContentLength); // Now do something with uploadedBytes }
The previous example showed a <form>
tag with an attribute you may find unfamiliar: enctype="multipart/form-data"
. This is necessary for a successful upload! Unless the form has this attribute, the browser won't actually upload the file—it will just send the name of the file instead, and the Request.Files
collection will be empty. (This is how browsers work; ASP.NET MVC can't do anything about it.) Similarly, the form must be submitted as a POST request (i.e., method="post"
); otherwise, it will contain no files.
In this example, I chose to render the <form>
tag by writing it out as literal HTML. Alternatively, you can generate a <form>
tag with an enctype
attribute by using Html.BeginForm()
, but only by using the four-parameter overload that takes a parameter called htmlAttributes
. Personally, I think literal HTML is more readable than sending so many parameters to Html.BeginForm()
.
What is validation? There's a whole range of ways you can think about it, including
Making demands about the presence or format of data that users may enter into a UI
Determining whether a certain .NET object is in a state that you consider valid
Applying business rules to allow or prevent certain operations being carried out against your domain model
ASP.NET MVC's built-in validation support focuses mainly on the first and second requirements. As part of the model binding process, the framework will test whether the bound object complies with your rules. If it doesn't, you can use built-in HTML helpers to display validation error messages. You can even try to avoid the validation errors in the first place by using your rules to generate a client-side validation script that will restrict what data users may enter.
At the end of this chapter, we'll consider the third requirement: integrating this system with business rules that live in your domain model layer. This goes beyond the simple notion of validating an object's state and deals with validating an operation in a certain context. For example, users may be allowed to edit a document's title, but not if the document has already been published, unless the user is an administrator. Your domain model can robustly protect itself, and you can use the same validation HTML helpers to provide UI feedback.
Before you start thinking about defining validation rules declaratively using attributes or custom validation providers, it's important to understand how you can implement validation logic directly inside an action method and then display error messages to users.
As you learned earlier in this chapter, the MVC Framework uses ModelState
as a place to store information about what's happening with a model object during the current request. The model binding system uses ModelState
to store both incoming attempted values and details of any binding errors. You can also manually register errors in ModelState
. Altogether, this is how to communicate error information to views, and is also how input controls can recover their previous state after a validation or model binding failure.
Here's an example. You're creating a controller called BookingController
, which lets users book appointments. Appointments are modeled as follows:
public class Appointment { public string ClientName { get; set; } [DataType(DataType.Date)] public DateTime AppointmentDate { get; set; } }
To place a booking, users first visit BookingController
's MakeBooking
action:
public class BookingController : Controller { public ViewResult MakeBooking() { var initialState = new Appointment { AppointmentDate = DateTime.Now.Date
}; return View(initialState); } }
This action does nothing more than render its default view, MakeBooking.aspx
(strongly typed with model type Appointment
), which includes the following form:
<h1>Book an appointment</h1> <% using(Html.BeginForm()) { %> <p> Your name: <%: Html.EditorFor(x => x.ClientName) %> </p> <p> Appointment date: <%:Html.EditorFor(x => x.AppointmentDate)%> </p> <p> <%: Html.CheckBox("acceptsTerms") %> <label for="acceptsTerms">I accept the Terms of Booking</label> </p> <input type="submit" value="Place booking" /> <% } %>
This action will now render as shown in Figure 12-8.
Since the view template generates a form tag by calling Html.BeginForm()
without specifying an action name parameter, the form posts to the same URL that generated it. In other words, to handle the form post, you need to add another action method called MakeBooking()
, except this one should handle POST requests. Here's how it can detect and register validation errors:
[HttpPost] public ActionResult MakeBooking(Appointment appt, bool acceptsTerms)
{ if (string.IsNullOrEmpty(appt.ClientName)) ModelState.AddModelError("ClientName", "Please enter your name"); if (ModelState.IsValidField("AppointmentDate")) { // Parsed the DateTime value. But is it acceptable under our app's rules? if (appt.AppointmentDate < DateTime.Now.Date) ModelState.AddModelError("AppointmentDate", "The date has passed"); else if ((appt.AppointmentDate - DateTime.Now).TotalDays > 7) ModelState.AddModelError("AppointmentDate", "You can't book more than a week in advance"); } if (!acceptsTerms) ModelState.AddModelError("acceptsTerms", "You must accept the terms"); if (ModelState.IsValid) { // To do: Actually save the appointment to the database or whatever return View("Completed", appt); } else return View(); // Re-renders the same view so the user can fix the errors }
The preceding code won't win any awards for elegance or clarity. I'll soon describe a tidier way of doing this, but for now I'm just trying to demonstrate the most basic way of registering validation errors.
I've included DateTime
in this example so that you can see that it's a tricky character to deal with. It's a value type, so the model binder will register the absence of incoming data as an error, just as it registers an unparsable date string as an error. You can test whether the incoming value was successfully parsed by calling ModelState.IsValidField(...)
—if it wasn't, there's no point applying any other validation logic to that field.
This action method receives incoming form data as parameters via model binding. It then enforces certain validation rules in the most obvious and flexible way possible—plain C# code—and for each rule violation, it records an error in ModelState
, giving the name of the input control to which the error relates. Finally, it uses ModelState.IsValid
(which checks whether any errors were registered, either by you or by the model binder) to decide whether to accept the booking or redisplay the same data entry screen.
It's a very simple validation pattern, and it works just fine. However, if the user enters invalid data right now, they won't see any error messages, because the view doesn't contain instructions to display them.
The easiest way to tell your view to render error messages is as follows. Just place a call to Html.ValidationSummary()
somewhere inside the view—for example:
<h1>Book an appointment</h1>
<%: Html.ValidationSummary() %>
... all else unchanged ...
This helper simply produces a bulleted list of errors recorded in ModelState
. If you submit a blank form, you'll now get the output shown in Figure 12-9.
You can also pass to Html.ValidationSummary()
a parameter called message
. This string will be rendered immediately above the bulleted list if there is at least one registered error. For example, you could display the heading "Please amend your submission, and then resubmit it."
There are two things to notice about this screen:
Where did the "The AppointmentDate field is required" message come from? That's not in my controller! Yes, when the framework prepares a ModelMetadata
instance to describe a property that can't hold null
(such as DateTime
, a value type), it automatically sets the metadata's IsRequired
flag to true
. Then, the built-in default validation provider enforces this rule. If you don't like this, change the property type to DateTime?
, or explicitly add your own required field validator for that property giving an alternative error message, as discussed shortly.
Some of the input controls are highlighted with a shaded background to indicate their invalidity. The framework's built-in HTML helpers for input controls are smart enough to notice when they correspond to a ModelState
entry that has errors, and will give themselves relevant CSS classes including input-validation-error
and validation-summary-errors
. Whenever you use Visual Studio to create a new ASP.NET MVC project, it gives you a basic stylesheet at /Content/Site.css
that declares all of these validation CSS classes.
Alternatively, you can choose not to use Html.ValidationSummary()
, and instead to use a series of Html.ValidationMessage()
or Html.ValidationMessageFor()
helpers to place specific potential error messages at different positions in your view. For example, update MakeBooking.aspx
as follows:
<% using(Html.BeginForm()) { %> <p> Your name: <%: Html.EditorFor(x => x.ClientName) %><%: Html.ValidationMessageFor(x => x.ClientName) %>
</p> <p> Appointment date: <%: Html.EditorFor(x => x.AppointmentDate)%><%: Html.ValidationMessageFor(x => x.AppointmentDate) %>
</p> <p> <%: Html.CheckBox("acceptsTerms") %> <label for="acceptsTerms">I accept the Terms of Booking</label><%: Html.ValidationMessage("acceptsTerms") %>
</p> <input type="submit" value="Place booking" /> <% } %>
Now, a blank form submission would produce the display shown in Figure 12-10.
Some validation error messages may relate to specific properties, while others may relate to the entire model object and not any single specific property. You've already seen how to register property-level errors by passing the property name as a parameter (e.g., ModelState.AddModelError
("ClientName",
message
)). You can register model-level errors by passing an empty string for the key
parameter—for example:
bool isSaturday = appt.AppointmentDate.DayOfWeek == DayOfWeek.Saturday;
if (appt.ClientName == "Steve" && isSaturday)
ModelState.AddModelError("" /* key */,
"Steve can't book on Saturdays");
By default, Html.ValidationSummary()
shows both model- and property-level errors. But if you're rendering property-level errors in other places using Html.ValidationMessage()
or Html.ValidationMessageFor()
, you probably don't want property-level errors to be duplicated in the validation summary.
To fix this, you can instruct Html.ValidationSummary()
to display only model-level errors (i.e., those registered with an empty key) so that the user sees no duplication. Just pass true
for its excludePropertyErrors
parameter—that is, call Html.ValidationSummary(true)
. You can see example output in Figure 12-11.
To create the preceding screenshot (Figure 12-11), I entered the values shown and clicked "Place booking." When the form reappeared with validation error messages, the data I previously entered (in this case a name and a date) was still present in the form fields.
ASP.NET Web Forms achieves a kind of statefulness using its ViewState mechanism, but there's no such mechanism in ASP.NET MVC. So how was the state retained?
Once again, it's because of a convention. The convention is that input controls should populate themselves using data taken from the following locations, in this order of priority:
Previously attempted value recorded in ModelState["
name
"].Value.AttemptedValue
Explicitly provided value (e.g., Html.TextBox("
name
", "Some value")
or Html.TextBoxFor(x => x.SomeProperty)
)
ViewData
, by calling ViewData.Eval("
name
")
(so ViewData["
name
"]
takes precedence over ViewData.Model
.name
)
Since model binders record all attempted values in ModelState
, regardless of validity, the built-in HTML helpers naturally redisplay attempted values after a validation or model binding failure. And because this takes top priority, even overriding explicitly provided values, then any explicitly provided values are really just initial control values.
If you think about how the preceding appointment booking example works, you'll notice that there are two distinct phases of validation:
First, DefaultModelBinder
enforces some basic data formatting rules as it parses incoming values and tries to assign them to the model object. For example, if it can't parse the incoming appt.AppointmentDate
value as a DateTime
, then DefaultModelBinder
registers a validation error in ModelState
.
Second, after model binding is completed, our MakeBooking()
action method checks the bound values against custom business rules. If it detects any rule violations, it also registers those as errors in ModelState
.
You'll consider how to improve and simplify the second phase of validation shortly. But first, you'll learn how DefaultModelBinder
does validation and how you can customize that process if you want.
There are five virtual methods on DefaultModelBinder
relating to its efforts to validate incoming data. These are listed in Table 12-8.
Table 12-8. Overridable Validation Methods on DefaultModelBinder
Description | Default Behavior | |
---|---|---|
| This runs when | It does nothing—just returns |
| This runs after | It invokes all the |
| This runs before each time | It does nothing—just returns |
| This runs after each time | It does nothing. |
| This is the method that | If the property cannot hold |
If you want to implement a different kind of validation during data binding, you can create a subclass of DefaultModelBinder
and override the relevant methods listed in the preceding table. Then hook your custom binder into the MVC Framework by adding the following line to your Global.asax.cs
file:
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ModelBinders.Binders.DefaultBinder = new MyModelBinder
();
}
However, it's rarely necessary to subclass DefaultModelBinder
, especially not as a way of implementing custom validation rules, because the framework provides a whole system for defining validation rules using either Data Annotations attributes or a custom provider. DefaultModelBinder
will invoke your rules as part of its OnModelUpdated()
behavior.
Not surprisingly, ASP.NET MVC's approach to defining validation rules follows the usual provider pattern so you can extend or replace it if you wish. Figure 12-12 illustrates how arbitrary validation rule sources are mapped to standard ModelValidator
instances.
When any component (such as DefaultModelBinder
) wishes to know the validation rules associated with a particular ModelMetadata
instance, it calls ModelMetadata
's virtual GetValidators()
method, which in turn asks all your registered validation providers (such as DataAnnotationsModelValidationFactory
) to return a set of ModelValidator
instances related to that model item.
We'll start by looking at the framework's built-in validation providers and then move on to see how you could create one of your own.
By default, ASP.NET MVC applications are configured to use DataAnnotationsModelValidationFactory
, which recognizes the Data Annotations attributes listed in Table 12-9.
Table 12-9. Data Annotations Validation Attributes Recognized by ASP.NET MVC
Meaning | |
---|---|
| A numeric value (or any property type that implement |
| A string value must match the specified regular expression pattern. Note that your pattern has to match the entire user-supplied value, not just a substring within it. By default, it matches case sensitively, but you can make it case insensitive by applying the |
| The value must not be empty or be a string consisting only of spaces. If you want to treat whitespace as valid, use |
| A string value must not be longer than the specified maximum length. In .NET 4, you can also specify a minimum length. |
Even though [DataType]
looks like a validation attribute along with the others in Data Annotations, ASP.NET MVC does not treat it as one, so don't expect [DataType(DataType.EmailAddress)]
to validate for legal e-mail addresses! [DataType]
is an anomaly; even though it inherits from System.ComponentModel.DataAnnotations.ValidationAttribute
, its IsValid()
method is hard-coded to return true
regardless of the property's value. Microsoft has explained that [DataType]
is only meant to serve as a hint for formatting data in a scaffolded UI, though it still seems strange that it inherits from ValidationAttribute
.
When you use any of the Data Annotations validation attributes, you can supply a custom error message as a parameter in either of the following two ways:
[AttributeName(ErrorMessage = "Your custom error message")]
or
[AttributeName(ErrorMessageResourceName = "YourResourceEntryName", ErrorMessageResourceType = typeof(YourResources))]
The second option is intended to work with RESX localization files—you need to give the .NET type name corresponding to the RESX file, and at runtime the framework will extract the named resource string according to the active thread culture. You'll learn more about working with RESX files and localization in Chapter 17.
Continuing the previous example, you could apply these attributes to the Appointment
model class as follows:
public class Appointment {[Required(ErrorMessage = "Please enter your name")] [StringLength(50)]
public string ClientName { get; set; } [DataType(DataType.Date)][Required(ErrorMessage = "Please choose a date")]
public DateTime AppointmentDate { get; set; } }
These rules will now be applied during model binding, and any violations will be registered in ModelState
automatically, so it's no longer necessary for your controller to check whether ClientName
was provided. Of course, you can still implement further validation logic directly inside your controller, which in this example is necessary to validate that the booking date falls within the next week, and that the Terms of Booking check box was checked.
It would make sense to add a bool
property called AcceptsTerms
to the Appointment
class and then apply a [Required]
validator to it. That way, you wouldn't need any custom logic for it in your controller. The reason I haven't done that in this example is to illustrate that you can write code to validate any incoming data, whether or not it maps to a model property.
It's very easy to create your own validation attribute that DataAnnotationsModelValidationFactory
can recognize. Just inherit from the Data Annotations ValidationAttribute
base class. Here's a simple example:
public class ValidEmailAddressAttribute : ValidationAttribute { public ValidEmailAddressAttribute() { // Default message unless declared on the attribute ErrorMessage = "{0} must be a valid email address."; } public override bool IsValid(object value) { // You might want to enhance this logic... string stringValue = value as string; if (stringValue != null) return stringValue.Contains("@"); return true; } }
It's conventional for IsValid()
to return true
if the supplied value is empty. Otherwise, you're implicitly making the property required even if there is no [Required]
attribute associated with it.
As well as DataAnnotationsModelValidationFactory
, the framework also includes DataErrorInfoModelValidatorProvider
. This provides a more awkward and less powerful way of implementing custom validation logic, and is mainly intended for backward compatibility with ASP.NET MVC 1, where DefaultModelBinder
was hard-coded to recognize an interface called IDataErrorInfo
.
To use this, make your model class implement the IDataErrorInfo
interface. This requires you to implement two methods—one to return property-level errors, and another to return object-level errors—for example:
public class Appointment : IDataErrorInfo { public string ClientName { get; set; } public DateTime AppointmentDate { get; set; } public string this[string columnName] { get { if (columnName == "ClientName") { if (string.IsNullOrEmpty(ClientName)) return "Please enter a name."; } if (columnName == "AppointmentDate") { if (AppointmentDate < DateTime.Now.Date) return "Bookings cannot be placed in the past"; } return null; // No property-level errors } } public string Error { get { if (ClientName == "Steve" && AppointmentDate.DayOfWeek == DayOfWeek.Saturday) return "Steve can't book on Saturdays."; return null; // No object-level errors } } }
Now you can simplify the MakeBooking
action as follows:
[HttpPost] public ActionResult MakeBooking(Appointment appt, bool acceptsTerms) { if (!acceptsTerms) ModelState.AddModelError("acceptsTerms", "You must accept the terms"); if (ModelState.IsValid) { // To do: Actually save the appointment to the database or whatever
return View("Completed", appt); } else return View(); // Re-renders the same view so the user can fix the errors }
DataErrorInfoModelValidatorProvider
will call your IDataErrorInfo
methods and populate ModelState
as part of model binding. You can still add extra validation logic, as in this example with the Accepts Terms check box, directly inside your action method.
This whole technique is much less useful than using Data Annotations attributes or a custom validation provider for several reasons:
It provides no easy means of reusing validation logic between different model classes.
It provides no means of reporting multiple errors relating to a single property, or multiple errors relating to the whole model object, other than concatenating all the messages into a single string.
It provides no means of generating client-side validation scripts.
It's good that ASP.NET MVC 2 supports IDataErrorInfo
as a matter of backward compatibility, but most developers will not want to use it now that better alternatives exist.
If you want to go in a different direction from Data Annotations attributes, you can create a custom validation provider by inheriting a class from either of the following base classes:
ModelValidatorProvider
: the abstract base class for all validator providers
AssociatedValidatorProvider
: usually a better choice of base class if your validation rules are expressed mainly as .NET attributes, because it deals with the tricky business of detecting custom attributes, including transparently fetching them from "buddy" classes referenced by [MetadataType]
attributes
Either way, you must override a method called GetValidators()
and return a set of ModelValidator
instances. This lets you hook into the validation system at a lower level than a Data Annotations ValidationAttribute
, so you get more control over what happens.
Why would you want to do this? As an example, you might want to validate that two model properties must be equal. The ASP.NET MVC 2 Web Application project template includes its own custom validation attribute, PropertiesMustMatchAttribute
, which you can apply to a model class and specify the names of the two properties that must match. But what if you want to apply a validation attribute to a property (not to the whole model class) and say that its value must match the value of another property?
You can't easily do this by inheriting from ValidationAttribute
, because Data Annotations is mainly intended for validating objects in isolation, and its API doesn't provide any way for you to access sibling properties. In situations like this, you'll need to step away from Data Annotations and implement a totally separate custom validation provider.
To get started, define the following custom attribute:
public class EqualToPropertyAttribute : Attribute { public readonly string CompareProperty; public EqualToPropertyAttribute(string compareProperty)
{ CompareProperty = compareProperty; } }
Using this attribute, you could declare that two password fields have to match.
public class UserRegistrationViewModel
{
// ... other properties ...
[Required] [DataType(DataType.Password)]
public string Password { get; set; }
[Required] [DataType(DataType.Password)] [EqualToProperty("Password")]
public string ConfirmPassword { get; set; }
}
Next, you need to define a custom validator provider that can detect [EqualToProperty]
attributes and convert them into instances of a ModelValidator
subclass. Here's one such class that returns instances of a new class, EqualToPropertyValidator
, which we'll define in a moment:
public class MyValidatorProvider : AssociatedValidatorProvider { protected override IEnumerable<ModelValidator> GetValidators( ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes) { foreach (var attrib in attributes.OfType<EqualToPropertyAttribute>()) yield return new EqualToPropertyValidator(metadata, context, attrib.CompareProperty); } }
You need to tell ASP.NET MVC to use your new validator provider by registering it in the static ModelValidatorProviders.Providers
collection. For example, update Application_Start()
in Global.asax.cs
as follows:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelValidatorProviders.Providers.Add(new MyValidatorProvider());
}
Of course, before any of this will compile, you'll also need to implement the EqualToPropertyValidator
class previously referenced by the validator provider. This is where all the real validation logic lives.
public class EqualToPropertyValidator : ModelValidator { private readonly string compareProperty; public EqualToPropertyValidator(ModelMetadata metadata, ControllerContext context, string compareProperty) : base(metadata, context) {
this.compareProperty = compareProperty; } public override IEnumerable<ModelValidationResult> Validate(object container) { if (Metadata.Model == null) yield break; var propertyInfo = container.GetType().GetProperty(compareProperty); if (propertyInfo == null) throw new InvalidOperationException("Unknown property:"+compareProperty); var valueToCompare = propertyInfo.GetValue(container, null); if (!Metadata.Model.Equals(valueToCompare)) yield return new ModelValidationResult { Message = "This value must equal the value of " + compareProperty }; } }
As you can see, custom ModelValidator
instances get access to the property's complete set of ModelMetadata
information, as well as the container
object of which the property is part. Your validator provider can supply any collection of ModelMetadata
instances, so you're not limited just to using .NET attributes—you can obtain your configuration from any source.
Whenever you use model binding to populate a model object—either by receiving it as an action method parameter, or by calling UpdateModel()
or TryUpdateModel()
manually—then DefaultModelBinder
will automatically run the validators associated with all model objects that it has updated (i.e., ones where it has set a value on at least one property).
If you update a model object in any other way, its validators will not be run unless you explicitly tell the framework to run them. For example, you might sometimes manually update model properties within an action method as shown here:
[HttpPost] public ActionResult MakeBooking(string clientName, DateTime? appointmentDate,
bool acceptsTerms) {var appt = new Appointment {
ClientName = clientName,
AppointmentDate = appointmentDate.GetValueOrDefault()
};
if (!acceptsTerms) ModelState.AddModelError("acceptsTerms", "You must accept the terms"); if (ModelState.IsValid) { // To do: Actually save the appointment to the database or whatever return View("Completed", appt); } else return View(); // Re-renders the same view so the user can fix the errors }
Now, the custom validation logic for acceptsTerms
is still enforced, but any validators associated with the Appointment
class or its properties will not be run, so they will never register any errors in ModelState
.
The Controller
base class exposes two methods that will run the validators associated with an arbitrary object at any time:
ValidateModel()
runs the validators, registers any errors in ModelState
, and finishes by throwing an InvalidOperationException
if ModelState
contains at least one error.
TryValidateModel()
does the same, except instead of throwing an exception, it returns a bool
value to signal the result. The return value is actually just the value of ModelState.IsValid
.
So, you could update the previous code sample by calling TryValidateModel()
immediately after populating the Appointment
instance.
var appt = new Appointment {
ClientName = clientName,
AppointmentDate = appointmentDate.GetValueOrDefault()
};
TryValidateModel(appt);
Even though we're ignoring the return value from TryUpdateModel()
, it will register any errors in ModelState
, so the subsequent code that checks ModelState.IsValid
will work as expected.
It's difficult to validate value type properties when you call ValidateModel()
or TryValidateModel()
manually. For example, since you can't assign a null
value for a DateTime
property, it will certainly hold some date value, so it's meaningless to say that the property is required. To work around this, you'd need to change the AppointmentDate
property to be of the nullable type DateTime?
, and then remove GetValueOrDefault()
from the assignment. DefaultModelBinder
doesn't have this problem because it knows whether it has just applied a value from the request to each property.
In web applications, most people expect to see validation feedback immediately, before submitting anything to the server. This is known as client-side validation, usually implemented using JavaScript. Pure server-side validation is robust, but doesn't yield a great end-user experience unless accompanied by client-side validation.
ASP.NET MVC has a system for generating client-side validation scripts directly from the ModelValidator
instances associated with your model object's metadata. It's pretty easy to use—if you're using standard Data Annotation attributes, then in most cases it will only take one extra line of code to enable client-side validation.
Continuing the previous appointment booking example, in the MakeBooking.aspx
view, call Html.EnableClientValidation()
before rendering the form:
<% Html.EnableClientValidation(); %>
<% using(Html.BeginForm()) { %>
... rest as before ...
Note that Html.EnableClientValidation()
returns void
, so you must not try to emit any output from it by using <%: ... %>
or <%= ... %>
. If you do, you'll get a compilation error.
Also, make sure that somewhere in your view or its master page you've referenced the following two scripts, which Visual Studio automatically provides when you create any new ASP.NET MVC 2 project. A good place to reference them is at the bottom of a master page, right before the closing </body>
tag.
<script type="text/javascript" src="<%: Url.Content("~/Scripts/MicrosoftAjax.js") %>"></script> <script type="text/javascript" src="<%: Url.Content("~/Scripts/MicrosoftMvcValidation.js") %>"></script>
Now, if you reload the appointment booking form in your browser, you should find that the [Required]
and [StringLength]
rules will be enforced on the client using JavaScript. Validation messages will appear and disappear dynamically, and until the user supplies acceptable values to satisfy these rules, the form cannot be submitted.
I expect most readers will find this obvious, but it's so important I still have to point it out. Enabling client-side validation is not a substitute for enforcing validation on the server! You still need to check ModelState.IsValid
(or use some other mechanism for ensuring validity), because client-side validation can easily be bypassed. Users can disable JavaScript in their browsers, or they can use some other tool to send an arbitrary HTTP POST request to your server. See Chapter 15 for more details.
The MicrosoftMvcValidation.js
script is smart enough to notice if your form contains a validation summary rendered using Html.ValidationSummary()
, and if it does, the script will dynamically show and hide messages in the summary list as needed. Here's how you could update the MakeBooking.aspx
view to do client-side validation with a validation summary:
<% Html.EnableClientValidation(); %> <% using(Html.BeginForm()) { %><%: Html.ValidationSummary() %>
<% Html.ValidateFor(x => x.ClientName); %>
<% Html.ValidateFor(x => x.AppointmentDate); %>
<p>Your name: <%: Html.EditorFor(x => x.ClientName) %></p> <p>Appointment date: <%: Html.EditorFor(x => x.AppointmentDate)%></p> <p> <%: Html.CheckBox("acceptsTerms") %> <label for="acceptsTerms">I accept the Terms of Booking</label> </p> <input type="submit" value="Place booking" /> <% } %>
This is different from the previous version of the view in two main ways:
Html.ValidationSummary()
is now inside the form. It has to be—views can contain any number of forms, and any number of validation summaries. MicrosoftMvcValidation.js
resolves the possible ambiguity by associating each form with the validation summary that it contains.
Instead of using Html.ValidationMessageFor()
to display messages at specific locations, we're now using Html.ValidateFor()
. This HTML helper doesn't emit any HTML; it just tells ASP.NET MVC to register client-side validation metadata in FormContext
for the referenced model item. Without this, those fields wouldn't be validated on the client, and the validation summary would only be updated after the whole form was posted to the server.
Not surprisingly, the string-based equivalent of Html.ValidateFor()
is called Html.Validate()
. But even with this, we still can't validate the acceptsTerms
check box on the client, because it doesn't correspond to any model property, so there's no metadata associated with it.
As you learned earlier in this chapter, ASP.NET MVC's built-in HTML helpers use certain CSS classes, such as input-validation-error
, to highlight themselves when they correspond to validation errors in ModelState
. To fit in with this convention, MicrosoftMvcValidation.js
will dynamically add and remove these CSS classes on your input controls while the user is entering data into the form.
What's less well known is that MicrosoftMvcValidation.js
also dynamically applies a further CSS class, input-validation-valid
, to input elements once they've been detected as valid. This means you can highlight "good" values, reassuring the user that their data will be accepted. Validation doesn't always have to be negative!
For example, if you add the following rule to your CSS file:
.input-validation-valid { border: 1px solid green; background-color: #CCFFCC; }
then for any form that has client-side validation enabled, whenever the user types a valid value into a text box, that text box will turn green.
By default, client-side validation prevents the user from submitting a form while one or more validation errors are present. Normally that's exactly what you want. If for some reason you want a button to be able to submit a form regardless of whether it's displaying any validation errors, you can assign a value to a property called disableValidation
on that button's DOM node.
For example, if you have a submit button defined as follows:
<input id="submitBooking" type="submit" value="Place booking" />
then the following JavaScript allows it to bypass client-side validation:
<script type="text/javascript"> document.getElementById("submitBooking").disableValidation = true; </script>
If you look at the HTML source code for your view once rendered in a browser, you'll notice that just after the closing </form>
tag, the MVC Framework has emitted a JavaScript block that describes and applies your client-side validation rules:
... </form><script type="text/javascript"> //<![CDATA[window.mvcClientValidationMetadata=[];
}
window.mvcClientValidationMetadata.push({"Fields":[{ "FieldName":"ClientName", ...
//]]> </script>
To get an overview of how this information has reached the browser in JavaScript form, see Figure 12-13. We'll use this understanding in a moment when implementing client-side validation logic for a custom validation rule.
The ModelValidator
base class exposes a virtual method, GetClientValidationRules()
, that can return a set of ModelClientValidationRule
instances. Each ModelClientValidationRule
instance is a description of how that rule should be represented in the client-side JavaScript Object Notation (JSON) block. The framework's JavaScript libraries understand the JSON descriptions of standard Data Annotations validators such as [Required]
and [StringLength]
, plus they let you register your own JavaScript functions to implement custom validation logic for other rules.
ASP.NET MVC also automatically adds client-side validators for all properties of numeric types (i.e., byte, sbyte, short, ushort, int, uint, long, ulong, float, double
, and decimal
). This is to make client-side validation more consistent with server-side validation, which has no choice but to reject nonnumeric strings for numeric data types (because there's no way the model binder could put a nonnumeric value into a .NET int property
).
Currently, if you use the [EqualToProperty]
custom validation rule (which we created earlier in the chapter) in a form with client-side validation enabled, rules from Data Annotations may be validated on the client, but [EqualToProperty]
will not—it will only be validated on the server. It's hardly surprising—ASP.NET MVC can't automatically translate arbitrary server-side .NET code into JavaScript. Let's see how to use ModelValidator
's GetClientValidationRules()
, plus some JavaScript code, to run [EqualToProperty]
on the client too.
First, update EqualToPropertyValidator
by overriding its GetClientValidationRules()
method. You can return any ValidationType
and ValidationParameters
values—these will be made available on the client as part of the rule's JSON description.
public class EqualToPropertyValidator : ModelValidator { // ... rest as before public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() { var clientValidationRule = new ModelClientValidationRule { ValidationType = "EqualToProperty", ErrorMessage = "This value must equal the value of " + compareProperty }; clientValidationRule.ValidationParameters["compareTo"] = compareProperty; yield return clientValidationRule; } }
Next, add some JavaScript code directly after wherever you've referenced MicrosoftMvcValidation.js
, adding a new custom validator to the client-side validation registry. Note that your new client-side validator must be named to match whatever ValidationType
you declare in your ModelClientValidationRule
, which in this case is EqualToProperty
.
<script type="text/javascript"> Sys.Mvc.ValidatorRegistry.validators.EqualToProperty = function (rule) { // Prepare by extracting any parameters sent from the server var compareProperty = rule.ValidationParameters.compareTo; // Return a function that tests a value for validity
return function (value, context) { // Find the comparison element by working out what its name must be var thisElement = context.fieldContext.elements[0]; var compareName = thisElement.name.replace(/[^.]*$/, compareProperty); var compareElement = document.getElementsByName(compareName)[0]; // Check that their values match return value == compareElement.value; } }; </script>
This is an unusually complicated client-side validator, because it has to locate another element in the HTML DOM to compare against. If you only need to validate a single value in isolation, your client-side validator would be simpler:
<script type="text/javascript">
Sys.Mvc.ValidatorRegistry.validators.MyValidationType
= function (rule) {
return function (value, context) {
// Return true if 'value' is acceptable; otherwise false.
// Alternatively, return a non-null string to show a custom message
}
};
</script>
When the framework calls your custom validation function, it passes two parameters: value
, which of course is the value to be validated, and context
, which has the properties listed in Table 12-10.
Table 12-10. Useful Properties Available on the Context Object Passed to a Validation Function
Property | Description |
---|---|
| Takes one of three values: |
| An array of HTML DOM nodes that are associated with your validator. Typically this will contain just one element—the form field whose value you are validating. |
| The HTML DOM node that will be used to display any validation message for this validator. |
| An array containing all the |
If your custom validation logic is very simple, you don't necessarily need to create a whole new validation provider or write your own client-side validation code. All that work may be overkill, because it's often possible to build on an existing Data Annotations rule.
For example, earlier in this chapter we created a custom validator that validates e-mail addresses. Previously, we overrode the IsValid()
method to implement custom logic, but a different way to do it would be to inherit from RegularExpressionAttribute
.
public class ValidEmailAddressAttribute : RegularExpressionAttribute { private const string EmailPattern = ".+@.+\..+"; public ValidEmailAddressAttribute() : base(EmailPattern) { // Default message unless declared on the attribute ErrorMessage = "{0} must be a valid email address."; } }
This will work for server-side validation immediately, but what might surprise you is that it won't give you client-side validation. This is because, although DataAnnotationsModelValidatorProvider
can run anything inherited from ValidationAttribute
on the server, it only knows how to generate client-side representations of the four specific subclasses listed in Table 12-9.
DataAnnotationsModelValidatorProvider
has a system of adapters, which are delegates that can convert ValidationAttribute
subclasses into ModelValidator
instances. It has four built-in adapters:
There are several possible ways of making DataAnnotationsModelValidatorProvider
understand ValidEmailAddressAttribute
, the easiest of which is simply to associate EmailAddressAttribute
with RegularExpressionAttributeAdapter
. Add code similar to the following to Application_Start()
in Global.asax.cs
:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes);DataAnnotationsModelValidatorProvider.RegisterAdapter(
typeof(ValidEmailAddressAttribute),
typeof(RegularExpressionAttributeAdapter)
);
}
Now, you can apply the [ValidEmailAddress]
rule to get basic e-mail address validation both on the server and on the client.
You understand ASP.NET MVC's mechanism for registering rule violations, displaying them in views, and retaining attempted values. You have also seen how to define validation rules using Data Annotations attributes or a custom validation provider, how the default model binder applies these rules during binding, and how you can map them all onto a client-side validation script.
So far in this chapter's appointment booking example, our ASP.NET MVC application has been in charge of enforcing validation rules. That's OK in a small application, but it does tightly couple the implementation of business logic to the nuances of a particular UI technology (i.e., ASP.NET MVC). Such tight coupling is accepted practice in ASP.NET Web Forms because of how that platform guides you with its built-in validator controls. However, it's not an ideal separation of concerns, and over time it leads to the following practical problems:
Repetition: You have to duplicate your rules in each view model to which they apply. Like any violation of the don't-repeat-yourself (DRY) principle, it creates extra work and opens up the possibility of inconsistencies.
Obscurity: It's all too easy for someone to add a new action method that accidentally forgets to check ModelState.IsValid
and then accepts illegal data. Plus, if you have lots of different view models and no single central definition of your business rules, it's only a matter of time until you lose track of your intended design. You can't blame the new guy: nobody told him to enforce that obscure business rule in the new feature he just built.
Restricted technology choices: If your rules are defined by a mixture of Data Annotations attributes, custom ModelValidator
classes, and arbitrary logic inside action methods, you can't just choose to build a new Silverlight client or native iPhone edition of your application without having to reimplement your business rules yet again (if you can even figure out what they are). Your validation rules may run inside ASP.NET MVC, but an arbitrary future technology probably won't run them.
Unnatural chasm between validation rules and business rules: It might be convenient to drop a [Required]
attribute onto a view model, but what about rules such as "Usernames must be unique," or "Only 'Gold' customers may purchase this product when stock levels are low"? This is more than UI validation. But why should you implement such rules differently?
As described in Chapter 3, the solution to all these problems is to establish an independent domain model (most likely as a separate class library that doesn't know anything about ASP.NET MVC), and put it in control of validating the operations you perform against it.
For example, you may create a domain service class called AppointmentService
in your domain class library project.[77] It may expose a method, CreateAppointment()
, that saves new Appointment
instances.
public static class AppointmentService { public static void CreateAppointment(Appointment appt) { EnsureValidForCreation(appt); // To do: Now save the appointment to a database or wherever } private static void EnsureValidForCreation(Appointment appt) { var errors = new RulesException<Appointment>(); if (string.IsNullOrEmpty(appt.ClientName)) errors.ErrorFor(x => x.ClientName, "Please specify a name"); if (appt.AppointmentDate < DateTime.Now.Date) errors.ErrorFor(x => x.AppointmentDate, "Can't book in the past"); else if ((appt.AppointmentDate - DateTime.Now.Date).TotalDays > 7) errors.ErrorFor(x => x.AppointmentDate, "Can't book more than a week in advance"); if (appt.ClientName == "Steve" && appt.AppointmentDate.DayOfWeek == DayOfWeek.Saturday) errors.ErrorForModel("Steve can't book on weekends"); if (errors.Errors.Any()) throw errors; } }
If you don't want to hard-code error messages inside your domain code, you could amend AppointmentService
to fetch error messages from a RESX file at runtime. This would add support for localization as well as better configurability. You'll learn more about localization in Chapter 17.
Now the domain layer takes responsibility for enforcing its own rules. No matter how many different UI technologies try to create and save new Appointment
objects, they'll all be subject to the same rules whether they like it or not. If the data isn't acceptable, the operation will be aborted with a RulesException
.
But hang on a minute, what's a RulesException
? This is just a custom exception type that can store a collection of error messages. You can put it into your domain model project and use it throughout your solution. Here it is:
public class RulesException : Exception { public readonly IList<RuleViolation> Errors = new List<RuleViolation>(); private readonly static Expression<Func<object, object>> thisObject = x => x; public void ErrorForModel(string message) {
Errors.Add(new RuleViolation { Property = thisObject, Message = message }); } public class RuleViolation { public LambdaExpression Property { get; set; } public string Message { get; set; } } } // Strongly-typed version permits lambda expression syntax to reference properties public class RulesException<TModel> : RulesException { public void ErrorFor<TProperty>(Expression<Func<TModel, TProperty>> property, string message) { Errors.Add(new RuleViolation { Property = property, Message = message }); } }
Now you can update BookingController
's MakeBooking()
action so that it calls AppointmentService
to save a new Appointment
object and deals with any RulesException
that may occur.
[HttpPost] public ActionResult MakeBooking(Appointment appt, bool acceptsTerms) { if (!acceptsTerms) ModelState.AddModelError("acceptsTerms", "You must accept the terms");try {
if (ModelState.IsValid) // Not worth trying if we already know it's bad
AppointmentService.CreateAppointment(appt);
}
catch (RulesException ex) {
ex.CopyTo(ModelState); // To be implemented in a moment
}
if (ModelState.IsValid) { return View("Completed", appt); } else return View(); // Re-renders the same view so the user can fix the errors }
To copy any error messages from a RulesException
into ModelState
, here's a helpful extension method that you can put inside your ASP.NET MVC project:
internal static class RulesViolationExceptionExtensions { public static void CopyTo(this RulesException ex, ModelStateDictionary modelState) { CopyTo(ex, modelState, null); } public static void CopyTo(this RulesException ex, ModelStateDictionary modelState,
string prefix) { prefix = string.IsNullOrEmpty(prefix) ? "" : prefix + "."; foreach (var propertyError in ex.Errors) { string key = ExpressionHelper.GetExpressionText(propertyError.Property); modelState.AddModelError(prefix + key, propertyError.Message); } } }
You can use the overload that accepts a prefix
parameter if you are using a prefix when model-binding the incoming model object.
Following this pattern, it's easy to express arbitrarily sophisticated rules in plain C# code. You don't have to express complex rules as custom ModelValidator
classes that wouldn't be respected by other technologies anyway. Your rules can even depend on other data (such as stock levels) or what roles the current user is in. It's just basic object-oriented programming—throwing an exception if you need to abort an operation.
Exceptions are the ideal mechanism for this job because they can't be ignored and they can contain a description of why the operation was rejected. Controllers don't need to be told in advance what errors to look for, or even at what points a RulesException
might be thrown. As long as it happens within a try...catch
block, error information will automatically bubble up to the UI without any extra work.
As an example of this, imagine that you have a new business requirement: you can only book one appointment for each day. The robust way to enforce this is as a UNIQUE
constraint in your database for the column corresponding to Appointment
's AppointmentDate
property. Exactly how to do that is off topic for this example (it depends on what database platform you're using), but assuming you've done it, then any attempt to submit a clashing appointment would provoke a SqlException
.
Update the AppointmentService
class's Create()
method to translate the SqlException
into a RulesException
, as follows:
public static void CreateAppointment(Appointment appt) { EnsureValidForCreation(appt); try { // To do: Now save the appointment to a database or wherever } catch (SqlException ex) {if (ex.Message.Contains("IX_DATE_UNIQUE")) { // Name of my DB constraint
var clash = new RulesException<Appointment>();
clash.ErrorFor(x => x.AppointmentDate, "Sorry, already booked");
throw clash;
}
throw; // Rethrow any other exceptions to avoid interfering with them } }
This is a key benefit of model-based validation. You don't have to touch any of your controllers or views when you change or add business rules—new rules will automatically bubble up to every associated UI without further effort (as shown in Figure 12-14).
Just because your model layer enforces its own rules doesn't mean you have to stop using ASP.NET MVC's built-in validation support. I find it helpful to think of ASP.NET MVC's validation mechanism as a useful first line of defense that is especially good at generating a client-side validation script with virtually no work. It fits in neatly with the view model pattern (i.e., having simple view-specific models that exist only to transfer data between controllers and views and do not hold business logic): each view model class can use Data Annotations attributes to configure client-side validation.
But still, your domain layer shouldn't trust your UI layer to enforce business rules. The real enforcement code has to go into the domain using some technique like the one you've just seen.
In this chapter you took a detailed tour of the ASP.NET MVC facilities that relate to models and data entry. This include generating UIs from model metadata using template view helpers, defining custom metadata, parsing incoming data automatically using model binding, and performing validation in controllers, on the client, and in your model layer.
In the next chapter, you'll move on to combine many of these techniques to build sophisticated UIs, including multiple-step forms (wizards), CAPTCHA controls, reusable widgets that can include their own application logic, and even your own custom view engine.
[72] Simple types are those that can be converted from a string
using TypeDescriptor.GetConverter()
.
[73] ASP.NET threads by default take their culture mode from the host server, but you can change it, either programmatically by assigning to Thread.CurrentThread.CurrentCulture
, or in Web.config
by adding a node such as <globalization culture="en-GB" />
inside <system.web>
. See Chapter 17 for more about this, including how to autodetect each visitor's preferred culture setting.
[74] When you run in Release mode and don't have a debugger attached, .NET exceptions rarely cause any measurable performance degradation, unless you throw tens of thousands of exceptions per second.
[75] For an example of a dynamic list editor that lets users add or remove any number of items, see my blog post at http://tinyurl.com/mvclist
. Internally, it uses DefaultModelBinder
's support for nonsequential indexes.
[76] ASP.NET MVC 2 actually contains two ways of receiving an uploaded file. It has both HttpFileCollectionValueProvider
and a custom model binder called HttpPostedFileBaseModelBinder
. The custom model binder is really just a holdover from ASP.NET MVC 1, which didn't have such a neat system of value providers. As far as I understand, HttpPostedFileBaseModelBinder
is deprecated and is likely to be removed in ASP.NET MVC 3.
[77] To keep this example focused, AppointmentServices
is just a static class. In practice, you probably want to decouple your controllers from your service classes by having the services implement interfaces and using DI to inject an implementation at runtime. You saw an example of doing this with ProductsRepository
in Chapter 4.
18.222.107.64