次の方法で共有


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 https://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

  • Anonymous
    September 10, 2012
    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

  • Anonymous
    September 10, 2012
    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
  • Anonymous
    April 19, 2013
    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.

  • Anonymous
    April 19, 2013
    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.

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

  • Anonymous
    June 27, 2013
    Taking your example, how do I use RequiredIf on "Comments" field if "Title" is entered

  • Anonymous
    December 09, 2013
    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;                }            }        }

  • Anonymous
    December 31, 2013
    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

  • Anonymous
    November 23, 2015
    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.