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.Model to the object type to get the model you’re binding to.
  • If the form contains a collection of the objects, and you’ve used EditorFor on the whole collection or a model that contains it (like you should), the form parameters will have names like ItemCollection[4]_ItemClass.PropName. If you use EditorFor directly on the object, the form parameter names will just be the property names. bindingContext.ModelName contains the prefix for the first case, and I’ve appended .DeliveryDate to get the right form parameter name for the particular WebOrderEntity I’m binding to. I’m not sure if this will work for the second case; the key might just be DeliveryDate without the prefix or period.
  • Once you figure out the key, bindingContext.ValueProvider.GetValue() returns an object that describes the value that was passed in. AttemptedValue is the string that was passed in. There’s a RawValue property too, but it was also a string. I’m not sure when you’d use it; I was expecting it to be a DateTime object.
  • If there are any errors, call bindingContext.ModelState.AddModelError() to add an error to the ModelState object 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.