I have a model to validate and the problem is the date of birth field.
It must be composed of 3 dropdowns (day, month, year).
<div id="dob-editor-field" class="model-field-editor">
@Html.LabelFor(m => m.DateOfBirth, new { @class = "label-div" })
@Html.Telerik().DropDownList().Name("DobDay").BindTo((SelectList)ViewData["Days"]).HtmlAttributes(new {id = "DobDaySel"})
@Html.Telerik().DropDownList().Name("DobMonth").BindTo((SelectList)ViewData["Months"]).HtmlAttributes(new { id = "DobMonthSel"})
@Html.Telerik().DropDownList().Name("DobYear").BindTo((SelectList)ViewData["Years"]).HtmlAttributes(new { id = "DobYearSel" })
@Html.ValidationMessageFor(m => m.DateOfBirth)
</div>
On the server side i do this
[HttpPost]
public ActionResult Register(RegistrationModel regInfo, int DobDay, int DobMonth, int DobYear)
{
SetRegisterViewData(DobDay, DobMonth, DobYear);
if (DobDay == 0 || DobMonth == 0 && DobYear == 0)
{
ModelState.AddModelError("DateOfBirth", "Date of birth is required");
}
else
{
DateTime dt = new DateTime(DobYear, DobMonth, DobDay);
long ticks = DateTime.Now.Ticks - dt.Ticks;
int years = new DateTime(ticks).Year;
if (years < 18)
{
ModelState.AddModelError("DateOfBirth", "You must be at least 18");
}
}
if (ModelState.IsValid)
{
//register user
return RedirectToAction("Index", "Home");
}
return View(regInfo);
}
Questions:
- Server side : how to make it better? (i am thinking of adding dob,
month, and year properties RegistrationModel and add attribute on
DateOfBirth to check those properties) - Client side : i was looking at Perform client side validation for custom attribute but it got me confused. What is the way to make it?
LE:
I created a custom model binder for the date like this:
public class DobModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.Name == "DateOfBirth")
{
DateTime dob = DateTime.MinValue;
var form = controllerContext.HttpContext.Request.Form;
int day = Convert.ToInt32(form["DobDay"]);
int month = Convert.ToInt32(form["DobMonth"]);
int year = Convert.ToInt32(form["DobYear"]);
if (day == 0 || month == 0 || year == 0)
{
SetProperty(controllerContext, bindingContext, propertyDescriptor, DateTime.MinValue);
}
else
{
SetProperty(controllerContext, bindingContext, propertyDescriptor, new DateTime(year, month, day));
}
}
else
{
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
}
I registered it like this:
ModelBinders.Binders.Add(typeof(DateTime), new DobModelBinder());
I used it like this:
public ActionResult Register([ModelBinder(typeof(DobModelBinder))]RegistrationModel regInfo)
DateOfBirth binds well.
LE2:
I created validation attributes for the date of birth like this:
public override bool IsValid(object value)
{
DateTime date = Convert.ToDateTime(value);
return date != DateTime.MinValue;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage,
ValidationType = "dateRequired"
};
}
}
public class DateGraterThanEighteen : ValidationAttribute, IClientValidatable
{
public override bool IsValid(object value)
{
DateTime date = Convert.ToDateTime(value);
long ticks = DateTime.Now.Ticks - date.Ticks;
int years = new DateTime(ticks).Year;
return years >= 18;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage,
ValidationType = "dateGraterThanEighteen"
};
}
}
I applied attributes like this
[DateGraterThanEighteen(ErrorMessage="You must be at least 18")]
[DateRequired(ErrorMessage = "Date of birth is required")]
public DateTime DateOfBirth { get; set; }
LE3:
In the client side i do this:
$(function () {
jQuery.validator.addMethod('dobRequired', function (value, element, params) {
if (!/Invalid|NaN/.test(new Date(value))) {
return true;
}
else {
return false;
}
}, '');
jQuery.validator.unobtrusive.adapters.add('dateRequired', {}, function (options) {
options.rules['dobRequired'] = true;
options.messages['dobRequired'] = options.message;
});
});
Client validation doesn't seems to work.
How can I fix it? I am kinda confused with the way these adapters work.
Best Answer
You could use a custom editor template.
Let's first look at how the final solution might look like first before getting into implementation details.
So we could have a view model (as always) decorated with some data annotation attributes indicating the metadata we would like to attach to it:
then we could have a controller:
a view (
~/Views/Home/Index.cshtml
):and a corresponding editor template which will allow us to display 3 dropdown lists for editing the DateTime field instead of a simple textbox (
~/Views/Shared/EditorTemplates/TrippleDDLDateTime.cshtml
):Now let's see how the
[TrippleDDLDateTime]
attribute could be implemented:Notice how the attribute implements the
IMetadataAware
interface which allows us to associate the view model property with the custom editor template we wrote (TrippleDDLDateTime.cshtml
).And next comes the
[MinAge]
attribute:The last piece of the puzzle is to write a custom model binder that will be associated to properties decorated with the
[TrippleDDLDateTime]
attribute in order to perform the parsing:Notice how the binder simply uses the default binder if the field is not decorated with the custom attribute. This way it doesn't interfere with other DateTime fields for which we don't want the tripple ddl behavior. The model binder will simply be associated with the
DateTime?
type inApplication_Start
:OK, so far we have a solution that performs server side validation. That's always what you should start with. Because that's where you can also stop and still have a safe and working site.
Of course if you have time you could now improve the user experience by implementing client side validation. Client side validation is not compulsory, but it saves bandwidth and avoids server round-trips.
So we start by making our 2 custom attributes implement the
IClientValidatable
interface which is the first step in enabling unobtrusive client side validation.[TrippleDDLDateTime]
:[MinAge]
:OK, so we have implemented the
GetClientValidationRules
on both attributes. All that's left is to write the corresponding unobtrusive adapters.This should be done in a separate javascript file of course. For example it could be
trippleddlAdapters.js
:Finally we include the 3 necessary scripts to the page to enable the unobtrusive client side validation: