ASP.NET MVC Application Building: Forums #4 – Server-Side Form Validation

In this series of blog entries, I build an entire ASP.NET MVC forums application from start to finish. In this blog entry, I add server-side form validation to the forums application.

This post has been updated. Please read the updated post here:
Forums #4 1/2 - Validation Revisited

Before reading this blog entry, you should read the previous three blog entries in this series:

ASP.NET MVC Application Building: Forums #1 – Create the Perfect Application – In this first entry, I explain the overall goals of the ASP.NET MVC Forums application. I emphasize the importance of Software Design Principles and justify my choice to use test-driven development.

ASP.NET MVC Application Building: Forums #2 – Create the First Unit Test – In the second entry, I build the first unit test and create the Index() action which returns a collection of messages.

ASP.NET MVC Application Building: Forums #3 – Post Messages – In the third entry, I add the unit tests and functionality required to post new messages and replies.

Creating a Form Validation Framework

The ASP.NET MVC framework does not include built-in support for performing form validation. If you want to validate form data before submitting the data to a database then you must write the validation logic yourself. In this section, I describe how you can create a custom form validation framework.

The validation framework that I describe in this section was developed by Brian Henderson and me way back during the Preview 1 state of the ASP.NET MVC framework. I’ve updated the original framework that Brian and I developed so that it integrates more smoothly with the new ViewData.ModelState property introduced with the ASP.NET MVC framework Preview 5.

The validation framework takes advantage of attributes. You decorate the properties that you want to validate with validator attributes. The validation framework includes the following attributes:

· RequiredValidator – Use this attribute to display a validation error message when a property does not have a value.

· TypeValidator – Use this attribute to display a validation error message when a property is not the right type.

· RegularExpressionValidator – Use this attribute to display a validation error message when a property does not match a regular expression.

· LengthValidator – Use this attribute to display a validation error message when the value of an attribute is too long.

· EmailValidator – Use this attribute to display a validation error message when a property does not represent a valid email address.

· USCurrencyValidator – Use this attribute to display a validation error message when a property does not represent a valid U.S. currency amount.

Let’s look at an example of how you can use these attributes. Imagine that you are creating a product catalog application and you want to validate the form that enables you to enter product information (see Figure 1).

Figure 1 – Creating a new product

clip_image002

The view for creating a new product is contained in Listing 1.

Listing 1 – Views\Home\Create.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Create.aspx.cs" Inherits="MvcValidationWebsite.Views.Home.Create" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Create New Product</title>
    
    <style type="text/css">
    
    .input-validation-error
    {
        border: solid 2px red;
    }
    
    </style>
</head>
<body>
    <div>
    
    <form method="post" action="/Home/Create">
    
    <label for="name">Product Name:</label>
    <br />
    <%= Html.TextBox("name") %>
    <%= Html.ValidationMessage("name") %>
    
    <br /><br />
    <label for="price">Product Price:</label>
    <br />
    <%= Html.TextBox("price") %>
    <%= Html.ValidationMessage("price") %>

    <br /><br />
    <label for="description">Product Description:</label>
    <br />
    <%= Html.TextArea("description") %>
    <%= Html.ValidationMessage("description") %>

    <br /><br />
    <label for="saleStartDate">Sale Start Date:</label>
    <br />
    <%= Html.TextBox("saleStartDate") %>
    <%= Html.ValidationMessage("saleStartDate") %>

    <br /><br />
    <label for="saleEndDate">Sale End Date:</label>
    <br />
    <%= Html.TextBox("saleEndDate") %>
    <%= Html.ValidationMessage("saleEndDate") %>

    <br /><br />
    <input type="submit" value="Add Product" />
    
    </form>
    
    </div>
</body>
</html>

There are two special things that you should notice about the view in Listing 1. First, notice that there is a Html.ValidationMessage() helper associated with each input field. This helper is included in the ASP.NET MVC framework. It displays a validation error message.

Second, notice that the view includes a cascading style sheet that defines a class named input-validation-error. When the Html.TextBox() helper renders a input field that has an invalid value, this CSS class is added to the input field automatically. In the view in Listing 1, Input fields associated with validation errors get a red border.

The view for creating a new product is returned from the Home controller. The Home controller is contained in Listing 2.

Listing 2 – Controllers\HomeController.cs

using System;
using System.Globalization;
using System.Linq;
using System.Web.Mvc;
using LinqToSqlExtensions;
using Microsoft.Web.Mvc;
using MvcFakes;
using MvcValidation;
using MvcValidationWebsite.Models;

