Flexible Conditional Validation with ASP.NET MVC – adding client-side support


I’ve worked with a number of customers that wanted to be able to do cross-field validation of their models along the lines of “if property x is set then property y is required”. Some customers approached this using IValidatableObject, and others created attributes such as RequiredIfXSet. The downsides with IValidatableObject include the lack of client-side validation support and difficulties with re-use of the validation logic. With RequiredIfXSet, the challenge becomes the proliferation of RequiredIfZSet etc attributes. Often this leads people to a more general purpose approach, and I showed how to create a flexible RequiredIf attribute in Part 1.

This post will briefly look at the process and key points for adding client-side support. I’ll skip over a lot of the implementation details so that the post doesn’t end up too long, but the link to the code is at the bottom of the article.

To recap, the previous post gave us the RequiredIf attribute which can be used as shown below:

    public class PendingRequestModel
    {
         [HiddenInput(DisplayValue = false)]
         public int Id { get; set; }        

         [StringLength(20)]
         public string Title { get; set; }

         [DataType(DataType.Date)]
         public DateTime RequestedOn { get; set; }

         [DataType(DataType.Date)]
         public DateTime DueOn { get; set; }

         public bool IsComplete { get; set; }

         [DataType(DataType.MultilineText)]
         [RequiredIf("IsComplete", true, ErrorMessage = "Must add comments if the item is complete")]
        public string Comments { get; set; }
     }

In this example, the condition is simply “IsComplete”, but it can be more complicated if desired, such as “IsComplete || DueOn < DateTime.Now”.

 

Steps for adding client-side validation

To add client-side validation with ASP.NET MVC (3 onwards)

  • Create the client-side validator and register it with the client-side validation framework
  • Implement IClientValidatable on your attribute and serve up the meta-data about the validation to be passed to the client-side framework

We’ll tackle those two parts in reverse order…

Generating the validation meta-data

Normally the metadata is fairly obvious and simple. For the StringLength validator it would be the maximum string length, for RegEx it is the regular expression. In the case of RequiredIf, we have a C# expression which isn’t likely to be much use in the browser. The approach I took for this post was to convert the C# expression into a JavaScript expression that would form part of the meta-data.

In the original code the DynamicQuery NuGet package is used to parse the expression string. This gives an expression tree which is dynamically compiled each time into a delegate represents the condition (read: perf hit that needs optimising!). For the client-side support we will take the expression tree that gives us rich data about the expression and then convert that into a JavaScript expression.

The basic approach is to write an Expression visitor by deriving from System.Linq.Expressions.ExpressionVisitor, which provides the base functionality to walk the expression tree. We can then override the various methods on the base class such as VisitBinary to provide the implementation

                protected override Expression VisitBinary(BinaryExpression node)
        {
            string operatorString = null;
            switch (node.NodeType)
            {
                case ExpressionType.AndAlso:
                    operatorString = "&&";
                    break;
                case ExpressionType.OrElse:
                    operatorString = "||";
                    break;
                // code omitted
            }
            Visit(node.Left);
            _buf.Append(" ");
            _buf.Append(operatorString);
            _buf.Append(" ");
            Visit(node.Right);
            return node;
        }

With the JavaScriptExpressionVisitor we can convert simple C# expressions to JavaScript, and we can now bundle that JavaScript expression as part of the meta-data that the client-side validator consumes (in the IClientValidatable.GetClientValidationRules implementation on RequiredIfAttribute).

Creating and wiring up the client-side validator

In the downloadable code (see link at the bottom), the client-side code is in requiredIf.js. To use this you need to ensure that you have referenced jQuery, jQuery.validate, jQuery.validate.unobtrusive and requiredIf (you’ll also need jQuery-ui if your expressions contain date values as I reused the date parsing – feel free to replace in your implementation)

There are two key pieces in requiredIf.js: creating the validator and wiring it up to the unobtrusive framework.

Creating the validator involves registering it with jQuery.validate, and this is done in the call to $.validator.addMethod(). This function receives a set of parameters, and these are specified when wiring the validator up to the unbobtrusive framework. This is the code that pulls out the meta-data that we specified in the IClientValidatable.GetClientValidationRules implementation server-side. It lives in the call to $.validator.unobtrusive.adapters.add().

Show me the code

The sample code for this article is hosted on code.msdn.microsoft.com at http://code.msdn.microsoft.com/Flexible-Conditional-37ae638e

NOTE: The code is not fully featured or production-ready. There is only sufficient implementation of the JavaScriptExpressionVisitor for the purposes of the blog post, there are areas that are known to be a perf hit (and probably unknown areas too), and there has been no real testing. 🙂

Comments (9)

  1. Ergun Yasar says:

    This is exactly what im looking for. But it would be great if you can give us download link of the sample project 🙂 it doesnt appear at code.msdn.microsoft.com/Flexible-Conditional-37ae638e

  2. stuartle says:

    Hi Ergun,

    I've just re-uploaded the code to code.msdn.microsoft.com – if you try again you should be able to download the code 🙂

    – Stuart

  3. Zatos says:

    Was this meant to be able to work with two different objects having different RequiredIf conditions?  I am currently doing that, yet the second one always requires validation even when the condition is false.  I can't find anything wrong, so I thought it might have been a limitation of the code.

  4. Zatos says:

    Hmmm, strangely I discovered it wasn't working properly with boolean values.  I changed them to strings and it works perfectly!  Thanks for the work on this, it is extremely useful.

  5. Chandra says:

    This is awesome. However, how do I apply a condition in RequiredIf to check a textbox is not empty?

  6. Chandra says:

    Taking your example, how do I use RequiredIf on "Comments" field if "Title" is entered

  7. AntBoots says:

    Great bit of code just what I needed 🙂

    For the client side validation of a radio button I just added the following piece of code where 'actualValue' is being set:

    if (controlType === 'radio') {

               for (var i = 0; i < control.length; i++) {

                   if (control[i].checked) {

                       actualValue = control[i].value;

                   }

               }

           }

  8. ardith hutch says:

    i stiill don t have a password  can t reset one i don t remember  my old one and we keep going around in circiles

  9. matt says:

    I feel stupid as I don't know what Im suppose to do now. I have added the 2 class files and included the js.

    I have a tab that contains a few dropdowns that via jquery appears if a checkbox is checked. The only time the dropdown in the associated tab is required is when the checkbox is checked.

    The html does contain the attrubuyte values i added to the model, but I dont know what to do next.

    data-val="true" data-val-requiredif="Required when Analyst role is selected."

    data-val-requiredif-expression="gv('*.isAnalyst') == true"

    I was just going to write some jquery to loop through the checkboxes and see which ones are checked and examine the "requiredif" is set, but if im going to to that then what would i need your code for? I must be off the rails.

Skip to main content