Entity Framework: Creating Custom Validation Attributes, Part 2

September 26th, 2016

We created a server-side validation attribute before, but how do we validate it on the client-side? In today's post, I answer a reader's request on how to add JavaScript validation to our DateTime comparer.

When we made our DateCompareAttribute for an ASP.NET MVC application, it was strictly built for a server-side validation.

Recently, I received a simple comment from that post asking,

What about client-side?

Yes, what about client-side indeed?

For our client-side application to validate properly, we need to add client-side JavaScript.

And that's what today's post covers: How to apply client-side validation to your custom server-side validation attribute.

Implementing IClientValidatable

The good news is we don't have to remove or refactor anything. We only need to add more to our existing DateCompareAttribute.

After digging around the code, I found the IClientValidatable which implements the GetClientValidationRules on a custom attribute.

This method attaches simple attributes to work in conjunction with the jQuery validation library which gives us our client-side validation.

While it's not automatic (yet), these hints or attributes are what trigger the validation on the client.

So let's continue with our DateCompare validator (changes in bold).

CustomAttributes/DateCompareAttribute.cs

public class DateCompareAttribute : ValidationAttribute, IClientValidatable
{
    private readonly string _propertyName;
    private readonly DateTimeComparer _comparerType;

    public DateCompareAttribute(string propertyName, DateTimeComparer comparerType)     {         _propertyName = propertyName;         _comparerType = comparerType;     }
    protected override ValidationResult IsValid(object firstValue, ValidationContext validationContext)     {         var propertyInfo = validationContext.ObjectType.GetProperty(_propertyName);         if (propertyInfo != null)             return new ValidationResult(String.Format("Property {0} does not exist.", _propertyName));
        var propertyValue = propertyInfo.GetValue(validationContext.ObjectInstance, null);
        switch (_comparerType)         {             case DateTimeComparer.IsEqualTo:                 if ((DateTime) propertyValue == (DateTime) firstValue)                 {                     return ValidationResult.Success;                 }                 break;             case DateTimeComparer.IsGreaterThan:                 if ((DateTime) propertyValue > (DateTime) firstValue)                 {                     return ValidationResult.Success;                 }                 break;             case DateTimeComparer.IsGreaterThanOrEqualTo:                 if ((DateTime) propertyValue >= (DateTime) firstValue)                 {                     return ValidationResult.Success;                 }                 break;             case DateTimeComparer.IsLessThan:                 if ((DateTime) propertyValue < (DateTime) firstValue)                 {                     return ValidationResult.Success;                 }                 break;             case DateTimeComparer.IsLessThanOrEqualTo:                 if ((DateTime) propertyValue <= (DateTime) firstValue)                 {                     return ValidationResult.Success;                 }                 break;         }         return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));     }
    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)     {         return new[] { new ModelClientValidationDateCompareRule(ErrorMessage, _propertyName, _comparerType) };     } }

Now that we have extended our DateCompare attribute, we need to add our custom rules as attributes for our jQuery validation to function properly.

The ModelClientValidationDateCompareRule class was modeled after what was already there with existing validation attributes.

CustomAttributes/ModelClientValidationDateCompareRule.cs

public class ModelClientValidationDateCompareRule : ModelClientValidationRule
{
    public ModelClientValidationDateCompareRule(string errorMessage, string property, DateTimeComparer comparer)
    {
        ErrorMessage = errorMessage;
        ValidationType = "datecompare";
        ValidationParameters["dateclass"] = property;

        string comparerValue = String.Empty;         switch (comparer)         {             case DateTimeComparer.IsEqualTo:                 comparerValue = "eq";                 break;             case DateTimeComparer.IsGreaterThan:                 comparerValue = "gt";                 break;             case DateTimeComparer.IsGreaterThanOrEqualTo:                 comparerValue = "gtoe";                 break;             case DateTimeComparer.IsLessThan:                 comparerValue = "lt";                 break;             case DateTimeComparer.IsLessThanOrEqualTo:                 comparerValue = "ltoe";                 break;         }
        ValidationParameters["comparer"] = comparerValue;     } }

If we run our web app, here's the rendered Expiration textbox:

<form action="/Home/Index" method="post">
    <div class="row">
        <label for="EffectiveDate">EffectiveDate</label>
        <input data-val="true" data-val-date="The field EffectiveDate must be a date." 
               data-val-required="The EffectiveDate field is required." id="EffectiveDate" 
               name="EffectiveDate" type="text" value="1/1/0001 12:00:00 AM">
    </div>
    <div class="row">
        <label for="ExpirationDate">ExpirationDate</label>
        <input data-val="true" data-val-date="The field ExpirationDate must be a date." 
               data-val-datecompare="Expiration must be greater than Effective Date" 
               data-val-datecompare-comparer="gt" 
               data-val-datecompare-dateclass="EffectiveDate" 
               data-val-required="The ExpirationDate field is required." id="ExpirationDate" 
               name="ExpirationDate" type="text" value="1/1/0001 12:00:00 AM">
    </div>
    <div class="row">
        <input type="submit" value="Submit">
    </div>