namespace MvcValidationWebsite.Controllers
{
    [HandleError]
    public class HomeController : Controller
    {
        private IDataContext _dataContext;
        private ITable<Product> _products;

        public HomeController()
        {
            _dataContext = new DataContextWrapper("conProductsDB", "~/Models/ProductsDB.xml");
            _products = _dataContext.GetTable<Product>();
        }

        public ActionResult Index()
        {
            return View("Index", _products.ToList());
        }


        [AcceptVerbs("GET")]
        public ActionResult Create()
        {
            return View("Create");
        }
        
        [AcceptVerbs("POST")]
        public ActionResult Create(FormCollection form)
        {
            // Perform validation
            Validation.Validate<Product>(ViewData.ModelState, form);
            if (!ViewData.ModelState.IsValid)
                return View("Create", form);

            // Create product
            var product = new Product();
            product.Name = form["name"];
            product.Price = Decimal.Parse(form["price"], NumberStyles.Currency);
            product.Description = form["description"];
            if (!String.IsNullOrEmpty(form["saleStartDate"]))
                product.SaleStartDate = DateTime.Parse(form["saleStartDate"]);
            if (!String.IsNullOrEmpty(form["saleEndDate"]))          
                product.SaleEndDate = DateTime.Parse(form["saleEndDate"]);

            // Insert product into database
            _dataContext.Insert(product);

            // Redirect
            return RedirectToAction("Index");
        }

    }
}

Notice that the Home controller has two Create() action. One Create() action is invoked when performing an HTTP GET operation. In other words, the first Create() action is called when the form for creating a new product is first displayed.

The second Create() action is invoked only when performing an HTTP POST operation. This Create() action is called when the product form is posted back to the server. This second Create() method validates the form data and, if there are no validation errors, adds the new product to the database.

Validation is performed with the following three lines of code:

Validation.Validate<Product>(ViewData.ModelState, form);
if (!ViewData.ModelState.IsValid)
   return View("Create", form);

The Validation.Validate() method validates the form parameters passed to the controller action against the Product class. When validation errors are encountered, the errors are added to ModelState. If there are any validation errors, the ModelState.IsValid property returns the value False and the Create view is displayed again. When the Create view is redisplayed, all of the values that the user entered are redisplayed (Notice that the form variable is passed as ViewData to the Create view).

You specify how form fields get validated in the model (validation is model driven). The Product class is contained in Listing 3.

Listing 3 – Models\Product.cs

using System;
using MvcValidation;
using System.Web.Mvc;
using Microsoft.Web.Mvc;
using MvcValidationWebsite.Validators;

namespace MvcValidationWebsite.Models
{
    [CustomValidator(typeof(ProductValidator))]
    public class Product
    {
        public int Id { get; set; }

        [RequiredValidator("Product name is required.")]
        public string Name { get; set; }

        [RequiredValidator("Product price is required.")]
        [USCurrencyValidator("Product price is not a valid currency amount.")]
        public decimal Price { get; set; }

        [RequiredValidator("Product description is required")]
        [LengthValidator(50, "Description too long.")]
        public string Description { get; set; }

        [TypeValidator(typeof(DateTime), "Sale start date must be a valid date.")]
        public DateTime? SaleStartDate { get; set; }

        [TypeValidator(typeof(DateTime), "Sale end date must be a valid date.")]
        public DateTime? SaleEndDate { get; set; }
    }
}

Notice that the validator attributes are applied to the Product properties. For example, the Name property is marked as required with the RequiredValidator attribute. The error message specified by the validator bubbles up to be displayed in the view.

There is one validator attribute that requires additional explanation. The CustomValidator is applied to the class instead of to a class property. The CustomValidator enables you to perform more complicated types of validation logic that might involve several properties.

For example, the Product class has both a StartSaleDate and EndSaleDate property that represent the time period when the product is on sale. The EndSaleDate should always be after the StartSaleDate. The CustomValidator in Listing 3 is used to enforce this validation rule.

Notice that when you add a CustomValidator to a class, you specify a type of object. In Listing 3, the CustomValidator points to a ProductValidator class. This class contains the logic for performing the custom validation. The ProductValidator class is contained in Listing 4.

Listing 4 – Validators\ProductValidator.cs

using System;
using System.Web.Mvc;
using Microsoft.Web.Mvc;
using MvcValidation;

namespace MvcValidationWebsite.Validators
{
    public class ProductValidator : ICustomValidator
    {
        #region ICustomValidator Members

