We will use the jQuery UI Date Picker to provide a nice calendar for picking the dates and as a final twist in the tale, we’ll use a Custom Date format in the Picker. There is a nasty bug in jQuery UI’s date picker, so we’ll see how we can fix that as well using another JavaScript library Moment.js.
Date formatting is often a major pain. Throw in the weird Date format that MVC returns and you will end up tearing out your hair in frustration with Date.Parse. Enter Moment.JS, a neat little Open Source, JavaScript library, which takes the pain of Date Formatting and Conversion (among other things) away!
The ASP.NET MVC Demo Application
We have a Basic ASP.NET MVC Application with the latest jQuery, jQuery UI and Unobtrusive Validation libraries installed from Nuget. It has a single HomeController that has the Index Action Method. We have a corresponding view in the Home\Index.cshtml.public class ScheduleTask
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
}
The Index.cshtml uses the Create Template for the above entity so it has the following markup to start off with
@model DateRangeValidator.Models.ScheduleTask
@using (Html.BeginForm())
{
@Html.ValidationSummary(true)
<fieldset>
<legend>ScheduleTask</legend>
<div class="editor-label">
@Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.Description)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Description)
@Html.ValidationMessageFor(model => model.Description)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.StartDate)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.StartDate)
@Html.ValidationMessageFor(model => model.StartDate)
</div>
<div class="editor-label">
@Html.LabelFor(model => model.EndDate)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.EndDate)
@Html.ValidationMessageFor(model => model.EndDate)
</div>
<p>
<input type="submit" value="Index" />
</p>
</fieldset>
}
For jQuery UI styling, we add the following bundle in the _Layout.cshtml
@Scripts.Render("~/bundles/jqueryui")
If we run the app, it looks as follows:
Enabling Client Side Validation
Initially unobtrusive validators are not in place and validations happen on the server. To enable client side validation, we update the _Layout.cshtml and add the jQueryVal script bundle to it. The final markup for _Layout.cshtml is as follows<body>
@RenderBody()
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/jqueryui")
@Scripts.Render("~/bundles/jqueryval")
@RenderSection("scripts", required: false)
</body>
The default validations are now in place, so for example, if you put in invalid dates, Unobtrusive validator kicks in and flags an error, without a server round trip.
Adding jQuery UI DatePicker
We add the following script in the Index.cshtml to add DatePickers for the StartDate and EndDate inputs boxes.@section Scripts{
<script type="text/javascript">
$(document).ready(function () {
$('#StartDate,#EndDate').datepicker({
showOn: "button"
}).next('button').text('Select Date').button({ icons: { primary: 'ui-icon-calendar' },
text: false });
})
</script>
}
This will give us Date Pickers for the Date Fields
We are all set with our harness now, let’s build the custom date validator.
Implementing Custom Server Side Date Comparison Validator
Custom Validators are usually derived from the ValidationAttribute class. To hook in required data- attributes when rendering the HTML, we also need to implement the IClientValidatable interface.We add a new folder called Validators and add the class DateComparerAttribute that inherits from ValidationAttribute and implements IClientValidatable interface
public class DateComparerAttribute : ValidationAttribute, IClientValidatable
{
public string FirstDate { get; private set; }
public string SecondDate { get; private set; }
public CompareType Compare { get; set; }
public DateComparerAttribute(string firstDate, string secondDate, CompareType type)
: base()
{
FirstDate = firstDate;
SecondDate = secondDate;
Compare = type;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata
metadata, ControllerContext context)
{
}
}
We define a CompareType enum to make things a little more readable compared to passing magic numbers representing comparison type.
public enum CompareType
{
EqualsTo,
GreaterThan,
LessThan
}
Defining ValidationRules for hookup up client validation adapter
The GetClientValidationRules method defines a ModelClientValidationRule object that contains the meta-information needed to be passed back at the time of validation. In our case, we’ll pass the name of the two date fields that we’ll be comparing against and the comparison that needs to be done (i.e. LessThan, GreaterThan or EqualsTo).public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata
metadata, ControllerContext context)
{
var clientValidationRule = new ModelClientValidationRule()
{
ErrorMessage = FormatErrorMessage(FirstDate + " must be " +
Enum.GetName(typeof(CompareType), Compare) + " " + SecondDate),
ValidationType = "datecomparer"
};
clientValidationRule.ValidationParameters.Add("firstdate", FirstDate);
clientValidationRule.ValidationParameters.Add("seconddate", SecondDate);
clientValidationRule.ValidationParameters.Add("compare", Compare);
return new[] { clientValidationRule };
}
Validating on the Server
To validate on the server, we override the IsValid method of our custom ValidationAttribute.We then extract the property values passed to us in the model object and based on the Comparison requested, we compare the two dates. If they are not according to the comparison requested, we set an appropriate error message. The code listing for it is as follows:
protected override ValidationResult IsValid(object value, ValidationContext
validationContext)
{
var firstDate = validationContext.ObjectInstance.GetType().GetProperty(FirstDate);
var secondDate = validationContext.ObjectInstance.GetType().GetProperty(SecondDate);
var firstDateValue = firstDate.GetValue(validationContext.ObjectInstance, null);
var secondDateValue = secondDate.GetValue(validationContext.ObjectInstance, null);
if (value is DateTime && ((firstDateValue is DateTime) && (secondDateValue is DateTime)))
{
if (Compare == CompareType.EqualsTo)
{
bool equals = ((DateTime)firstDateValue)==((DateTime)secondDateValue);
if (!equals)
{
return new ValidationResult(FormatErrorMessage(FirstDate + " must be equal to " +
SecondDate));
}
}
else if (Compare == CompareType.GreaterThan)
{
bool equals = ((DateTime)firstDateValue) > ((DateTime)secondDateValue);
if (!equals)
return new ValidationResult(FormatErrorMessage(FirstDate + " must be greater than " +
SecondDate));
}
else if (Compare == CompareType.LessThan)
{
bool equals = ((DateTime)firstDateValue) < ((DateTime)secondDateValue);
if (!equals)
return new ValidationResult(FormatErrorMessage(FirstDate + " must be less than " +
SecondDate));
}
}
return ValidationResult.Success;
}
This essentially covers the Server Side validator.
Using the Validator
To use the validator we update the ScheduleTask.cs and decorate the StartDate OR EndDate with the validation attribute. We have put the validator on the EndDate here and we are checking if EndDate is Greater than (after) Start Date.public class ScheduleTask
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
//[DateComparer("StartDate", "EndDate", CompareType.LesserThan)]
public DateTime StartDate { get; set; }
[DateComparer("EndDate", "StartDate", CompareType.GreaterThan)]
public DateTime EndDate { get; set; }
}
If we run the application now we’ll get validation errors, if any, when the data is Posted.
Implementing the jQuery Adapter for Unobtrusive client side Date Comparison
To do the client side validation, we need to create a custom adapter for the unobtrusive validator. We add it as follows$(function () {
jQuery.validator.unobtrusive.adapters.add
('datecomparer', ['firstdate', 'seconddate', 'compare'], function (options) {
var attribs = {
firstdate: options.params.firstdate,
seconddate: options.params.seconddate,
compare: options.params.compare
};
options.rules['datecomparercheck'] = attribs;
if (options.message) {
$.validator.messages.datecomparercheck = options.message;
}
});
//.. the validation method – removed for brevity
}
As we can see the first input parameter is the string ‘datecomparer’. This is the same key that we added to the Client Validation Rule above. The array implies the three parameters that we need for validation. These are all stored as data-datecomparer-[param name].
If we do a ‘View Source’ of the page after we’ll see the attributes
<input
class="text-box single-line"
data-val="true"
data-val-date="The field EndDate must be a date."
data-val-datecomparer="EndDate must be GreaterThan StartDate"
data-val-datecomparer-compare="GreaterThan"
data-val-datecomparer-firstdate="EndDate"
data-val-datecomparer-seconddate="StartDate"
data-val-required="The EndDate field is required."
id="EndDate" name="EndDate"
type="datetime" value="1/1/0001 12:00:00 AM" />
Validation Implementation
The validation method in the above adapter is defined below. Its implementation is similar to the server side except here it extracts the values from the DOM using jQuery. Once the values have been extracted, we convert them to dates. If the dates are valid, we check the Comparison condition and send back a true or false based on the comparison result:jQuery.validator.addMethod(
'datecomparercheck',
function (value, element, params) {
var result = false;
if (value && (params['firstdate'] != null && params['seconddate'] != null)) {
var startdatevalue = $('input[id="' + params['firstdate'] + '"]').datepicker().val();
var enddatevalue = $('input[id="' + params['seconddate'] + '"]').datepicker().val();
var dateFormat = $('input[id="' + params['firstdate'] + '"]').datepicker("option", "dateFormat");
var sDate = Date.parse(startdatevalue);
var eDate = Date.parse(enddatevalue);
if (params['compare'] == 'GreaterThan') {
result = sDate > eDate;
}
else if (params['compare'] == 'LessThan') {
result = sDate < eDate;
}
else if (params['compare'] == 'Equals') {
result = sDate == eDate;
}
}
return result;
}
);
At this point, our client side validator is working as well and we’ll get validation messages even without a postback.
Custom Formats and a subtle issue with jQuery UI Datepicker
The date picker allows us to specify custom date formats and if you use them, a bug in the Datepicker causes immense pain. Let’s see the bug first.Update the date picker initialization in Index.cshtml to include a format as follows
<script type="text/javascript">
$(document).ready(function () {
$('#StartDate,#EndDate').datepicker({
showOn: "button",
dateFormat: 'd-M-yy',
onClose: function (dateText, inst) {
$('#EndDate').valid();
}
}).next('button').text('Select Date').button({ icons: { primary: 'ui-icon-calendar' }, text: false });
})
</script>
Now run the application and select two valid dates, you’ll get the following!
Bummer! How do we hack around this now? Well after a lot of searching, I got the a SO result which led the following solution.
1. Install Moment.JS from Nuget using
PM> install-package moment.js
2. Add the following function on $(document).ready(…). This is a bazooka, there might be more elegant solutions feel free to shout out
$.validator.methods.date = function (value, element) {
return this.optional(element)
|| moment(value).format("DD-MMM-YYYY");
};
What this does is it adds an additional format option for the date function. In case you use the default format, this.optional(element) call takes care of the formatting. In case of custom formatting, moment JS is taking care of the formatting
3. Disadvantage of using Moment.JS is that format strings for date picker and MomentJS are not the same. So you may have to write a translation function.
4. Now the Custom Validator will start to fail because Date.parse will fail for this format
5. So we fix the Custom Validator also using Moment and update the code as follows
jQuery.validator.addMethod(
'datecomparercheck',
function (value, element, params) {
… removed for brevity
var sDate = moment(startdatevalue, "DD-MMM-YYYY");
var eDate = moment(enddatevalue, "DD-MMM-YYYY");
… removed for brevity
}
);
As you can see, we are using the same formatting as in Step 2 above. This results in the following values that are validated correctly again.
Neat so all the functionality we want are working. Yes, we did hardcode the formatting to stick to the context, but take it as an exercise on how to make the format generic or a part of the validator itself.
Conclusion
We saw how to build a custom unobtrusive jquery validator that verifies date conditions between two dates.We also saw how to workaround a ‘bug’ in the jQuery UI Date Picker and last but not least, we saw a glimpse of Moment.JS. It is an awesome Date manipulation library for JavaScript, do check it out!
Download the entire source code of this article (Github)
Tweet
1 comment:
Nice Article. Thank you very much for posting this blog. It makes validation pretty simple in Server and Client side.
Post a Comment