</form>

Now that we have our server-side validation attribute ready, we can focus on the client-side now.

jQuery Validation

On the client-side, we need our jQuery library along with the validation library and unobtrusive validation library.

<script src="/Scripts/jquery-3.1.0.min.js"></script>
<script src="/Scripts/jquery.validate.min.js"></script>
<script src="/Scripts/jquery.validate.unobtrusive.min.js"></script>

Once we have the scripts added to our page, there is a two-phase process on writing our own client-side validator.

  1. Write the validator for the jQuery Validate
  2. Write the adapter that turns the HTML attributes into metadata for jQuery.

jQuery Validation Adapter

First, our adapter.

Our validator can use one of four adapters. Each one of these creates an adapter, but each has specific functionality.

  1. add - Specific for your own number of additional parameters.
  2. addBool - Specific for a validation rule that can be "on" or "off.
  3. addSingleVal - Used to retrieve a single parameter value.
  4. addMinMax - Uses two validation rules: one used to check for a minimum value and one to check for the maximum value.

For our purposes, we'll use the add adapter.

Based on what I've been reading about the validation library, DO NOT place your validation code inside of a document.ready or $(function(){}); block.

Our script starts out like this:

if ($.validator && $.validator.unobtrusive) {

    $.validator.unobtrusive.adapters.add("datecompare",         ["dateclass", "comparer"],         function(options) {             var params = {                 dateclass: "#" + options.params.dateclass,                 comparer: options.params.comparer             };             options.rules['datecompare'] = params;
            if (options.message) {                 options.messages["datecompare"] = options.message;             }         }     ); }

As mentioned, we are using the adapters.add method.

Once setup, we can move on to the actual validation method itself.

$.validator.addMethod("datecompare",
    function(value, element, paramList) {

        var returnValue = false;
        var dateclass = paramList.dateclass;         var comparer = paramList.comparer;
        if (value) {             try {                 // Split the other date making it a datetime                  var otherDate = Date.parse($(dateclass).val());
                var thisDate = Date.parse(value);
                if (comparer === "eq") {                     returnValue = (thisDate === otherDate);                 } else if (comparer === "gt") {                     returnValue = (thisDate > otherDate);                 } else if (comparer === "gtoe") {                     returnValue = (thisDate >= otherDate);                 } else if (comparer === "lt") {                     returnValue = (thisDate < otherDate);                 } else if (comparer === "ltoe") {                     returnValue = (thisDate <= otherDate);                 }             } catch (err) {                 returnValue = false;             }         }
        return returnValue;     });

This is the easy part.

The signature of the addMethod is simple.

Once we have our validator built, we can convert the string to dates.

Finally, based on the comparer type, we return whether it was validated or not. 

So our final script looks like this:

if ($.validator && $.validator.unobtrusive) {
 
    $.validator.unobtrusive.adapters.add("datecompare",
        ["dateclass", "comparer"],
        function(options) {
            var params = {
                dateclass: "#" + options.params.dateclass,
                comparer: options.params.comparer
            };
            options.rules['datecompare'] = params;
 
            if (options.message) {
                options.messages["datecompare"] = options.message;
            }
        }
    );
 
    $.validator.addMethod("datecompare",
        function(value, element, paramList) {
 
            var returnValue = false;
 
            var dateclass = paramList.dateclass;
            var comparer = paramList.comparer;
 
            if (value) {
                try {
                    // Split the other date making it a datetime 
                    var otherDate = Date.parse($(dateclass).val());
 
                    // Split the this value into a datetime
                    var thisDate = Date.parse(value);
 
                    if (comparer === "eq") {
                        returnValue = (thisDate === otherDate);
                    } else if (comparer === "gt") {
                        returnValue = (thisDate > otherDate);
                    } else if (comparer === "gtoe") {
                        returnValue = (thisDate >= otherDate);
                    } else if (comparer === "lt") {
                        returnValue = (thisDate < otherDate);
                    } else if (comparer === "ltoe") {
                        returnValue = (thisDate <= otherDate);
                    }
                } catch (err) {
                    returnValue = false;
                }
            }
 
            return returnValue;
        });
}

Final Notes

This wasn't the most straight-forward implementation, but here are some cliff notes to make your custom validation go a little smoother.

Conclusion

In today's post, we applied client-side validation to our already-built server-side DateComparer.

While it seems like a lot to code, it makes the visitor's user experience ten times better because immediate validation appears without a "postback."

Was there something I missed? Please post your comments (and complaints) below.