        public void Validate(ModelStateDictionary modelState, FormCollection form)
        {
            // Don't bother with custom validation when attribute validation failed
            if (modelState.IsValid)
            {
                string strSaleStartDate = form["saleStartDate"];
                string strSaleEndDate = form["saleEndDate"];

                // Verify that either both or neither saleStartDate and saleEndDate have values
                if (!String.IsNullOrEmpty(strSaleStartDate) && String.IsNullOrEmpty(strSaleEndDate))
                    modelState.AddModelError("saleEndDate", strSaleEndDate, "sale end date must have a value when sale start date has a value.");
                if (String.IsNullOrEmpty(strSaleStartDate) && !String.IsNullOrEmpty(strSaleEndDate))
                    modelState.AddModelError("saleStartDate", strSaleStartDate, "sale start date must have a value when sale end date has a value.");

                // Verify that saleEndDate > saleStartDate
                if (!String.IsNullOrEmpty(strSaleStartDate) && !String.IsNullOrEmpty(strSaleEndDate))
                {
                    DateTime saleStartDate = DateTime.Parse(strSaleStartDate);
                    DateTime saleEndDate = DateTime.Parse(strSaleEndDate);
                    if (saleEndDate <= saleStartDate)
                    {
                        modelState.AddModelError("saleStartDate", strSaleStartDate, "sale start date must be before end date.");
                        modelState.AddModelError("saleEndDate", strSaleEndDate, "sale start date must be before end date.");
                    }
                }
            }

        }

        #endregion
    }
}

Notice that the ProductValidator implements the ICustomValidator interface. This interface has one required method that you must implement named Validate().

The ProductValidator class in Listing 4 first checks that when a date is supplied for either StartSaleDate or EndSaleDate, a date is supplied for both. It wouldn’t make much sense to specify a start date for a sale without specifying an end date.

Next, the ProductValidator confirms that the EndSaleDate is greater than the StartSaleDate. If there is an error, a new error message is added to ModelState to represent the error. Just as long as the view includes an HTML.ValidationMessage() call that includes the validation error key, the validation error message will be displayed.

You can take advantage of a custom validator to perform any complicated validation task. For example, if you need to perform a database lookup to ensure a unique value then you can perform the database lookup in the custom validator class.

Testing the Validation Framework

Because the validation framework is used in the Forums application, it needs unit tests just like any other part of the Forums application. At the end of this blog entry, you can download the Visual Studio solution that includes the validators. This solution also includes a test project for the validators. The test project includes 33 tests that verify how the validators work with different form field values (see Figure 2).

Figure 2 – Unit tests for the validators

clip_image004

For example, the test class in Listing 5 contains all of the unit tests for the LengthValidator. The LengthValidator is tested with an empty string, a string over the maximum length, a string equal to the maximum length, and a string less than the maximum length.

Listing 5 – LengthValidatorTests.cs

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcValidation;

namespace MvcValidationTests
{
    [TestClass]
    public class LengthValidatorTests
    {
        [TestMethod]
        public void LengthValidatorEmptyIsValid()
        {
            // Arrange
            var validator = new LengthValidatorAttribute(2);

            // Act
            var result = validator.Validate(String.Empty);

            // Assert
            Assert.IsTrue(result);
        }

        [TestMethod]
        public void LengthValidatorOverMaximum()
        {
            // Arrange
            var validator = new LengthValidatorAttribute(2);

            // Act
            var result = validator.Validate("abc");

            // Assert
            Assert.IsFalse(result);

        }

        [TestMethod]
        public void LengthValidatorEqualMaximum()
        {
            // Arrange
            var validator = new LengthValidatorAttribute(2);

            // Act
            var result = validator.Validate("ab");

            // Assert
            Assert.IsTrue(result);

        }

        [TestMethod]
        public void LengthValidatorUnderMaximum()
        {
            // Arrange
            var validator = new LengthValidatorAttribute(2);

            // Act
            var result = validator.Validate("a");

            // Assert
            Assert.IsTrue(result);

        }
    
    }
}

A Moment of Reflection

It might seem strange that we expended so much effort to avoid putting LINQ to SQL attributes on our Model classes and now we are placing validator attributes on the very same classes. Why is it okay to add validator attributes but not okay to add LINQ to SQL attributes?

There are two important considerations here. First, there is a likelihood that we will need to switch data access technologies in the future. For example, I might need to swap out LINQ to SQL for the Microsoft Entity Framework or even NHibernate. I don’t want to bake a particular data access technology into our Model classes when I might need to change the data access technology in the future.

