DefaultModelBinder
I don't need to say much about model binding, most of us aware of that. The built-in
DefaultModelBinder takes away most of the burden from our shoulders
and it's ideal in most of the cases. But in some cases the DefaultModelBinder is not enough for binding a particular model or a property and in
those cases normally we go for creating a custom model binder either by creating a brand new one by implementing
IModelBinder or by extending the DefaultModelBinder.
The created custom model binder can be registered to a model by two ways either by adding into the
Binders collection in Global.asax.cs or through
the ModelBinderAttribute. The created custom model binder can be linked to a class but not to a property.
Listing 1. shows the ModelBinderAttribute and it's usage levels. The
ModelBinderAttribute helps to specify a custom model binder at
class or parameter level but not at property level. Because of this when we want to customize the binding for a single property in a model
we are forced to create a custom model binder and attach that to that model instead of that property.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct |
AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Parameter,
AllowMultiple = false, Inherited = false)]
public sealed class ModelBinderAttribute : CustomModelBinderAttribute
{
-
}
Listing 1. ModelBinderAttribute
In this article we will see how we can attach custom binding behaviors to a property through attributes.
Example
Let's take a simple example that helps you to clearly understand what are up to.
public class Movie
{
public int Id { get; set; }
public string Director { get; set; }
public string[] Actors { get; set; }
public DateTime ReleasedOn { get; set; }
}
Listing 2. Movie model
We have a Movie class and a form that helps to add new movies. The user enter the details of the actors in the textbox as a comma separated
list and if you use the DefaultModelBinder it just stores the complete actors list as a single value in the array but what we want is to convert the
comma separated list into a string array and assign to the Actors property.
Typically what we do at this time is go and create a custom model binder and register the custom binder for the complete Movie model. The problem here
is the DefaultModelBinder is a heavy class and all we want is to override the binding for the Actors property but for that we have to
create a custom binder by extending the DefaultModelBinder. It would really great if we specify the framework some way to delegate the
binding for only that property to another class.
Attributes are great way to customize the behaviors at runtime. So what we are going to do is create an attribute that contains a single method
which takes care of the binding work for the target property. The below listing shows our attribute class
PropertyBindAttribute which contains
a single abstract method BindProperty.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public abstract class PropertyBindAttribute : Attribute
{
public abstract bool BindProperty(ControllerContext controllerContext,
ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor);
}
Listing 3. PropertyBindAttribute that helps to customize binding for model properties
For customize binding the Actors property in the Movie model we have to create a custom attribute by implementing the
PropertyBindAttribute.
The below listing shows the StringArrayPropertyBindAttribute that converts the comma separated actors list into a string array.
public class StringArrayPropertyBindAttribute : PropertyBindAttribute
{
public override bool BindProperty(ControllerContext controllerContext,
ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
if (propertyDescriptor.PropertyType == typeof(string[]))
{
var value = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
propertyDescriptor.SetValue(bindingContext.Model,
value.AttemptedValue.Split(',').Select(s => s.Trim()).ToArray());
return true;
}
return false;
}
}
Listing 4. StringArrayPropertyBindAttribute that binds a comma separated string into string array
Next what we have to do is mark the Actors property with the
StringArrayPropertyBindAttribute.
public class Movie
{
...
[StringArrayPropertyBind]
public string[] Actors { get; set; }
...
}
Listing 5. Applying StringArrayPropertyBindAttribute over property
Custom Model Binder
The DefaultModelBinder doesn't know about our PropertyBindAttribute so we have to create a custom model binder. Don't think again a custom model binder?
because once we have this custom model binder we can avoid creating many model binders by extending the
DefaultModelBinder class.
public class ExtendedModelBinder: DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext,
ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor)
{
if(propertyDescriptor.Attributes.OfType<PropertyBindAttribute>().Any())
{
var modelBindAttr = propertyDescriptor.Attributes.OfType<PropertyBindAttribute>().FirstOrDefault();
if(modelBindAttr.BindProperty(controllerContext, bindingContext, propertyDescriptor))
return;
}
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
}
Listing 6. Custom model binder that delegates the binding work of properties to custom classes
We are nearly done! All we have to set our ExtendedModelBinder as the default in Global.asax.cs.
ModelBinders.Binders.DefaultBinder = new ExtendedModelBinder();
Summary
What we saw in this article is just an idea of how we can avoid creating more model binders when all we want is to customize the binding for a property in a model.
I'm a software developer from south tip of India. I spent most of the time in learning new technologies. I've a keen interest in client-side technologies especially JavaScript and admire it is the most beautiful language ever seen.
I like sharing my knowledge and written some non-popular articles. I believe in quality and standards but blames myself for lagging them.
I believe in small things and they makes me happy!