Tuesday, August 27, 2013

Binding enum arrays with MVC models

The simplicity of model binding in the MVC framework makes it a powerful and pleasant framework to send models up and down to views. But a little short coming prevents the usage of enum arrays and raising maintainability in my opinion. MVC 4 cannot convert / materialize enum values to an array when used in a model. To put it correctly, there is no standard type converter for the job.

This issue manifested when I was working on a checkbox list of statuses that could be selected as a filter. This would then be posted as a comma separated parameter value to be set on the model object. Which is finally used in a LINQ query.

Using the following model would not be working in MCV when an action method accepts it as a parameter. You'd have to use an int array, convert the int array to an enum array or the other way around to send it to the view. Which I find kind of a hassle to me, especially when debugging. Enums are godly when it comes to understand what happens in a process during runtime. Even in JavaScript even though they are string values. It's almost costs no more performance than int values nowadays.

public class CustomerListQueryModel
{
    public string SearchText { get; set; }

    public CustomerStatus[] FilterStatuses { get; set; }
}

The solution to this, as described in lots of other blogs, is to implement your own model binder. A bit of a no-brainer, although my one objection is that a lot of the conversion logic is often not being shared as an all-round conversion standard in the whole system. An unnecessary loss of coherency and potentially increasing risks of bugs in my opinion.

I have been carrying around a conversion API for the past few years which has been proven to be very useful. When used properly it tolerated enough for correct interpretation and is strict enough to prevent faulty conversions. It deals just a little bit better with string-to-type conversions than the daily Parse methods on most value type. Hence it has a generic interface for almost every type. A logical choice to integrate it into the custom model binder.

How the conversion API works in detail can be seen in the following post. The converter is required for this model binder to work correctly, because it provides the custom type converter for value type array's.

Below is the model binder. It is nothing more than a facade that makes use of the converter API.

public class ExtendedModelBinder : DefaultModelBinder
{
 /// <summary>
 /// Provides custom model binding by utilizing the Conversion helper API.
 /// </summary>
 protected override object GetPropertyValue(
  ControllerContext controllerContext,
  ModelBindingContext bindingContext,
  PropertyDescriptor propertyDescriptor, 
  IModelBinder propertyBinder)
 {
  object value = GetCustomConvertedValue(bindingContext, propertyDescriptor); 
    
  if (value != null)
  {
   return value;
  }

  return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
 }

 private static object GetCustomConvertedValue(
  ModelBindingContext bindingContext,
  PropertyDescriptor propertyDescriptor)
 {
  var propertyType = propertyDescriptor.PropertyType;
  var providerValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

  if (providerValue != null && providerValue.RawValue != null)
  {
   return Conversion.AsValue(propertyType, providerValue.RawValue.ToString());
  }

  return null;
 }
}

In the above code, when the model is bound, the model binder will first try to convert by using the conversion API, if that fails (without exception) and returns a null value, the binder falls back on the default MVC logic.

To use the binder is a matter of registration to override the default implementation, like so:

protected void Application_Start()
{
    //// Your other useful code...

    ModelBinders.Binders.DefaultBinder = new ExtendedModelBinder();
}

Voila, all done. MVC will now be using the custom model binder. To check fro sure, you could set a break point to see if the GetPropertyValue method is being called.

No comments:

Post a Comment