Validating the Contact Manager with MVC 2.0 And VS 2010

4 (10 votes)

The MVC 2.0 Framework was finally RTM'd on March 11th 2010, so I took the opportunity to update my Contact Manager application with some of its new features. While the focus of this article is validating the Model using DataAnnotations, I also touch on one or two other new features in the latest release.

Before continuing, I should point out that I have installed VS 2010 RC1, and used it to convert the original project (available for download here) to ASP.NET 4.0.

Most often, when you see validation code acting against user input arriving from a posted form, you see each value being tested for presence, data type and possibly range. Or at least, you should do. Wherever you see a function to receive input, there you see the validation. This means that potentially, the same or similar values have the same code applied in multiple places throughout an application. If you decide that you will not accept a surname longer than 20 characters, that maximum range test needs to be applied in multiple places. Then along comes Jan Vennegoor of Hesselink (the Dutch footballer, currently playing for doomed Hull City FC) to your site to purchase a £50,000.00 diamond earring before his wages drop along with his football club to a lower level. He tries to register with his 22 character surname, and goes away frustrated after sending a stinging comment to the site's web master. You clearly don't want to upset his brother, so you go through all your code, updating the validation functions wherever a surname is tested for length to accept more characters.

Wouldn't this be much easier if the validation constraints were in one central place? DataAnnotations on your model makes this so, and MVC 2.0 includes support for this out-of-the-box.

DataAnnotations have been part of the .NET Framework for some time, but what's new in MVC 2.0 is the ModelMetaData class. This is a container for MetaData about your model, which is used by default by the DataAnnotationsMetaDataProvider - also new. At the point that incoming values are received by an Action method, Model Binding kicks in and attempts to map the values to Action method parameters and even custom objects. In MVC 2.0 the default model binder will now use the DataAnnotationsMetaDataProvider to obtain metadata about an object that the model binder is attempting to map to, and if validation metadata exists, it will attempt to validate the incoming values against properties of the object. This metadata is provided by you, in the form of attributes that decorate properties.

The example I am going to illustrate this with is the Add Contact process from the original application. In that, we used a Custom ViewModel, the ContactPersonViewModel, for adding a contact in the Contact.Add() Action method:


using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.ComponentModel;

namespace ContactManagerMVC.Views.ViewModels
{
  public class ContactPersonViewModel
  {
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string MiddleName { get; set; }
    public string LastName { get; set; }
    public DateTime DateOfBirth { get; set; }
    public IEnumerable<SelectListItem> Type { get; set; }
  }
}

Now I'm going to add some attributes:


using System;
using System.Collections.Generic;
using System.Web.Mvc;
using System.ComponentModel.DataAnnotations;
using ContactManagerMVC.Attributes;
using System.ComponentModel;

namespace ContactManagerMVC.Views.ViewModels
{
  public class ContactPersonViewModel
  {
    public int Id { get; set; }
    [Required(ErrorMessage = "Please provide a First Name!")]
    [StringLength(25, ErrorMessage = "First name must be less than 25 characters!")]
    [DisplayName("First Name")]
    public string FirstName { get; set; }

    [DisplayName("Middle Name")]
    public string MiddleName { get; set; }

    [Required(ErrorMessage = "Please provide a Last Name!")]
    [StringLength(25, ErrorMessage = "Last name must be less than 25 characters!")]
    [DisplayName("Last Name")]      
    public string LastName { get; set; }

    [Required(ErrorMessage = "You must provide a Date Of Birth!")]
    [BeforeTodaysDate(ErrorMessage = "You can't add someone who hasn't been born yet!")]
    [DisplayName("Date Of Birth")]
    public DateTime? DateOfBirth { get; set; }

    public IEnumerable<SelectListItem> Type { get; set; }
  }
}

Most of these attributes are provided by System.ComponentModel.Annotations, but one of them is not. The RequiredAttribute states that the field has to have a value supplied, and includes an ErrorMessage property. You can pass in your own string for display here, although a default one is provided. The StringLengthAttribute specifies the minimum and maximum number of characters that will be accepted. When combined with the RequiredAttribute, only a maximum value needs to be provided. The DisplayNameAttribute provides a means for how the property is treated when used for display purposes.

The attribute which is not provided by the .NET Framework is the BeforeTodaysDateAttribute. This is a custom attribute, which tests a DateTime value to see if it is before the current date and time. You can see from the ErrorMessage value provided, that it is intended to prevent any contact being added to the system who has not been born yet. Here's the code for the attribute:


using System.ComponentModel.DataAnnotations;
using System;

namespace ContactManagerMVC.Attributes
{
  public class BeforeTodaysDateAttribute : ValidationAttribute
  {
    public override bool IsValid(object value)
    {
      if (value == null)
      {
        return true;
      }
      DateTime result;
      if (DateTime.TryParse(value.ToString(), out result))
      {
        if (result < DateTime.Now)
        {
          return true;
        }
      }
      return false;
    }
  }
}

