ASP.NET MVC Application Building: Forums #4 1/2– Validation Revisited

I received a lot of feedback on my last blog entry on implementing server-side form validation for the MVC Forums application. Thank you everyone for the feedback!

In my previous blog entry, I described how you can perform validation in an MVC application by taking advantage of custom validation attributes. By decorating your model with attributes, such as the RequiredValidator and RegularExpressionValidator attributes, you can enforce validation rules and display custom validation error messages.

In order to perform more complex validation, I suggested using a CustomValidator that could be applied to the entity class. The CustomValidator class points to another class that executes custom validation rules.

One piece of feedback that I consistently received was that this approach for performing custom validation was too complex for most scenarios. Most people liked the attribute approach to validation, but also wanted an easy way to do custom validation.

Therefore, in this blog entry, I take a slightly different approach to handling the problem of form validation. In this blog entry, I combine the approach described by Scott Guthrie with an attribute approach. In other words, I mix an imperative and declarative approach to validation. I call this approach a hybrid approach to form validation.

In this blog entry, I describe the hybrid approach. Before we get into the details, I should mention that I just came across an excellent blog entry by Steve Sanderson that describes almost exactly the same approach to validation described in this blog entry (however -- his blog entry has much nicer diagrams than mine). You can view his post here:

http://blog.codeville.net/2008/09/08/thoughts-on-validation-in-aspnet-mvc-applications/

An Imperative Approach to Form Validation

Let’s start with the approach described by Scott Guthrie. When following this approach to form validation, you must create the following classes:

· IRuleEntity – an interface that each entity must implement in order to be validated.

· RuleViolation – a class that represents a validation error.

· RuleViolationException – an exception that is raised when an entity fails validation.

· Validation – a class that includes a method for copying rule violations to ModelState.

The IRuleEntity interface is really simple. It consists of two methods named Validate() and GetRuleViolations(). This interface is contained in Listing 1.

Listing 1 – IRuleEntity.cs

using System.Collections.Generic;

namespace MvcValidation
{
    public interface IRuleEntity
    {
        List<RuleViolation> GetRuleViolations();
        void Validate();
    }
}

The RuleViolation class represents a particular validation error. A collection of RuleViolations is used to represent all of the validation errors that result when submitting a form. The RuleViolation class is contained in Listing 2.

Listing 2 – RuleViolation.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MvcValidation
{
    public class RuleViolation
    {
        public string PropertyName { get; private set; }

        public object PropertyValue { get; private set; }

        public string ErrorMessage { get; private set; }

        public RuleViolation(string propertyName, object propertyValue, string errorMessage)
        {
            this.PropertyName = propertyName;
            this.PropertyValue = propertyValue;
            this.ErrorMessage = errorMessage;
        }

    }
}

When there is at least 1 rule violation, a RuleViolationException is raised. This special exception class is contained in Listing 3.

Listing 3 – RuleViolationException.cs

using System;
using System.Collections.Generic;

namespace MvcValidation
{
    public class RuleViolationException : Exception
    {
        public RuleViolationException(string message, List<RuleViolation> validationIssues)
            : base(message)
        {
            this.ValidationIssues = validationIssues;
        }

        public List<RuleViolation> ValidationIssues { get; private set; }
    }
}

Finally, the Validation class is a utility class that exposes one method named UpdateModelStateWithViolations(). This method copies all of the rule violations to a controller class’s ModelState. The Validation class is contained in Listing 4.

Listing 4 – Validation.cs

using System.Web.Mvc;

namespace MvcValidation
{
    public class Validation
    {
        public static void UpdateModelStateWithViolations(RuleViolationException ruleViolationException, ModelStateDictionary modelState)
        {
            foreach (var issue in ruleViolationException.ValidationIssues)
            {
                var value = issue.PropertyValue ?? string.Empty;
                modelState.AddModelError(issue.PropertyName, value.ToString(), issue.ErrorMessage);
            }
        }


    }
}

Let’s take a look at how all of these classes work together. The Message class in Listing 5 represents a Forum message. Notice that this class implements the IRuleEntity interface by implementing both the Validate() and the GetRuleViolations() methods.

Listing 5 – Message.cs

using System;
using MvcValidation;
using System.Collections.Generic;
using System.Data.Linq;
using MvcValidation.Attributes;
using System.Web.Mvc;

namespace MvcForums.Models.Entities
{
    public class Message :IRuleEntity
    {
        private DateTime _entryDate = DateTime.Now;

        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; }
        public string Subject { get; set; }
        public string Body { get; set; }

        
        public DateTime EntryDate 
        {
            get { return _entryDate; }
            set { _entryDate = value; }  
        }

        #region IRuleEntity Members

        public List<RuleViolation> GetRuleViolations()
        {
            var validationIssues = new List<RuleViolation>();

            // Validate Subject
            if (String.IsNullOrEmpty(this.Subject))
                validationIssues.Add(new RuleViolation("subject", this.Subject, "You must enter a message subject."));

            // Validate Body
            if (String.IsNullOrEmpty(this.Body))
                validationIssues.Add(new RuleViolation("body", this.Body, "You must enter a message body.")); 

            return validationIssues;
        }

        public void Validate()
        {
            var validationIssues = GetRuleViolations();
            if (validationIssues.Count > 0)
                throw new RuleViolationException("Validation Issues", validationIssues);
        }

        #endregion

    }
}

The Validate() method calls the GetRuleViolations() method. If there are any validation rule violations then the Validate() method throws a RuleViolationException.

In the Forums application, the Validate() method is called within the ForumRepository. Before a new Message is added to the database, the Validate() method is called on the Message class like this:

public Message AddMessage(Message messageToAdd)

{

  messageToAdd.Validate();

  _dataContext.Insert(messageToAdd);

  return messageToAdd;

}

If there is a validation error then a RuleViolationException is thrown and the message never gets inserted into the database.

The ForumRepository is used within the Forum controller. When you submit a form for creating a new forum message, the ForumController.Create(FormCollection collecction) method is called. The Forum controller is contained in Listing 6.

Listing 6 – ForumController.cs

using System;
using System.Web.Mvc;
using MvcForums.Models;
using Microsoft.Web.Mvc;
using MvcForums.Models.Entities;
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)
        {
            var messageToCreate = new Message();

            try
            {
                UpdateModel(messageToCreate, new[] { "Author", "ParentThreadId", "ParentMessageId", "Subject", "Body" });
                _repository.AddMessage(messageToCreate);
            }
            catch (RuleViolationException rex)
            {
                Validation.UpdateModelStateWithViolations(rex, ViewData.ModelState);
                return View("Create", messageToCreate);
            }
            catch (Exception ex)
            {
                ViewData.ModelState.AddModelError("message", null, "Could not save message.");
                return View("Create", messageToCreate);
            }

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

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

    }
}

Notice that there are two Create() methods. The first Create() method is invoked only for an HTTP GET operation. This action returns the form for creating a new Message (see Figure 1).

Figure 1 – Form for adding a Forums message

clip_image002

The second Create() method is invoked when an XHTML form is posted to the server. When the form data is posted, the Create() method calls the Controller.UpdateModel() method to generate a Product class that has all of the values submitted in the form. Next, the ForumRepository.Add() method is called to add the new instance of the Product class to the database.

Both statements are wrapped in a Try..Catch block. If either the Controller.UpdateModel() or the ForumRepository.Add() methods fail then the Catch clause of the Try…Catch statement is executed. The first Catch clause updates the ViewData.ModelState property with information about the validation errors by copying the validation errors from the RuleViolationException to ModelState. Next, the Create view is redisplayed.

Notice that there are two Catch clauses. The second Catch clause captures a generic Exception. This Catch clause is invoked when the UpdateModel() method encounters a validation issue (the wrong type of value is being assigned to a property). This clause will be invoked when there are network problems communicating with your database server or your database server crashes. Notice that a error key named message is updated.

