Once upon a day I created a viewmodel class:
public class MyViewModel { public List<KeyValuePair<string, int>> MyList { get; set; } }
I wanted to use it as a parameter in my MVC action method. The wonderful model binding feature of MVC allows me to do that and it seemed to be working without error.
I got the exact number of key value pairs in my list property but the Key and Value props were always null and 0. I repeat: without any error!
After checking DefaultModelBinder’s source I realized that it will never work: KeyValuePair<,> is a struct, so assigning to variable means a copy and it’s members are readonly so can be set only during construction. The logic in DefaultModelBinder is different: it creates the model objects, handles them over via variable assignations, evaluates their member values and then assigns those values to members. There is a workaround implemented inside related to Dictionary<,>, but it’s logic isn’t reusable for my situation because the programmer didn’t intended to allow that (private methods) and the logic there is a bit smells for me.
There are solutions on the net, but those I found suffer from one common problem: they evaluate Key and Value on their onnw, which skips some goods of model binding, e.g. validation and model state propagation. Not too good.
Here comes my solution. 🙂
First I created a new default model binder which in case of KeyValuePair<,> model type calls my BindModelViaKeyValuePairSubstitute from BindModel method but leaves all other things handled by original implementation.
public class KeyValuePairCapableDefaultModelBinder:DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { object ret = null; if (TypeHelper.IsSubclassOf(bindingContext.ModelType, typeof(KeyValuePair<,>))) { ret = BindModelViaKeyValuePairSubstitute(controllerContext, bindingContext); } else { ret = base.BindModel(controllerContext, bindingContext); } return ret; } }
I created a substitute class which overcomes the limitations: not a struct and has writable members.
To make the trick transparent to model binding the substitute class must contain members of same name and type as the KeyValuePair<,> we want to handle.
/// <summary> /// KeyValuePair substitute. /// </summary> private class KeyValuePairSubstitute<TKey, TValue> : KeyValuePairSubstituteBase { public TKey Key { get { return (TKey)KeyAsObject; } set { KeyAsObject = value; } } public TValue Value { get { return (TValue)ValueAsObject; } set { ValueAsObject = value; } } public KeyValuePairSubstitute() { // set values to defaults to avoid NullReferenceExceptions when trying to get // an uninitialized null value from a generic type which cannot stand that (e.g. int). this.Key = default(TKey); this.Value = default(TValue); } } /// <summary> /// Base class for KeyValuePair substitute to allow access to generic values in handy way. /// </summary> private class KeyValuePairSubstituteBase { public object KeyAsObject { get; set; } public object ValueAsObject { get; set; } }
Now my BindModelViaKeyValuePairSubstitute is trivial.
The logic here is to let DefaultModelBinder bind our substitute object instead of a KeyValuePair<,> and then instantiate a KeyValuePair<,> from that object’s content.
/// <summary> /// Default BindModel call doesnt handle KeyValuePair well, because it is a struct and has readonly props. /// It will return an instance with default values without any error! /// </summary> private object BindModelViaKeyValuePairSubstitute(ControllerContext controllerContext, ModelBindingContext bindingContext) { object ret = null; var keyValuePairSubstituteGeneric = typeof(KeyValuePairSubstitute<,>).MakeGenericType(bindingContext.ModelType.GetGenericArguments()); var kvpBindingContext = new ModelBindingContext() { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, keyValuePairSubstituteGeneric), ModelName = bindingContext.ModelName, ModelState = bindingContext.ModelState, ValueProvider = bindingContext.ValueProvider }; var keyValuePairSubstitute = (KeyValuePairSubstituteBase)base.BindModel(controllerContext, kvpBindingContext); ret = Activator.CreateInstance(bindingContext.ModelType, keyValuePairSubstitute.KeyAsObject, keyValuePairSubstitute.ValueAsObject); return ret; }
The last step: the new model binder should be registered in Application_Start as usual:
ModelBinders.Binders.DefaultBinder = new CustomModelBinder();
That’s all. You have bindable and validable KeyValuePair<,>s now!
One thought on “KeyValuePair<,> capable ASP.NET MVC5 model binder”