It is unlikely, however, that I will want to change how I handle form validation. I don’t anticipate changing form validation frameworks in the future. Therefore, adding validator attributes to the Model classes is not locking us into anything that I expect to change.

There are two (closely related) software design principles that we must consider here: the Single Responsibility Principle and the Encapsulate What Varies Principle. Both principles warn us that when we expect software to change, we should do everything we can to isolate the software that might change from the rest of our code.

If we prefer, we can isolate the validator attributes from the rest of our code. We could divide the Product class into two classes: the Product class and the ProductValidationModel class. We don’t need to touch the Product class to add validation to the Product class. We could add all of the validators to a duplicate ProductValidationModel class. When we call the Validation.Validate() method, we can pass the ProductValidationModel class instead of the Product class to this method.

I’m not going to divide the Product class into two classes for the Forums application because this smells like Needless Complexity. I really don’t expect to change validation frameworks, so the extra effort is not worth it.

Adding Form Validation to the Forums Application

Now that we have a simple validation framework, we can modify the Forums application to take advantage of it. To begin with, we want to make sure that a user cannot post a new message to the forum without entering a message subject and a message body (see Figure 3).

Figure 3 – Validating a forum post

clip_image006

Therefore, we need to create unit tests that express this intention. The two new unit tests are contained in Listing 6. The first unit test verifies that validation fails when the subject is set to an empty value. The second unit test verifies that validation fails when the body does not have a value.

Listing 6 – Controllers\ForumControllerTest.cs

[TestMethod]
public void EmptySubjectFailsValidation()
{
    // Arrange
    var controller = new ForumController(_repository);

    // Act
    var form = new FormCollection();
    form.Add("author", "Stephen");
    form.Add("subject", String.Empty);
    form.Add("body", "Body of new thread");
    var result = (ViewResult)controller.Create(form);

    // Assert
    Assert.IsFalse(result.ViewData.ModelState.IsValid);  
}


[TestMethod]
public void EmptyBodyFailsValidation()
{
    // Arrange
    var controller = new ForumController(_repository);

    // Act
    var form = new FormCollection();
    form.Add("author", "Stephen");
    form.Add("subject", "New Message");
    form.Add("body", String.Empty);
    var result = (ViewResult)controller.Create(form);

    // Assert
    Assert.IsFalse(result.ViewData.ModelState.IsValid);
}

In order to pass these new unit tests, we need to modify our Forum controller. The new version of the Forum controller is contained in Listing 7.

Listing 7 – Controllers\ForumController.cs

using System;
using System.Web.Mvc;
using MvcForums.Models;
using Microsoft.Web.Mvc;
using MvcValidation;

namespace MvcForums.Controllers
{
    public class ForumController : Controller
    {
        private IForumRepository _repository;

        public ForumController()
            : this(new ForumRepository())
        { }

        public ForumController(IForumRepository repository)
        {
            _repository = repository;
        }

        public ActionResult Index()
        {
            ViewData.Model = _repository.SelectThreads();
            return View("Index");
        }

        [AcceptVerbs("GET")]
        public ActionResult Create()
        {
            return View("Create");
        }

        [AcceptVerbs("Post")]
        public ActionResult Create(FormCollection form)
        {
            // Validate
            Validation.Validate<Message>(ViewData.ModelState, form);
            if (!ViewData.ModelState.IsValid)
                return View("Create", form);

            // Create new message
            var messageToCreate = new Message();
            if (!String.IsNullOrEmpty(form["parentThreadId"]))
                messageToCreate.ParentThreadId = int.Parse(form["parentThreadId"]);
            if (!String.IsNullOrEmpty(form["parentMessageId"]))
                messageToCreate.ParentMessageId =  int.Parse(form["parentMessageId"]);
            messageToCreate.Author = form["author"];
            messageToCreate.Subject = form["subject"];
            messageToCreate.Body = form["body"];
            messageToCreate.EntryDate = DateTime.Now;

            // Add to database
            _repository.AddMessage(messageToCreate);

            // Redirect
            return RedirectToAction("Index");
        }

        public ActionResult Thread(int threadId)
        {
            ViewData.Model = _repository.SelectMessages(threadId);
            return View("Thread");
        }

    }
}

Notice that the Forum controller now has two Create() methods. The first Create() method displays the form for creating a new forum message. The second Create() method is called when the form data is posted to the server.

