ASP.NET MVC 3: Integrating with the jQuery UI date picker and adding a jQuery validate date range validator

UPDATE: I've blogged about an more flexible way to wire up the editor template here.

This is post looks at working with dates in ASP.NET MVC 3. We will see how to integrate the jQuery UI date picker control automatically for model properties that are dates and then see an example of a custom validator that ensures that the specified date is in a specified range. Additionally, we will add client-side validation support and integrate with the date picker control to guide the user to pick a date in the valid range.

The demo project

For the purposes of this post I have created a Foo class (and a view model with an associated message):

    public class Foo
    {
        public string Name { get; set; }
        public DateTime Date { get; set; }
    }
 public class FooEditModel
    {
        public string Message { get; set; }
        public Foo Foo { get; set; }
    }

I’ve created the following minimal Edit action (omitting any save logic since we’re not interested in that aspect -  it just displays a “saved” message):

 [HttpGet]
public ActionResult Edit()
{
    FooEditModel model = new FooEditModel
    {
        Foo = new Foo {Name = "Stuart", 
 Date = new DateTime(2010, 12, 15)}
    };
    return View(model);
}
[HttpPost]
public ActionResult Edit(Foo foo)
{
    string message=null;
    if (ModelState.IsValid)
    {
        // would do validation & save here...
        message = "Saved " + DateTime.Now;
    }
    FooEditModel model = new FooEditModel
    {
        Foo = foo,
        Message = message
    };
    return View(model);
}

Finally, the view is implemented in razor using templated helpers

 @Model.Message
@using (Html.BeginForm())
{ 
    @Html.EditorFor(m => m.Foo)
    <input id="submit" name="submit" type="submit" value="Save" />
}

When the project is run, the output is as shown below:

image

In the screenshot above, notice how the Date property is displayed with both the date and time parts. The Foo class models an entity that has a name and associated date, but the .NET Framework doesn’t have a “date” type so we model it with a DateTime. The templated helpers see a DateTime and render both parts. Fortunately the templated helpers work on the model metadata and we can influence the metadata using a range of attributes. In this case we will apply the DataType attribute to add more information about the type of a property:

     public class Foo
    {
        public string Name { get; set; }
        [DataType(DataType.Date)]
        public DateTime Date { get; set; }
    }

With this attribute in place, the templated helpers know that we’re not interested in the time portion of the DateTime property and the output is updated:

image

Adding the date picker

So far so good, but there’s still no date picker! Since I’m lazy (and quite like consistency) I’d like the templated helpers to automatically wire up the date picker whenever it renders an editor for a date (i.e. marked with DataType.Date). Fortunately, the  ASP.NET MVC team enabled exactly this scenario when they created the templated helpers. If you’re not familiar with how the templated helpers attempt to resolve partial views then check out Brad Wilson’s series of posts. The short version is that if you add the DataType.Date annotation, the templated helpers will look for a partial view named “Date” under the EditorTemplates folder. To demonstrate, I created the following partial view as Views\Home\EditorTemplates\Date.cshtml (I’m using Razor, but the same applies for other view engines)

 @model DateTime
@Html.TextBox("", Model.ToString("dd/MM/yyyy")) 
** TODO Wire up the date picker! **

There are a couple of things to note (apart from the fact that I’m ignoring localisation and using a UK date format!). The first is that we’ve specified the model type as DateTime, so the Model property is typed as DateTime in the view. The second is that the first parameter we’re passing to the TextBox helper is an empty string. This is because the view has a context that is aware of what the field prefix is, so the textbox will be named “Foo.Date”. With this partial view in place the output now looks like:

image

We’re now ready to wire up the date picker! The first change is to Date.cshtml to add a date class to tag the textbox as a date:

 @model DateTime
@Html.TextBox("", Model.ToString("dd/MM/yyyy"), 
 new { @class = "date" })

With this in place we will create a script that looks for any textboxes with the date class set and adds the date picker behaviour. I’m going to use the jQuery UI  date picker as jQuery UI is now in the standard template for ASP.NET MVC 3. The first step is to add a script reference to jQuery-ui.js and to reference jQuery-ui.css (in Content/themes/base). I then created a script called EditorHookup.js with the following script:

 /// <reference path="jquery-1.4.4.js" />
/// <reference path="jquery-ui.js" />
$(document).ready(function () {
    $('.date').datepicker({dateFormat: "dd/mm/yy"});
});