The Create view is contained in Listing 7.

Listing 7 -- Create.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Create.aspx.cs" Inherits="MvcForums.Views.Forum.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>Index</title>
    <style type="text/css">
    
    .input-validation-error
    {
        border: solid 2px red;
    }
    
    #message
    {
        padding: 5px;
        color:red;   
    }
    
    </style>
</head>
<body>
    <div>
    
    <form method="post" action="/Forum/Create">
    
    <div id="message">
    <%= Html.ValidationMessage("message") %>
    </div>
    
    <input name="author" type="hidden" value="Stephen" />
    
    <label for="subject">Subject:</label>
    <%= Html.TextBox("subject") %>
    <%= Html.ValidationMessage("subject") %>
    
    <br /><br />
    <label for="body">Body:</label>
    <%= Html.TextArea("body") %>
    <%= Html.ValidationMessage("body") %>

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

 

Notice that the view includes a generic error message (named message).

 

Figure 2 -- Generic error message

image

 

This approach to performing validation is an imperative approach. The validation rules are expressed by imperative code in the GetRuleViolations() method like this:

public List<RuleViolation> GetRuleViolations()
{
    var validationIssues = new List<RuleViolation>();

    // Validate Subject
    if (String.IsNullOrEmpty(this.Subject))
        validationIssues.Add(new RuleViolation("subject", this.Subject, "You must enter a message subject."));

    // Validate Body
    if (String.IsNullOrEmpty(this.Body))
        validationIssues.Add(new RuleViolation("body", this.Body, "You must enter a message body.")); 

    return validationIssues;
}

Using an imperative approach you can perform any type of validation that you need. For example, you can perform a database lookup to make sure that the Message subject and body are unique in the database before inserting the Message into the database.

A Declarative Approach to Form Validation

I am a lazy person. I like a declarative approach to performing form validation. A declarative approach enables you to validate form data without writing any code.

Another advantage of a declarative approach is that a declarative approach enables you to easily implement client-side validation. We want to generate client-side validators from our server-side validators. That way, we can validate a form without posting the form back to the server.

The modified Message class in Listing 8 uses a declarative approach to validation.

Listing 8 – Message.cs (with attributes)

using System;
using MvcValidation;
using System.Collections.Generic;
using System.Data.Linq;
using MvcValidation.Attributes;
using System.Web.Mvc;

namespace MvcForums.Models.Entities
{
    public class Message :IRuleEntity
    {
        private DateTime _entryDate = DateTime.Now;

        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 { return _entryDate; }
            set { _entryDate = value; }  
        }

        #region IRuleEntity Members

        public List<RuleViolation> GetRuleViolations()
        {
            var validationIssues = new List<RuleViolation>();

        // Validate attributes
            AttributeValidation.Validate(this, validationIssues);

            return validationIssues;
        }

        public void Validate()
        {
            var validationIssues = GetRuleViolations();
            if (validationIssues.Count > 0)
                throw new RuleViolationException("Validation Issues", validationIssues);
        }

        #endregion

    }
}

Notice that both the Subject and Body properties in the modified Message class are decorated with the RequiredValidator attribute. The RequiredValidator attribute marks both of these properties as required.

Notice, furthermore, that the imperative code has been removed from the GetRuleViolations() method. Instead, the AttributeValidation.Validate() method is called. This method calls the Validate() method on each validator attribute applied to the Message class. If at least one validator fails then the Message class fails validation and a RuleViolationException is thrown.

Clearly, you can mix both an attribute and non-attribute approach to validation. For common validation tasks, you can use standard validators such as the RequiredValidator , LengthValidator, or RegularExpressionValidator. For more complicated types of validation, you can write imperative code to perform the validation.

Summary

In this blog entry, I’ve described an approach to validation that combines imperative and declarative validation. I believe that the easiest approach for common validation tasks is a declarative approach. For more complex or specialized validation tasks, write imperative validation code. In the case of the Forums application, my plan is to take a hybrid approach to validation.

34 Comments

Comments have been disabled for this content.