Quite simply, this class inherits from ValidationAttribute and overrides the IsValid() virtual method. It returns true if no value has been provided (the RequiredAttribute takes care of validation there) or if the value is indeed no later than DateTime.Now.

Attributes make it really easy to apply validation rules in a declarative manner in one place. Now, wherever the ContactPersonViewModel is used within the application, all validation will be present. This was always the case (http://www.asp.net/learn/mvc/tutorial-39-cs.aspx) but now DataAnnotations support are included with the DefaultModelBinder. so lets see how the new version of the Add Partial View looks:


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ContactPersonViewModel>" %>

<script type="text/javascript">
  $(function() {
  $('#DateOfBirth').datepicker({ dateFormat: 'yy/mm/dd' });
  });
  $('#save').click(function () {
      $.ajax({
          type: "POST",
          url: $("#AddContact").attr('action'),
          data: $("#AddContact").serialize(),
          dataType: "text/plain",
          success: function (response) {
              if (response == "Saved") {
                  window.location = "/"; 
              }else {
                  $("#details").html(response);
              }
          }
      });
  });
</script>

<% using (Html.BeginForm("Add", "Contact", FormMethod.Post, new { id = "AddContact" }))
   {%>
      <table>
        <tr>
            <td class="LabelCell"><%= Html.LabelFor(m => m.FirstName)%> </td>
            <td><%= Html.TextBox(m => m.FirstName)%> 
            <%= Html.ValidationMessageFor(m => m.FirstName)%></td>  
        </tr>
        <tr>
            <td class="LabelCell"><%= Html.LabelFor(m => m.MiddleName)%> </td>
            <td><%= Html.TextBox(m => m.MiddleName)%></td>  
        </tr>
        <tr>
            <td class="LabelCell"><%= Html.LabelFor(m => m.LastName)%> </td>
            <td><%= Html.TextBox(m => m.LastName)%> 
            <%= Html.ValidationMessageFor(m => m.LastName)%></td>  
        </tr>
        <tr>
            <td class="LabelCell"><%= Html.LabelFor(m => m.DateOfBirth)%> </td>
            <td><%= Html.TextBox(m => m.DateOfBirth)%> 
            <%= Html.ValidationMessageFor(m => m.DateOfBirth)%></td>  
        </tr>
        <tr>
          <td class="LabelCell"><%= Html.LabelFor(m => m.Type)%></td>
          <td><%= Html.DropDownList("Type")%>
          </td>
        </tr>
        <tr>
          <td class="LabelCell"></td>
          <td><input type="button" name="save" id="save" value="Save" /></td>
        </tr>
      </table>
<% } %>


 

You can see the new strongly typed HTML Helpers at work here. Two modifications to the previous project are to be found in the jQuery code. The first is that the Add Contact partial is now submitted via AJAX, and if the validation fails, the form is displayed again. If it passes, and a new contact is added, the page refreshes to show the revised List view, including the new contact. The Submit button in the form has been replaced with a simple html button to which a form submission has been attached within the onclick event. Let's have a look at what happens if the button is clicked without any data being entered into the forms fields:

The Action method needs to be altered from the original for a couple of reasons. The first is to change the method parameter to accept a ContactPersonViewModel instead of a ContactPerson, because that's the Model that the validation attributes have been applied to. If we left it like it was, as a ContactPerson, the model binder would map the incoming values successfully, but find no constraints on the ContactPerson properties, and would set ModelState to IsValid even though required values (according to the attributes on the ContactPersonViewModel) were missing. The second thing to change is the actual check on ModelState, otherwise the whole validation thing would be pointless.


[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Add([Bind(Exclude = "Id, Type")]ContactPersonViewModel person)
{

    if (ModelState.IsValid)
    {
        var p = new ContactPerson
        {
            FirstName = person.FirstName,
            MiddleName = person.MiddleName,
            LastName = person.LastName,
            Type = Request.Form["Type"].ParseEnum<PersonType>()
        };
        if (person.DateOfBirth != null)
            p.DateOfBirth = (DateTime)person.DateOfBirth;
        ContactPersonManager.Save(p);
        return Content("Saved");
    }
    var personTypes = Enum.GetValues(typeof(PersonType))
    .Cast<PersonType>()
    .Select(p => new
    {
        ID = p,
        Name = p.ToString()
    });
    person.Type = new SelectList(personTypes, "ID", "Name");
    return PartialView(person);
}

In the model binding, I exclude the Id and the Type properties. There is no Id for a new contact until they have been committed to the database. The Type is excluded because in the ViewModel, it's datatype is a SelectList, whereas the BLL method that performs the data persistence expects a ContactPerson object, and its Type property is an Enum. If the ModelState.IsValid, the properties of the ViewModel are mapped to a new ContactPerson object and passed into the method. If not, the data is passed back to the view for display with the validation error messages.

One thing to note is the ParseEnum<T> extension method which is used on the Request.Form["Type"] string value. That's why Type was excluded from the binding, so that it could be converted to its proper datatype. The extension method (used in my Google Analytics article) is as follows:


public static T ParseEnum<T>(this string token)
{
    return (T)Enum.Parse(typeof(T), token);
}

The Edit Action method gets a similar treatment, as does the Partial View except for one main change with the DateOfBirth:

        

<tr>
  <td class="LabelCell"><%= Html.LabelFor(m => m.DateOfBirth)%> </td>
  <td><%= Html.EditorFor(m => m.DateOfBirth)%> 
      <%= Html.ValidationMessageFor(m => m.DateOfBirth)%></td>  
</tr>


Instead of the TextBoxFor<T> helper, I have used the EditorFor<T> method instead. By default, DateTimes are displayed with the hh:mm:ss part, which I didn't want. So I have to create a template (which works with EditorFor) to specify the format for display. In the Shared folder under Views, I added another folder called EditorTemplates (which is the name that MVC expects, and a location it searches) and added a Partial View called DateTime. This also follows the convention within MVC, in that it will look for templates in a specific location named after datatypes:

The code in the Partial replaces the default way that EditorFor items of type DateTime are rendered:


<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<System.DateTime?>" %>
<%= Html.TextBox("", Model.HasValue ? Model.Value.ToShortDateString() : string.Empty) %>


It's only two lines, but it renders a TextBox with an empty string if there is no date, or a DateTime to Short Date format.

Summary

This article has examined validation within ASP.NET MVC 2.0 using DataAnnotations, which are now supported as part of the framework. It has also touched on one or two of the other new things introduced in the latest release, including strongly typed HTML Helpers and templating. The download associated with this article has been created using VS 2010 RC1, so it will not run within VS or VWD 2008.

The application has been enhanced with the addition of validation, but it is far from the finished piece. There is still much room for improvement and future articles will attempt to provide that.

Download the code

You might also like...

Date Posted:
Last Updated:
Posted by:
Total Views to date: 17783

7 Comments

- CareySon

hi mike,i download the code above and test it.and i found the add contact can not work correctly.and i trace the jquery code by firebug then i found it because the Html.TextBoxFor method does not exists..so..where this helper method come from?

is that because my environment is vs2010 beta2 rather than vs2010 RTM?

- Mike

@Careyson

I'm not sure. I used VS 2010 RC1 and the RTM version of MVC 2.0. It works in that environment.

- Nick

I found that to be a very informative post. You will endear yourself well to many people by continuing to take swipes at Hull City, too :)

- Elvin Asadov

Thank you very much for putting this to this page. It helps me a lot. Thanks

- arun

Good article

- Bharat Devada

Dear Sir,

The download link provided here is not working (shows errors : The resource cannot be found.), please update it.


Thanks & Regards,
Bharat

- Mike

@Bharat,

The link should work now.

Recent Comments

Pam 30/08/2017 11:30
In response to Sending Email in Razor Pages
Mike, RazorPages sound like a nice choice for somebody still working in ASP classic who wants to to...

Robby Robson 15/08/2017 00:43
In response to Routing in Razor Pages
Mike: great stuff. Now that .Core Standard 2.0 is formally out, how soon will you rewrite your book...

Satyabrata Mohapatra 28/07/2017 08:59
In response to Sending Email in Razor Pages
Bit off topic, but congratulation sir for your MVP award. You deserve it !!!...

Satyabrata Mohapatra 23/07/2017 16:43
In response to Razor Pages - The Elevator Pitch
@Dale Severin You can continue to build apps using asp.net web pages....

Satyabrata Mohapatra 23/07/2017 16:40
In response to Sending Email in Razor Pages
Thanks for sharing...learned a lot...

Gfw 22/07/2017 11:53
In response to Sending Email in Razor Pages
Question... Does System.Net.Mail support SSL?...

Dale Severin 20/07/2017 03:38
In response to Razor Pages - The Elevator Pitch
I work with razor web pages extensively. I appreciate the rapid development it permits me to I am as...

Obinna Okafor 14/07/2017 01:19
In response to Routing in Razor Pages
Thank you, Mike. Good post....

Satyabrata Mohapatra 11/07/2017 16:02
In response to Routing in Razor Pages
Very powerful routing system!!...

Cyrus 05/07/2017 03:41
In response to Razor Pages - Getting Started With The Preview
How can I trim packages and services as much as possible to use just razor pages? I don’t want to to...