Again, I’m simply hardcoding the date format to a UK date format. jQuery UI has some support for localisation, or you could manage the date format server-side and output it as an attribute that your client-side logic picks up (this is left as an exercise for the reader Winking smile ). Now we just need to reference the EditorHookup script and run the site to see the date picker in action:

image

 

Adding date range validation

Now that we have the date picker implemented, we will look at how to create a validator to ensure that the specified date is within a specified range. The following code shows how we will apply the validator to our model:

    public class Foo
    {
        public string Name { get; set; }
        [DataType(DataType.Date)]
         [ DateRange("2010/12/01", "2010/12/16"  )] 
        public DateTime Date { get; set; }
    }

(Note that we can’t pass a DateTime in as attribute arguments must be const)

The implementation of the attribute isn’t too bad. There’s some plumbing code for parsing the arguments etc and the full code is shown below:

 public class DateRangeAttribute : ValidationAttribute
{
    private const string DateFormat = "yyyy/MM/dd";
    private const string DefaultErrorMessage= 
 "'{0}' must be a date between {1:d} and {2:d}.";
 
    public DateTime MinDate { get; set; }
    public DateTime MaxDate { get; set; }
        
    public DateRangeAttribute(string minDate, string maxDate)
        : base(DefaultErrorMessage)
    {
        MinDate = ParseDate(minDate);
        MaxDate = ParseDate(maxDate);
    }
        
    public override bool IsValid(object value)
    {
        if (value == null || !(value is DateTime))
        {
            return true;
        }
        DateTime dateValue = (DateTime)value;
        return MinDate <= dateValue && dateValue <= MaxDate;
    }
    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentCulture, 
 ErrorMessageString,
            name, MinDate, MaxDate);
    }
 
    private static DateTime ParseDate(string dateValue)
    {
        return DateTime.ParseExact(dateValue, DateFormat, 
 CultureInfo.InvariantCulture);
    }
}

The core logic is inside the IsValid override and is largely self-explanatory. One point of note is that the validation isn’t applied if the value is missing as we can add a Required validator to enforce this. we now get a validation error if we attempt to pick a date outside of the specified range:

image

Adding date range validation – client-side support

As it stands, the validation only happens when the data is POSTed to the server. The next thing we’ll add is client-side support so that the date is also validated client-side before POSTing to the server. There are a few things that we need to add to enable this. Firstly there’s the client script itself and then there’s the server-side changes to wire it all up.

Creating the client-side date range validator

