Customizing property binding through attributes
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.
The 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 we are upto.
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 in place we can avoid creating many model binders by extending DefaultModelBinder.
public class ExtendedModelBinder: DefaultModelBinder { protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) { var propBindAttr = propertyDescriptor.Attributes.OfType<PropertyBindAttribute>().FirstOrDefault(); if(propBindAttr != null && propBindAttr.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 do is set our ExtendedModelBinder as the default one 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.