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”