This second Create() method takes advantage of the Validation class to validate the form data. If validation fails, the Create view is redisplayed.

The final modification that we need to make is to the Message class. The modified Message class is contained in Listing 8.

Listing 8 – Models\Message.cs

using System;
using MvcValidation;

namespace MvcForums.Models
{
    public class Message
    {
        public Message()
        { }

        public Message(int id, int? parentThreadId, int? parentMessageId, string author, string subject, string body)
        {
            this.Id = id;
            this.ParentThreadId = parentThreadId;
            this.ParentMessageId = parentMessageId;
            this.Author = author;
            this.Subject = subject;
            this.Body = body;
        }

        public int Id { get; set; }
        public int? ParentThreadId { get; set; }
        public int? ParentMessageId { get; set; }
        public string Author { get; set; }
        
        [RequiredValidator("You must enter a message subject.")]
        public string Subject { get; set; }
        
        [RequiredValidator("You must enter a message body.")]
        public string Body { get; set; }
        
        public DateTime EntryDate { get; set; }
    }
}

Two RequiredValidator attributes have been added to the Message class in Listing 8. No other changes have been made to the class.

After all of these modifications have been made to the Forums application, the new validation unit tests pass (see Figure 4). We have successfully added validation to the Forums application.

Figure 4 – Forums validation tests pass

clip_image008

Summary

In this blog entry, we’ve added support for basic form validation to the Forums application. We created a set of validation attributes that we can apply to our model classes. In this blog entry, we implemented server-side validation. In the next blog entry, I want to tackle the problem of client-side validation.

Download the Code

12 Comments

  • Great!
    Love stephenwalther

  • Good to see the validation libraries updated for Preview 5.

    But I'm also very keen to see samples using the standard System.ComponentModel.DataAnnotations attributes. Then using these attributes within a ModelBinder, which validates and adds the messages to the ModelState.

  • Thanks,Mr Walther
    I am so confused that why don't you use the ModelBinder attribute?

  • Hi Stephen,

    Just wondering why you have chose to couple validation of the model to the UI that is interacting with it? It seems to me that if I wish to re-use the model in a console or winforms app then the other UI's would also need to know about FormCollection and ModelStateDictionary to validate the model. I'm only bringing this up because of your first post about building the best app possible.

    For most people with only one UI this won't be a problem, and I also appreciate that this is a blog post not a chapter of a book so just ignore this comment if you want.

    Love the posts, keep up the good work

  • 'c:\mvc framework\appforums4\cs\vmkforums\vmkforums.tests\bin\debug\mvcforums.tests.dll.config' is not trusted.

  • Why would you do any validation directly against the form collection like that ProductValidator? I'd argue that ProductValidator is a code smell. Why not just put all the validation in Product itself? Surely the validation model you're using supports some kind of hook method on the entity itself for complex validations the way that Rails does.

    Unit testing the validation would be a lot easier if you were doing all the testing against the model class itself. Setting up the data in the form dictionary is tedious and error prone compared to just using an object initializer.

  • @Jeremy -- Thanks for the feedback! Here is my reasoning. I want to make sure that I can easily test the form validation in scenarios in which the wrong type of data is being assigned to a property. For example, I want to be able to test the scenario in which a user enters the string "apple" for a Product.Price property that requires a Decimal value.

    If I passed an instance of the Product class to an action method, then there would be no way to test what happens when an invalid value is assigned to the Product.Price property. Because I can't assign the wrong type of data to a property when working with a strongly typed object, I need to work with an untyped form collection instead of a typed Product class.

  • @Andy - Good point about using the System.ComponentModel.DataAnnotations attributes -- no reason to reinvent a set of perfectly good validation attributes. I want to see exactly what I need to do in order to add client-side validation support. I might end up using the DataAnnotations attributes.

  • @freechoice -- You could move the call to Validation.Validate() from the controller action to a custom Model Binder. The problem concerns unit testing. I want to make sure that I can pass invalid values to the action within a unit test and see what happens. Therefore, I decided to use the untyped FormCollection instead of a strongly typed object.

  • @PaulBlamire -- Great feedback! I have mixed feelings about what you are saying. I think someone could argue that the FormCollection is just a slightly specialized Dictionary collection. In that case, using the FormCollection does not couple you to a particular type of UI.

  • But using FormCollection does require you to take a dependency on its assembly. If you accepted an IDictionary instead you wouldn't have this dependency.

  • Asp net mvc application building forums 4 server side form validation.. I like it :)

Comments have been disabled for this content.