Professional ASP.NET MVC 5 (2014)
Chapter 6
Data Annotations and Validation
—by K. Scott Allen
What's In This Chapter?
· Using data annotations for validation
· Creating your own validation logic
· Using model metadata annotations
WROX.COM CODE DOWNLOADS FOR THIS CHAPTER
You can find the www.wrox.com code downloads for this chapter at http://www.wrox.com/go/proaspnetmvc5 on the Download Code tab. The code for this chapter is contained in the following file: Wrox.ProMvc5.C06.zip.
Validating user input has always been challenging for web developers. Not only do you want validation logic executing in the browser, but you also must have validation logic running on the server. The client validation logic gives users instant feedback on the information they enter into a form, and is an expected feature in today's web applications. Meanwhile, the server validation logic is in place because you should never trust information arriving from the network.
When you look at the bigger picture, however, you realize how logic is only one piece of the validation story. You also need to manage the user-friendly (and often localized) error messages associated with validation logic, place the error messages in your UI, and provide some mechanism for users to recover gracefully from validation failures.
If validation sounds like a daunting chore, you'll be happy to know the MVC framework can help you with the job. This chapter is devoted to giving you everything you need to know about the validation components of the MVC framework.
When you talk about validation in an MVC design pattern context, you are primarily focusing on validating model values. Did the user provide a required value? Is the value in range? The ASP.NET MVC validation features can help you validate model values. The validation features are extensible—you can build custom validation schemes to work in any manner you require—but the default approach is a declarative style of validation using attributes known as data annotations.
In this chapter, you see how data annotations work with the MVC framework. You also see how annotations go beyond just validation. Annotations are a general-purpose mechanism you can use to feed metadata to the framework, and the framework not only drives validation from the metadata, but also uses the metadata when building the HTML to display and edit models. Let's start by looking at a validation scenario.
ANNOTATING ORDERS FOR VALIDATION
A user who tries to purchase music from the ASP.NET MVC Music Store will go through a typical shopping cart checkout procedure. The procedure requires payment and shipping information. In this chapter, you'll learn about form validation by looking at some examples using that shopping cart scenario.
For these examples, you'll continue with the stripped-down Music Store sample from Chapter 4 (available for download as MvcMusicStore.C04.zip). As a refresher, the application contains the following application-specific model class files (in addition toAccountViewModels.cs and IdentityModels.cs, which were created with the project template):
· Album.cs
· Artist.cs
· MusicStoreDB.cs
· MusicStoreDbInitializer.cs
To add support for a shopping cart, you'll next add an Order.cs class to the models directory. The Order class represents everything the application needs to complete a checkout, as shown in Listing 6.1.
Listing 6.1: Order.cs
public class Order
{
public int OrderId { get; set; }
public DateTime OrderDate { get; set; }
public string Username { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public decimal Total { get; set; }
public List<OrderDetail> OrderDetails { get; set; }
}
Some of the properties in the Order class require user input (such as FirstName and LastName), whereas the application derives other property values from the environment, or looks them up from the database (such as the Username property, because a user must log in before checking out—thus the application will already have the value).
In order to focus specifically on the form validation topic, this chapter covers a scaffolded OrderController, which is strongly typed to the Order class, and you'll be examining the /Views/Order/Edit.cshtml view.
Note
This focused example scenario enables you to concentrate on form validation. An actual store would include classes, logic, and controllers to support things such as shopping cart management, multiple step checkout, and anonymous shopping cart migration to a registered account.
In the MVC Music Store tutorial, the shopping and checkout processes are split into a ShoppingCartController and a CheckoutController.
Don't get confused or worried when these examples show saving order data directly to an OrderController without any store-specific logic. Remember that the focus in this chapter is on data annotations and form validation, and the fields on an order form provide some pretty good examples for that.
Right-click on the controllers directory and scaffold a new controller using the “MVC 5 Controller with views, using Entity Framework” scaffold template. Name the controller OrderController and set the model class to Order as shown in Figure 6.1, and then click the Add button.
Figure 6.1
Next, run the application and browse to /Order/Create as shown in Figure 6.2.
Figure 6.2
The form has some visible problems. For example, you do not want the customer to enter an OrderDate or the order Total amount. The application will set the values of these properties on the server. Also, although the input labels might make sense to a developer (FirstName is obviously a property name), the labels will probably leave a customer bewildered (was someone's spacebar broken?). You'll fix these problems later in the chapter.
For now, a more serious problem exists that you can't see reflected in the screenshot of Figure 6.2. The problem is that customers can leave nearly the entire form blank and click the Submit Order button at the bottom of the form. The application will not tell them how they need to provide critically important information such as their name and address. You'll fix this problem using data annotations.
Note
The scaffolded form does automatically require the two non-string properties, OrderDate and Total. More on why those work in just a minute.
Using Validation Annotations
Data annotations are attributes you can find in the System.ComponentModel.DataAnnotations namespace (although one attribute is defined outside this namespace, as you will see). These attributes provide server-side validation, and the framework also supports client-side validation when you use one of the attributes on a model property. You can use four attributes in the DataAnnotations namespace to cover common validation scenarios. Let's start by looking at the Required attribute.
Required
Because you need the customers to give you their first and last name, you can decorate the FirstName and LastName properties of the Order model with the Required attribute (remembering to add a using statement for System.ComponentModel.DataAnnotations):
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
The updated Order class appears as shown in Listing 6.2.
Listing 6.2: Order.cs (updated for required fields)
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
namespace MvcMusicStore.Models
{
public class Order
{
public int OrderId { get; set; }
public DateTime OrderDate { get; set; }
public string Username { get; set; }
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
public string Address { get; set; }
public string City { get; set; }
public string State { get; set; }
public string PostalCode { get; set; }
public string Country { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public decimal Total { get; set; }
}
}
The attribute raises a validation error if either property value is null or empty. (You will learn how to deal with validation errors in just a bit.)
Like all the built-in validation attributes, the Required attribute delivers both server-side and client-side validation logic (although internally, it is another, different component in the MVC framework that delivers the client-side validation logic for the attribute through a validation adapter design).
With the attribute in place, if customers try to submit the form without providing a last name, they'll see the default error shown in Figure 6.3.
Figure 6.3
However, even if customers do not have JavaScript enabled in their browser, the validation logic will catch an empty name property on the server, and they'll see the exact same error message. Assuming your controller action is implemented correctly (which I promise I will talk about in just a bit), users will still see the error message in Figure 6.3. This client-server synchronized validation is a pretty big deal—enforcing the same rules in JavaScript and on the server is important. Attribute-based validation ensures that your client- and server-side validation rules are kept in sync, because they're declared in only one place.
StringLength
You've forced customers to enter their names, but what happens if they enter a name of enormous length? Wikipedia says the longest name ever used belonged to a German typesetter who lived in Philadelphia. His full name is more than 500 characters long. Although the .NET string type can store (in theory) gigabytes of Unicode characters, the MVC Music Store database schema sets the maximum length for a name at 160 characters. If you try to insert a larger name into the database, you'll have an exception on your hands. The StringLength attribute can ensure the string value provided by the customer will fit in the database:
[Required]
[StringLength(160)]
public string FirstName { get; set; }
[Required]
[StringLength(160)]
public string LastName { get; set; }
Notice how you can stack multiple validation attributes on a single property. With the attribute in place, if customers enter too many characters, they'll see the default error message shown below the LastName field in Figure 6.4.
Figure 6.4
MinimumLength is an optional, named parameter you can use to specify the minimum length for a string. The following code requires the FirstName property to contain a string with three or more characters (and less than or equal to 160 characters) to pass validation:
[Required]
[StringLength(160, MinimumLength=3)]
public string FirstName { get; set; }
RegularExpression
Some properties of Order require more than a simple presence or length check. For example, you want to ensure the Email property of an Order contains a valid, working e-mail address. Unfortunately, ensuring an e-mail address is working without sending a mail message and waiting for a response is practically impossible. What you can do instead is ensure the value looks like a working e-mail address using a regular expression:
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}")]
public string Email { get; set; }
Regular expressions are an efficient and terse means to enforce the shape and contents of a string value. If the customer gives you an e-mail address and the regular expression doesn't think the string looks like an e-mail address, the error in Figure 6.5 appears to the customer.
Figure 6.5
To someone who isn't a developer (and even to some developers, too), the error message looks like someone sprinkled catnip on a keyboard before letting a litter of Norwegian Forest Cats run wild. You see how to make a friendlier error message in the next section.
Range
The Range attribute specifies minimum and maximum constraints for a numerical value. If the Music Store only wanted to serve middle-aged customers, you could add an Age property to the Order class and use the Range attribute as in the following code:
[Range(35,44)]
public int Age { get; set; }
The first parameter to the attribute is the minimum value, and the second parameter is the maximum value. The values are inclusive. The Range attribute can work with integers and doubles, and another overloaded version of the constructor takes a Type parameter and two strings (which can allow you to add a range to date and decimal properties, for example).
[Range(typeof(decimal), "0.00", "49.99")]
public decimal Price { get; set; }
Compare
Compare ensures two properties on a model object have the same value. For example, you might want to force customers to enter their e-mail address twice to ensure they didn't make a typographical error:
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}")]
public string Email { get; set; }
[Compare("Email")]
public string EmailConfirm { get; set; }
If users don't enter the exact e-mail address twice, they'll see the error in Figure 6.6.
Figure 6.6
Remote
The ASP.NET MVC framework adds an additional Remote validation attribute. This attribute is in the System.Web.Mvc namespace.
The Remote attribute enables you to perform client-side validation with a server callback. Take, for example, the UserName property of the RegisterModel class in the MVC Music Store. No two users should have the same UserName value, but validating the value on the client to ensure the value is unique is difficult (to do so you would have to send every single username from the database to the client). With the Remote attribute you can send the UserName value to the server, and compare the value against the values in the database.
[Remote("CheckUserName", "Account")]
public string UserName { get; set; }
Inside the attribute you can set the name of the action, and the name of the controller the client code should call. The client code will send the value the user entered for the UserName property automatically, and an overload of the attribute constructor allows you to specify additional fields to send to the server.
public JsonResult CheckUserName(string username)
{
var result = Membership.FindUsersByName(username).Count == 0;
return Json(result, JsonRequestBehavior.AllowGet);
}
The controller action will take a parameter with the name of the property to validate and return a true or false wrapped in JavaScript Object Notation (JSON). You'll see more JSON, AJAX, and client-side features in Chapter 8.
Remote only exists because data annotations are extensible. You look at building a custom annotation later in the chapter. For now, let's look at customizing the error messages on display for a failed validation rule.
Custom Error Messages and Localization
Every validation attribute allows you to pass a named parameter with a custom error message. For example, if you don't like the default error message associated with the RegularExpression attribute (because it displays a regular expression), you could customize the error message with the following code:
[RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}",
ErrorMessage="Email doesn't look like a valid email address.")]
public string Email { get; set; }
ErrorMessage is the name of the parameter in every validation attribute.
[Required(ErrorMessage="Your last name is required")]
[StringLength(160, ErrorMessage="Your last name is too long")]
public string LastName { get; set; }
The custom error message can also have a single format item in the string. The built-in attributes format the error message string using the friendly display name of a property (you see how to set the display name in the display annotations later in this chapter). As an example, consider the Required attribute in the following code:
[Required(ErrorMessage="Your {0} is required.")]
[StringLength(160, ErrorMessage="{0} is too long.")]
public string LastName { get; set; }
The attribute uses an error message string with a format item ({0}). If customers don't provide a value, they'll see the error message in Figure 6.7.
Figure 6.7
In applications built for international markets, the hard-coded error messages are a bad idea. Instead of literal strings, you'll want to display different text for different locales. Fortunately, all the validation attributes also allow you to specify a resource type and a resource name for localized error messages:
[Required(ErrorMessageResourceType=typeof(ErrorMessages),
ErrorMessageResourceName="LastNameRequired")]
[StringLength(160, ErrorMessageResourceType = typeof(ErrorMessages),
ErrorMessageResourceName = "LastNameTooLong")]
public string LastName { get; set; }
The preceding code assumes you have a resource file in the project named ErrorMessages.resx with the appropriate entries inside (LastNameRequired and LastNameTooLong). For ASP.NET to use localized resource files, you must have the UICulture property of the current thread set to the proper culture. See “How to: Set the Culture and UI Culture for ASP.NET Web Page Globalization” at http://msdn.microsoft.com/en-us/library/bz9tc508.aspx for more information.
Looking Behind the Annotation Curtain
Before looking at how to work with validation errors in your controller and views, and before looking at building a custom validation attribute, understanding what is happening with the validation attributes behind the scenes is worthwhile. The validation features of ASP.NET MVC are part of a coordinated system involving model binders, model metadata, model validators, and model state.
Validation and Model Binding
As you were reading about the validation annotations, you might have asked a couple of obvious questions: When does validation occur? How do I know whether validation failed?
By default, the ASP.NET MVC framework executes validation logic during model binding. As discussed in Chapter 4, the model binder runs implicitly when you have parameters to an action method:
[HttpPost]
public ActionResult Create(Album album)
{
// the album parameter was created via model binding
// ..
}
You can also explicitly request model binding using the UpdateModel or TryUpdateModel methods of a controller:
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
var album = storeDB.Albums.Find(id);
if(TryUpdateModel(album))
{
// ...
}
}
After the model binder is finished updating the model properties with new values, the model binder uses the current model metadata and ultimately obtains all the validators for the model. The MVC runtime provides a validator to work with data annotations (theDataAnnotationsModelValidator). This model validator can find all the validation attributes and execute the validation logic inside. The model binder catches all the failed validation rules and places them into model state.
Validation and Model State
The primary side effect of model binding is model state (accessible in a Controller-derived object using the ModelState property). Not only does model state contain all the values a user attempted to put into model properties, but model state also contains all the errors associated with each property (and any errors associated with the model object itself). If any errors exist in model state, ModelState.IsValid returns false.
As an example, imagine the user submits the checkout page without providing a value for LastName. With the Required validation annotation in place, all the following expressions will return true after model binding occurs:
ModelState.IsValid == false
ModelState.IsValidField("LastName") == false
ModelState["LastName"].Errors.Count > 0
You can also look in model state to see the error message associated with the failed validation:
var lastNameErrorMessage = ModelState["LastName"].Errors[0].ErrorMessage;
Of course, you rarely need to write code to look for specific error messages. Just as the run time automatically feeds validation errors into model state, it can also automatically pull errors out of model state. As discussed in Chapter 5, the built-in HTML helpers use model state (and the presence of errors in model state) to change the display of the model in a view. For example, the ValidationMessage helper displays error messages associated with a particular piece of view data by looking at model state.
@Html.ValidationMessageFor(m => m.LastName)
The only question a controller action generally needs to ask is this: Is the model state valid or not?
Controller Actions and Validation Errors
Controller actions can decide what to do when model validation fails, and what to do when model validation succeeds. In the case of success, an action generally takes the steps necessary to save or update information for the customer. When validation fails, an action generally re-renders the same view that posted the model values. Re-rendering the same view allows the user to see all the validation errors and to correct any typos or missing fields. The AddressAndPayment action shown in the following code demonstrates a typical action 'margin-bottom:0cm;margin-bottom:.0001pt;line-height: normal;vertical-align:baseline'>[HttpPost]
public ActionResult AddressAndPayment(Order newOrder)
{
if (ModelState.IsValid)
{
newOrder.Username = User.Identity.Name;
newOrder.OrderDate = DateTime.Now;
storeDB.Orders.Add(newOrder);
storeDB.SaveChanges();
// Process the order
var cart = ShoppingCart.GetCart(this);
cart.CreateOrder(newOrder);
return RedirectToAction("Complete", new { id = newOrder.OrderId });
}
// Invalid -- redisplay with errors
return View(newOrder);
}
The code checks the IsValid flag of ModelState immediately. The model binder will have already built an Order object and populated the object with values supplied in the request (posted form values). When the model binder is finished updating the order, it runs any validation rules associated with the object, so you'll know whether the object is in a good state or not. You could also implement the action using an explicit call to UpdateModel or TryUpdateModel.
[HttpPost]
public ActionResult AddressAndPayment(FormCollection collection)
{
var newOrder = new Order();
UpdateModel(newOrder);
if (ModelState.IsValid)
{
newOrder.Username = User.Identity.Name;
newOrder.OrderDate = DateTime.Now;
storeDB.Orders.Add(newOrder);
storeDB.SaveChanges();
// Process the order
var cart = ShoppingCart.GetCart(this);
cart.CreateOrder(newOrder);
return RedirectToAction("Complete", new { id = newOrder.OrderId });
}
// Invalid -- redisplay with errors
return View(newOrder);
}
In this example, we're explicitly binding using UpdateModel, then checking the ModelState. You can simplify that to one step using TryUpdateModel, which binds and returns the results, as shown below:
[HttpPost]
public ActionResult AddressAndPayment(FormCollection collection)
{
var newOrder = new Order();
if(TryUpdateModel(newOrder));
{
newOrder.Username = User.Identity.Name;
newOrder.OrderDate = DateTime.Now;
storeDB.Orders.Add(newOrder);
storeDB.SaveChanges();
// Process the order
var cart = ShoppingCart.GetCart(this);
cart.CreateOrder(newOrder);
return RedirectToAction("Complete", new { id = newOrder.OrderId });
}
// Invalid -- redisplay with errors
return View(newOrder);
}
There are many variations on the theme, but notice that in both implementations the code checks whether model state is valid, and if model state is not valid the action re-renders the AddressAndPayment view to give the customer a chance to fix the validation errors and resubmit the form.
We hope that you can see how easy and transparent validation can be when you work with the annotation attributes. Of course, the built-in attributes cannot cover all the possible validation scenarios you might have for your application. Fortunately, creating your own custom validations is easy.
CUSTOM VALIDATION LOGIC
The extensibility of the ASP.NET MVC framework means an infinite number of possibilities exist for implementing custom validation logic. However, this section focuses on two core scenarios:
· Packaging validation logic into a custom data annotation
· Packaging validation logic into a model object itself
Putting validation logic into a custom data annotation means you can easily reuse the logic across multiple models. Of course, you have to write the code inside the attribute to work with different types of models, but when you do, you can place the new annotation anywhere.
On the other hand, adding validation logic directly to a model object often means the validation logic itself is easier to write (you only need to worry about the logic working with a single type of object, and thus you can more easily make assumptions about the state or shape of the object). Reusing the logic, however, is more difficult.
You'll see both approaches in the following sections, starting with writing a custom data annotation.
Custom Annotations
Imagine you want to restrict the last name value of a customer to a limited number of words. For example, you might say that 10 words are too many for a last name. You also might decide that this type of validation (limiting a string to a maximum number of words) is something you can reuse with other models in the Music Store application. If so, the validation logic is a candidate for packaging into a reusable attribute.
All the validation annotations (such as Required and Range) ultimately derive from the ValidationAttribute base class. The base class is abstract and lives in the System.ComponentModel.DataAnnotations namespace. Your validation logic will also live in a class deriving fromValidationAttribute:
using System.ComponentModel.DataAnnotations;
namespace MvcMusicStore.Infrastructure
{
public class MaxWordsAttribute : ValidationAttribute
{
}
}
To implement the validation logic, you need to override one of the IsValid methods provided by the base class. Overriding the IsValid version taking a ValidationContext parameter provides more information to use inside the IsValid method (the ValidationContextparameter gives you access to the model type, model object instance, and friendly display name of the property you are validating, among other pieces of information).
public class MaxWordsAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(
object value, ValidationContext validationContext)
{
return ValidationResult.Success;
}
}
The first parameter to the IsValid method is the value to validate. If the value is valid you can return a successful validation result, but before you can determine whether the value is valid, you'll need to know how many words are too many. You can do this by adding a constructor to the attribute and forcing the client to pass the maximum number of words as a parameter:
public class MaxWordsAttribute : ValidationAttribute
{
public MaxWordsAttribute(int maxWords)
{
_maxWords = maxWords;
}
protected override ValidationResult IsValid(
object value, ValidationContext validationContext)
{
return ValidationResult.Success;
}
private readonly int _maxWords;
}
Now that you've parameterized the maximum word count, you can implement the validation logic to catch an error:
public class MaxWordsAttribute : ValidationAttribute
{
public MaxWordsAttribute(int maxWords)
{
_maxWords = maxWords;
}
protected override ValidationResult IsValid(
object value, ValidationContext validationContext)
{
if (value != null)
{
var valueAsString = value.ToString();
if (valueAsString.Split(" ").Length > _maxWords)
{
return new ValidationResult("Too many words!");
}
}
return ValidationResult.Success;
}
private readonly int _maxWords;
}
You are doing a relatively naïve check for the number of words by splitting the incoming value using the space character and counting the number of strings the Split method generates. If you find too many words, you return a ValidationResult object with a hard-coded error message to indicate a validation error.
The problem with the last block of code is the hard-coded error message. Developers who use the data annotations will expect to have the ability to customize an error message using the ErrorMessage property of ValidationAttribute. To follow the pattern of the other validation attributes, you need to provide a default error message (to be used if the developer doesn't provide a custom error message) and generate the error message using the name of the property you are validating:
public class MaxWordsAttribute : ValidationAttribute
{
public MaxWordsAttribute(int maxWords)
:base("{0} has too many words.")
{
_maxWords = maxWords;
}
protected override ValidationResult IsValid(
object value, ValidationContext validationContext)
{
if (value != null)
{
var valueAsString = value.ToString();
if (valueAsString.Split(" ").Length > _maxWords)
{
var errorMessage = FormatErrorMessage(
validationContext.DisplayName);
return new ValidationResult(errorMessage);
}
}
return ValidationResult.Success;
}
private readonly int _maxWords;
}
There are two changes in the preceding code:
· First, you pass along a default error message to the base class constructor. You should pull this default error message from a resource file if you are building an internationalized application.
· Second, notice how the default error message includes a parameter placeholder ({0}). The placeholder exists because the second change, the call to the inherited FormatErrorMessage method, will automatically format the string using the display name of the property.
FormatErrorMessage ensures you use the correct error message string (even if the string is localized into a resource file). The code needs to pass the value of this name, and the value is available from the DisplayName property of the validationContext parameter. With the validation logic in place, you can apply the attribute to any model property:
[Required]
[StringLength(160)]
[MaxWords(10)]
public string LastName { get; set; }
You could even give the attribute a custom error message:
[Required]
[StringLength(160)]
[MaxWords(10, ErrorMessage="There are too many words in {0}")]
public string LastName { get; set; }
Now if customers type in too many words, they'll see the message in Figure 6.8 in the view.
Note
The MaxWordsAttribute is available as a NuGet package. Search for Wrox.ProMvc5.Validation.MaxWordsAttribute to add the code into your project.
Figure 6.8
A custom attribute is one approach to providing validation logic for models. As you can see, an attribute is easily reusable across a number of different model classes. In Chapter 8, you'll add client-side validation capabilities for the MaxWordsAttribute.
IValidatableObject
A self-validating model is a model object that knows how to validate itself. A model object can announce this capability by implementing the IValidatableObject interface. As an example, implement the check for too many words in the LastName field directly inside theOrder model:
public class Order : IValidatableObject
{
public IEnumerable<ValidationResult> Validate(
ValidationContext validationContext)
{
if (LastName != null &&
LastName.Split(" ").Length > 10)
{
yield return new ValidationResult("The last name has too many words!",
new []{"LastName"});
}
}
// rest of Order implementation and properties
// ...
}
This has a few notable differences from the attribute version:
· The method the MVC runtime calls to perform validation is named Validate instead of IsValid, but more important, the return type and parameters are different.
· The return type for Validate is an IEnumerable<ValidationResult> instead of a single ValidationResult, because the logic inside is ostensibly validating the entire model and might need to return more than a single validation error.
· No value parameter is passed to Validate because you are inside an instance method of the model and can refer to the property values directly.
Notice that the code uses the C# yield return syntax to build the enumerable return value, and the code needs to explicitly tell the ValidationResult the name of the field to associate with (in this case LastName, but the last parameter to the ValidationResult constructor will take an array of strings so you can associate the result with multiple properties).
Many validation scenarios are easier to implement using the IValidatableObject approach, particularly scenarios where the code needs to compare multiple properties on the model to make a validation decision.
At this point I've covered everything you need to know about validation annotations, but additional annotations in the MVC framework influence how the run time displays and edits a model. I alluded to these annotations earlier in the chapter when I talked about a “friendly display name,” and now you've finally reached a point where you can dive in.
DISPLAY AND EDIT ANNOTATIONS
A long time ago, in a paragraph far, far away (at the beginning of this chapter, actually), you were building a form for a customer to submit the information needed to process an order. You did this using the EditorForModel HTML helper, and the form wasn't turning out quite how you expected. Figure 6.9 should help to refresh your memory.
Figure 6.9
Two problems are evident in the screenshot:
· You do not want the Username field to display. (It's populated and managed by code in the controller action.)
· The FirstName field should appear with a space between the words First and Name.
The path to resolving these problems also lies in the DataAnnotations namespace.
Like the validation attributes you looked at previously, a model metadata provider picks up the following display (and edit) annotations and makes their information available to HTML helpers and other components in the MVC runtime. The HTML helpers use any available metadata to change the characteristics of a display and edit UI for a model.
Display
The Display attribute sets the friendly display name for a model property. You can use the Display attribute to fix the label for the FirstName field:
[Required]
[StringLength(160, MinimumLength=3)]
[Display(Name="First Name")]
public string FirstName { get; set; }
With the attribute in place, your view renders as shown in Figure 6.10.
Figure 6.10
In addition to the name, the Display attribute enables you to control the order in which properties will appear in the UI. For example, to control the placement of the LastName and FirstName editors, you can use the following code:
[Required]
[StringLength(160)]
[Display(Name="Last Name", Order=15001)]
[MaxWords(10, ErrorMessage="There are too many words in {0}")]
public string LastName { get; set; }
[Required]
[StringLength(160, MinimumLength=3)]
[Display(Name="First Name", Order=15000)]
public string FirstName { get; set; }
Assuming no other properties in the Order model have a Display attribute, the last two fields in the form should be FirstName, and then LastName. The default value for Order is 10,000, and fields appear in ascending order.
ScaffoldColumn
The ScaffoldColumn attribute hides a property from HTML helpers such as EditorForModel and DisplayForModel:
[ScaffoldColumn(false)]
public string Username { get; set; }
With the attribute in place, EditorForModel will no longer display an input or label for the Username field. Note, however, the model binder might still try to move a value into the Username property if it sees a matching value in the request. You can read more about this scenario (called overposting) in Chapter 7.
The two attributes you've looked at so far can fix everything you need for the order form, but take a look at the rest of the annotations you can use with ASP.NET MVC.
DisplayFormat
The DisplayFormat attribute handles various formatting options for a property via named parameters. You can provide alternate text for display when the property contains a null value, and turn off HTML encoding for properties containing markup. You can also specify a data format string for the runtime to apply to the property value. In the following code you format the Total property of a model as a currency value:
[DisplayFormat(ApplyFormatInEditMode=true, DataFormatString="{0:c}")]
public decimal Total { get; set; }
The ApplyFormatInEditMode parameter is false by default, so if you want the Total value formatted into a form input, you need to set ApplyFormatInEditMode to true. For example, if the Total decimal property of a model were set to 12.1, you would see the output in the view shown in Figure 6.11.
Figure 6.11
One reason ApplyFormatInEditMode is false by default is because the MVC model binder might not like to parse a value formatted for display. In this example, the model binder will fail to parse the price value during postback because of the currency symbol in the field, so you should leave ApplyFormatInEditModel as false.
ReadOnly
Place the ReadOnly attribute on a property if you want to make sure the default model binder does not set the property with a new value from the request:
[ReadOnly(true)]
public decimal Total { get; set; }
Note the EditorForModel helper will still display an enabled input for the property, so only the model binder respects the ReadOnly attribute.
DataType
The DataType attribute enables you to provide the runtime with information about the specific purpose of a property. For example, a property of type string can fill a variety of scenarios—it might hold an e-mail address, a URL, or a password. The DataType attribute covers all of these scenarios. If you look at the Music Store's model for account logon, for example, you'll find the following:
[Required]
[DataType(DataType.Password)]
[Display(Name="Password")]
public string Password { get; set; }
For a DataType of Password, the HTML editor helpers in ASP.NET MVC will render an input element with a type attribute set to password. In the browser, this means you won't see characters appear onscreen when typing a password (see Figure 6.12).
Figure 6.12
Other data types include Currency, Date, Time, and MultilineText.
UIHint
The UIHint attribute gives the ASP.NET MVC runtime the name of a template to use when rendering output with the templated helpers (such as DisplayFor and EditorFor). You can define your own template helpers to override the default MVC behavior, and you'll look at custom templates in Chapter 16. If the template specified by the UIHint is not found, MVC will find an appropriate fallback to use.
HiddenInput
The HiddenInput attribute lives in the System.Web.Mvc namespace and tells the runtime to render an input element with a type of hidden. Hidden inputs are a great way to keep information in a form so the browser will send the data back to the server, but the user won't be able to see or edit the data (although a malicious user could change submitted form values to change the input value, so don't consider the attribute as foolproof).
SUMMARY
In this chapter you looked at data annotations for validation, and saw how the MVC runtime uses model metadata, model binders, and HTML helpers to construct pain-free validation support in a web application. The validation supports both server-side validation and client-validation features with no code duplication. You also built a custom annotation for custom validation logic, and compared the annotation to validation with a self-validating model. Finally, you looked at using data annotations to influence the output of the HTML helpers rendering HTML in your views.