I have an MVC project that uses data entity objects which have a virtual DeliveryDate property whose getter creates a DateTime from DeliveryYear, DeliveryMonth, and DeliveryDay int properties. There is no setter for converting the other way; it had been intentionally removed at some point. This caused a problem for me: I’m displaying DeliveryDate in a form using @Html.EditorFor(m => m.DeliveryDate), but the model that my request handler gets back from POST requests always has DateTime.Now in DeliveryDate, because the year, month, and day properties aren’t being set and the virtual returns DateTime.Now by default.
My solution is to intercept MVC's model binding and set DeliveryYear, DeliveryMonth, and DeliveryDay based on the DeliveryDate value being submitted. This is actually pretty easy to do, but it was hard to find out how to do it.
My data entity class is named WebOrderEntity. The first step is to create a class named WebOrderEntityBinder. I’ll get to the details of that class in a moment.
Next, in Global.asax’s Application_Start() method I register my custom binder class for the WebOrderEntity type:
// Register custom model binder classes
ModelBinders.Binders[typeof(RR.Entities.WebOrderEntity)] = new WebOrderEntityBinder();
Easy enough.
Now, the binder class:
public class WebOrderEntityBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = bindingContext.Model as WebOrderEntity;
// The WebOrderEntity.DeliveryDate property is virtual and doesn't have a setter,
// so to bind to it we have to parse the submitted value into a DateTime object
// and then set the Year, Month, and Day properties individually.
var deliveryDateVPR = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".DeliveryDate");
if (deliveryDateVPR != null)
{
try
{
var deliveryDate = DateTime.Parse(deliveryDateVPR.AttemptedValue);
model.DeliveryYear = deliveryDate.Year;
model.DeliveryMonth = deliveryDate.Month;
model.DeliveryDay = deliveryDate.Day;
}
catch (Exception e)
{
bindingContext.ModelState.AddModelError("DeliveryDate", "Could not parse " + deliveryDateVPR.AttemptedValue);
}
}
base.OnModelUpdated(controllerContext, bindingContext);
}
}
The tricky bits are:
- Cast
bindingContent.Modelto the object type to get the model you’re binding to. - If the form contains a collection of the objects, and you’ve used
EditorForon the whole collection or a model that contains it (like you should), the form parameters will have names likeItemCollection[4]_ItemClass.PropName. If you useEditorFordirectly on the object, the form parameter names will just be the property names.bindingContext.ModelNamecontains the prefix for the first case, and I’ve appended.DeliveryDateto get the right form parameter name for the particularWebOrderEntityI’m binding to. I’m not sure if this will work for the second case; the key might just beDeliveryDatewithout the prefix or period. - Once you figure out the key,
bindingContext.ValueProvider.GetValue()returns an object that describes the value that was passed in.AttemptedValueis the string that was passed in. There’s aRawValueproperty too, but it was also a string. I’m not sure when you’d use it; I was expecting it to be aDateTimeobject. - If there are any errors, call
bindingContext.ModelState.AddModelError()to add an error to theModelStateobject that your request handler will use to determine if the model was bound and passed validation correctly. - Finally, call the base
OnModelUpdated()method to let it do whatever it needs to do.