Binding a custom property with model binders
public class MyModel { [Required] public string Name { get; set; } [Required] public DateTime StartDate { get; set; } [Required] public DateTime StartTime { get; set; } [CustomValidation(typeof(CreateJourneyModel), "OnDateValidate")] public DateTime FullStartDateTime { get; set; } //Custom validation logic which needs to validate the full date/time internal static ValidationResult OnDateValidate(DateTime dt, ValidationContext validationContext) { return dt < DateTime.Now.AddHours(1) ? new ValidationResult("Enter a date/time greater than 3 hours from now") : ValidationResult.Success; } }
The above model will allow us to have a different date and time field rendered for input on a form but requires the combined date/time to run custom validation against. You might already be thinking: Isn’t this the same problem that Scott Hanselman solved in this post? Well, sort of but not really. What Scott did was assign a custom model binder to all instances of DateTime or set specifically on an Action parameter and was also splitting one DateTime field into 2 on the front-end . I’m not really a fan of having a custom model binder for all instances of DateTime in my application , I don’t have a DateTime parameter in my Action to explicitly assign a model binder too and I also decided to use 2 DateTime properties to represent 2 different fields on my form. The solution presented below could allow you to custom bind any single property of your model which you could then assign custom validation to.
First, we need to put the fields on your form so they get posted back to the server. Using the Html.EditorFor(x => …..) syntax is the way to go, however, for the field we want to custom bind using Html.HiddenFor(x => …. ) is better so the field isn’t actually displayed on the form. This will add this field name to the ValueProvider when it’s posted to the server.
Next, create the model binder. Since we cannot assign a model binder to a property of a model, we need to make the binder for the model containing the property. And, since we don’t want to do any custom binding for the rest of the properties, we’ll just inherit from DefaultModelBinder and let it do the work for us. The only thing we have to do is override GetPropertyValue , check for the property that we want to custom bind and bind it!
public class MyCustomModelBinder : DefaultModelBinder { protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) { //check if it the 'FullStartDateTime' property if (propertyDescriptor.Name == "FullStartDateTime") { //try to get the date part var dateValue = bindingContext.ValueProvider.GetValue("StartDate"); DateTime date; if (!string.IsNullOrEmpty(dateValue.AttemptedValue) && DateTime.TryParseExact(dateValue.AttemptedValue, "dd/MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out date)) { //try to get the time part var timeValue = bindingContext.ValueProvider.GetValue("StartTime"); DateTime time; if (!string.IsNullOrEmpty(timeValue.AttemptedValue) && DateTime.TryParseExact(timeValue.AttemptedValue, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, out time)) { //we've got both parts, join them together var fullDateTime = new DateTime(date.Year, date.Month, date.Day, time.Hour, time.Minute, time.Second); return fullDateTime; } } //could not bind return null; } //let the default model binder do it's thing return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder); } }
Last, all we need to do is assign the custom model binder to the custom model which can easily be done by just attributing the model:
[ModelBinder(typeof(MyCustomModelBinder))] public class MyModel {...}
That's it! Now all validation is handled as per normal on the custom property.