I have found that the DefaultModelBinder, when binding to arrays, has an irritating quirk; dare I say bug? If the array is already instantiate in the model, the model binder will attempt to call .Clear() and then .Add() on the array (because all arrays support the IList interface). However arrays are fixed size lists. (IList.IsFixedSize returns true.) As a result, calling .Clear() throws a NotSupportedException as shown in the following stack trace:
System.Reflection.TargetInvocationException : Exception has been thrown by the target of an invocation. --System.NotSupportedException : Collection is read-only. at System.RuntimeMethodHandle._InvokeMethodFast(Object target, Object[] arguments, ref SignatureStruct sig, MethodAttributes methodAttributes, RuntimeTypeHandle typeOwner) at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture, Boolean skipVisibilityChecks) at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters) at System.Web.Mvc.DefaultModelBinder.CollectionHelpers.ReplaceCollection(Type collectionType, Object collection, Object newContents) at System.Web.Mvc.DefaultModelBinder.UpdateCollection(ControllerContext controllerContext, ModelBindingContext bindingContext, Type elementType) at System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext) at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) at ArrayModelBinderTests.DefaultModelBinder_should_sould_fail_to_bind_to_non_null_arrays_in_model() --NotSupportedException at System.SZArrayHelper.Clear() at System.Web.Mvc.DefaultModelBinder.CollectionHelpers.ReplaceCollectionImpl(ICollection`1 collection, IEnumerable newContents) |
In my humble opinion, what the model binder should do in the case of arrays is to ignore any already instantiated model and instead always create a new one and return it. This is what it does when there is no pre-existing instance. (i.e the model is null).
Why is this important you may ask? I mean, in general when binding to a model, the binder is creating the model from scratch anyway so won’t the array always be null? Well not always. Consider the following model:
public class FamilyModel { public string MyName { get; set; } public string MyWifesName { get; set; } private string[] _myChildrensNames; public string[] MyChildrensNames { get { return _myChildrensNames ?? (_myChildrensNames = new string[0]); } set { _myChildrensNames = value; } } } |
In this case, in order to avoid the need to check MyChildrensNames for null every time it is accessed, we ensure that it is never null. If I have no children, the property reasonably returns an empty collection. This is an example of the null object pattern.
Given such a model, we will encounter the described problem every time we attempt to bind to it.
To resolve this, I have written the following model binder which can be registered for any type of array. It uses something akin to a decorator pattern to wrap the default model binder. The decorator simply forces the model to null before invoking the default binder, effectively sidestepping the problem.
public class ArrayModelBinder : DefaultModelBinder { public override object BindModel( ControllerContext controllerContext, ModelBindingContext bindingContext) { var originalMetadata = bindingContext.ModelMetadata; bindingContext.ModelMetadata = new ModelMetadata( ModelMetadataProviders.Current, originalMetadata.ContainerType, () => null, //Forces model to null originalMetadata.ModelType, originalMetadata.PropertyName ); return base.BindModel(controllerContext, bindingContext); } } |
You can download the complete source and unit tests here.