Subscribe to this Blog

Subscribe to this Blog

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

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.

Published Tuesday, September 9, 2008 1:17 AM by swalther

Comments

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

Tuesday, September 9, 2008 5:13 AM by Andy

Yes, good decision to use the hybrid approach as it recognises that developers will be using the DataAnnotations attributes with Dynamic Data. A helper method AttributeValidation.Validate() would be great until the model is updated with more complex validation.

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

Tuesday, September 9, 2008 5:20 AM by SteveSanderson

Hey, looks good. A couple of suggestions:

[1] I don't think you actually need the IRuleEntity interface. This is one of the things I was trying to avoid in my approach to validation - model objects shouldn't have to advertise the fact that they do validation, they just do it. Looking at your code, I think you can remove IRuleEntity and it would just continue working. There's no need to define or implement this interface.

[2] I think there's a mistake in listing 6. You say "If either the Controller.UpdateModel() ... fail then the Catch clause of the Try…Catch statement is executed", but you're only catching a RuleViolationException, and UpdateModel() doesn't throw that type of exception. Or have I misunderstood what you're doing? This looks like the same issue with setter exceptions that I was writing about, and have asked the MVC team to fix at www.codeplex.com/.../View.aspx.

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

Tuesday, September 9, 2008 5:26 AM by Andreas Håkansson

I read Stephen Walter's article earlier today and liked what I saw. Great seeing even more discussion surrounding a similar approach!

One, tiny, thing I personally would alter is to make the UpdateModelStateWithViolations method an extension method to the ModelStateDictionairy (or atleast add an extension that wraps Validator.UpdateModelStateWithViolations) to make the code more fluent

public static void UpdateModelStateWithViolations(this ModelStateDictionary modelState, RuleViolationException ruleViolationException)

       {

           foreach (var issue in ruleViolationException.ValidationIssues)

           {

               var value =

                   issue.PropertyValue ?? string.Empty;

               modelState.AddModelError(issue.PropertyName, value.ToString(), issue.ErrorMessage);

           }

       }

Would enable you to call it

ViewData.ModelState.UpdateModelStateWithViolations(...) instead

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

Tuesday, September 9, 2008 5:28 AM by Andreas Håkansson

Erm, I did of course mean the blog of Steve Sanderson ;)

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

Tuesday, September 9, 2008 5:30 AM by Andreas Håkansson

Also about the IRuleEntity interface. Like Steven says, its not needed here. The only time you'd really need it is if you treated your entities in a polymorphic fashion and that's not needed in a lot of cases, especially not here.

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

Tuesday, September 9, 2008 5:59 AM by Andreas Håkansson

I belive my other post got lost. The blog i was refering to was of couse the one of Steve Sanderson. I also mentioned that the IRuleEntity isn't needed unless the entities are treated in a polymorphic fashion.

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

Tuesday, September 9, 2008 8:07 AM by awilinsk

What about type validation? if the user enters "one" for a price field instead of "1", how is this validated? Wouldn't it throw conversion exception or something?

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

Tuesday, September 9, 2008 11:38 AM by BringerOD

This will fit many more scenarios.  

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

Tuesday, September 9, 2008 12:18 PM by swalther

@SteveSanderson - Thanks for catching the mistake in the ForumController -- I fixed the code in the listing.

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

Tuesday, September 9, 2008 12:21 PM by swalther

@Andreas Håkansson -- the extension method on ModelState is a great idea! Very clean solution.

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

Wednesday, September 10, 2008 7:28 PM by chris_

I like this set-up, nice and clean. I can has code ?

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

Saturday, October 18, 2008 7:50 AM by ray247

Dear Stephen,

Just want to ask if AttributeValidation.Validate() you used in the last portion of the code from MvcValidation, the project found on Google Code?

Thanks,

Ray.

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

Thursday, February 9, 2012 8:51 AM by EBeyBDCKPxhjkpx

9mWFJL Interesting, but still I would like to know more about it. Liked the article:DD

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

Monday, March 5, 2012 12:33 AM by tinnitus causes

d5DtoW Very neat article post.Really looking forward to read more. Cool.

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

Monday, March 5, 2012 2:15 AM by iCatUvbOLv

ic4Tz8 Last a few years has been to Ibiza, so met a person there whose style of presentation is very similar to yours. But, unfortunately, that person is too far from the Internet!...

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

Tuesday, March 6, 2012 9:22 AM by mcLNqhJRtlZADadq

I do`t see a feedback or the other coordinates from the blog administration!...

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

Tuesday, March 6, 2012 9:28 AM by EFmwtVlFAFerwqFxzZ

See it for the first time!!...

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

Tuesday, March 6, 2012 9:34 AM by ykGeWOeMbcJuerf

Well, actually, a lot of what you write is not quite true !... well, okay, it does not matter:D

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

Tuesday, March 6, 2012 10:17 AM by emJFIrIXeYYiV

Uh, well, explain me a please, I am not quite in the subject, how can it be?!...

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

Tuesday, March 6, 2012 10:23 AM by IdemlzFhEK

I subscribed to RSS, but for some reason, the messages are written in the form of some hieroglyph (How  can it be corrected?!...

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

Tuesday, March 6, 2012 10:28 AM by RxkJlGnwQnp

Well, actually, a lot of what you write is not quite true !... well, okay, it does not matter:D

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

Tuesday, March 6, 2012 11:23 AM by kvYcuulAtAyeVPQOZNT

52. "The road will be overcome by that person, who goes." I wish you never stopped and be creative - forever..!

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

Tuesday, March 6, 2012 11:37 AM by sDBDaETWzIPuBZp

Good post! Found a lot of new and interesting! Will share the link with others:D

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

Tuesday, March 6, 2012 12:08 PM by PPnfGnYGlBZHLkJ

Extremely easy by words but in reality�, a lot of things don`t correspond. Not everything is so rosy..!

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

Tuesday, March 6, 2012 12:17 PM by bmRKDNhwlxa

Yeah, it is clear now !... From the very beginning I did not understand where was the connection with the title !!...

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

Tuesday, March 6, 2012 12:32 PM by NoqhMJDlCgCyQ

Of course, I understand a little about this post but  will try cope with it!!...

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

Tuesday, March 6, 2012 12:45 PM by MmdCYVrhrVYpmrmpYV

Thanks a lot! An extremely interesting comment!!...

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

Tuesday, March 6, 2012 1:15 PM by fxoKqfOxqCWThQedYxb

It's straight to the point! You could not tell in other words! :D

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

Tuesday, March 6, 2012 2:09 PM by OpJFjlBEpZoVSLSysyn

Cool:) I would say say it exploded my brain..!

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

Tuesday, March 6, 2012 2:44 PM by ekiWLEVDtitOpPwtsW

Are you interested in webmaster`s income?!...

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

Tuesday, March 6, 2012 3:00 PM by flumfvIqTIJdlinBiX

Honestly, not bad news!...

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

Tuesday, March 6, 2012 7:33 PM by FvQFMMRzbXqIn

It's pleasant sitting at work to distract from it�to relax and read the information written here:D

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

Wednesday, March 7, 2012 1:14 PM by mmOrbjLRkc

Pleased to read intelligent thoughts in Russian. I`ve been living in England for already 5 years!...

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

Friday, March 30, 2012 12:10 PM by bRwmQCqBxaNDKakZu

MwWDjQ Thanks so much for the blog.Really looking forward to read more. Awesome.