ASP.NET MVC 3 defaults to jQuery validate for client-side validation and uses unobtrusive javascript and we will follow this approach (for more info see my post on confirmation prompts and Brad Wilson’s post on unobtrusive validation). Server-side, the validation parameters are written to the rendered HTML as attributes on the form inputs. These attributes are then picked up by some client-side helpers that add the appropriate client-side validation. The RangeDateValidator.js script below contains both the custom validation plugin for jQuery and the plugin for the unobtrusive validation adapters (each section is called out by comments in the script.

 (function ($) {
    // The validator function
    $.validator.addMethod('rangeDate', 
 function (value, element, param) {
        if (!value) {
            return true; // not testing 'is required' here!
        }
        try {
            var dateValue = $.datepicker.parseDate("dd/mm/yy", value); 
        }
        catch (e) {
            return false;
        }
        return param.min <= dateValue && dateValue <= param.max;
    });
 
    // The adapter to support ASP.NET MVC unobtrusive validation
    $.validator.unobtrusive.adapters.add('rangedate', ['min', 'max'], 
 function (options) {
        var params = {
          min: $.datepicker.parseDate("yy/mm/dd", options.params.min),
          max: $.datepicker.parseDate("yy/mm/dd", options.params.max)
        };
 
        options.rules['rangeDate'] = params;
        if (options.message) {
            options.messages['rangeDate'] = options.message;
        }
    });
} (jQuery));

As metnioned previously, the script is hard-coded to use the dd/mm/yyyy format for user input to avoid adding any further complexity to the blog post. The min/max values are output in yyyy/mm/dd as a means of serialising the date from the server consistently.

The validator function retrieves arguments giving the value to validate, the element that it is from and the parameters. These parameters are set up by the unobtrusive adapter based on the attributes rendered by the server. The next step is to get the server to render these attributes. Returning to our DateRangeAttribute class, we can implement IClientValidatable (this interface is new in ASP.NET MVC 3 - see my earlier post for how this makes life easier). IClientValidatable has a single method that returns data describing the client-side validation (essentially the data to write out as attributes).

 public class DateRangeAttribute : ValidationAttribute, IClientValidatable
{
  // ... previous code omitted...    public IEnumerable<ModelClientValidationRule> GetClientValidationRules (ModelMetadata metadata, ControllerContext context)
    {
        return new[]
        {
            new ModelClientValidationRangeDateRule(  FormatErrorMessage(metadata.GetDisplayName()), 
 MinDate, MaxDate)
        };
    }
}
public class ModelClientValidationRangeDateRule 
 : ModelClientValidationRule
{
    public ModelClientValidationRangeDateRule(string errorMessage, 
 DateTime minValue, DateTime maxValue)
    {
        ErrorMessage = errorMessage;
        ValidationType = "rangedate";
 
        ValidationParameters["min"] = minValue.ToString("yyyy/MM/dd");
        ValidationParameters["max"] = maxValue.ToString("yyyy/MM/dd");
    }
}

Looking back at the client script, you can see that the validator name (“rangedate”) matches on both client and server, as do the parameters (“min” and “max”). All that remains is to add the necessary script references in the view (jquery.validate.js, jquery.validate.unobtrusive.js and our RangeDateValidator.js). With that in place, we get immediate client-side validation for our date range as well as server-side validation.

Going the extra mile

We’ve already achieved quite a lot. By adding a few script references we can apply the DataType(DataType.Date) attribute to enable the date picker control for date properties on our model. If we want to ensure the date is within a specified date range then we can also apply the DateRange attribute and we will get both client-side and server-side validation. There are many other features that could be added, including:

  • localisation support – currently hardcoded to dd/mm/yyyy format for the date input
  • adding support for data-driven validation – currently the min/max dates are fixed, but you could validate against properties on the model that specify the min/max dates
  • adding support for rolling date windows– e.g. for date of birth entry you might want the date to be at least/most n years from the current date.

All of the above are left as an exercise for the reader. We will, however, add a couple more features. The first is that when using the DateRange attibute you still have to specify the DataType attribute to mark the field as a date so that the date picker is wired up. The second is to take advantage of the support for date ranges in the date picker.

Removing the need to add DataType and DateRange

Since you’re likely to want to have the date picker for date fields and applying the DateRange attribute implies that you’re working with a date property, it would be nice if you didn’t have to apply the DataType attribute in this case. Fortunately this is made very simple in ASP.NET MVC 3 via the addition of the IMetadataAware interface. By implementing this on the DateRangeAttribute we can hook in to the metadata creation and mark the property as a date automatically:

 public class DateRangeAttribute : ValidationAttribute, 
 IClientValidatable, IMetadataAware
{
 // ... previous code omitted...
  public void OnMetadataCreated(ModelMetadata metadata)
    {
        metadata.DataTypeName = "Date";
    }
}

Integrating the date range with the date picker

The jQuery UI date picker allows you to specify a date range to allow the user to pick from. When the date range validator is applied then we have code to ensure that the date entered is in the specified range, but it would be nice to steer the user towards success by limiting the date range that the date picker displays. To do this we will modify the client-code that wires up the date picker so that it checks for the validation attributes and applies the date range if specified. All that is required for this is a slight change to the EditorHookup.js script:

 /// <reference path="jquery-1.4.4.js" />
/// <reference path="jquery-ui.js" />

$(document).ready(function () {
    function getDateYymmdd(value) {
        if (value == null)
            return null;
        return $.datepicker.parseDate("yy/mm/dd", value);
    }
    $('.date').each(function () {
        var minDate = getDateYymmdd($(this).data("val-rangedate-min"));
        var maxDate = getDateYymmdd($(this).data("val-rangedate-max"));
        $(this).datepicker({
            dateFormat: "dd/mm/yy",
            minDate: minDate,
            maxDate: maxDate
        });
    });
});

With these final two changes we no longer require the DataType attribute when we are specifying a DateRange, and the date picker will respect the date range for the validation and help to steer the user away from validation errors:

img

 

Summary

This has been a fairly lengthy post, but we’ve achieved a lot. By ensuring that we have a few script references, we will automatically get the date picker for any fields marked with DataType.Date or DateRange. If DateRange is specified then we get client-side and server-side validation that the date entered is within the specified date range, and the date picker limits the dates the user can pick to those that fall within the allowed date range. That’s quite a lot of rich functionality that can be enabled by simply adding the appropriate attribute(s).

 

I've attached a zip file containing an example project. It adds a couple of features like not requiring both min and max values for the date range, but is otherwise the code from this post.

DatePickerTemp.zip