Entity Framework: Creating Custom Validation Attributes, Part 2

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.

Written by Jonathan "JD" Danylko • Last Updated: • MVC •
Hand in the middle of the sky

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.

  • First Parameter - Name of the ValidationType. We called ours "datecompare".
  • Second Parameter - A parameter list of parameters. :-) Examine your ModelClientValidationDateCompareRule for the names of the parameters we used.
  • Third Parameter - A function that passes options to the validator. For example, we prepared the parameters and the error message in our function.

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.

  • First Parameter - Again, the name of the Validation Type (i.e. "datecompare")
  • Second Parameter - A function that passes the value, element, and your parameters to this function. Of course, these are passed from the adapter add method or whatever addxxx adapter you used.
  • Third Parameter (optional) - The error message to display.

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.

  • For your ASP.NET MVC View (HTML page), confirm you have an @Html.ValidationMessageFor() underneath each of your input fields. This is where your validation message will appear. It'll be hard to see if you don't have one in your HTML.
  • In your Web.Config, confirm you have the following two lines:
      <appSettings>
        <add key="ClientValidationEnabled" value="true" />
        <add key="UnobtrusiveJavaScriptEnabled" value="true" />
      </appSettings>
    
  • Again, DO NOT place this code inside a DOM ready or jQuery.ready. It's not necessary.

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.

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus