This article is part of the Maintainable MVC Series.
By using the form models as spoken about in View Model and Form Model the need for custom binding doesn’t arise to often anymore.
But every now and then we have some duplicate code doing something with incoming parameters. Perhaps we can move this logic into a binder to be more DRY. Or if our form model has types that MVC can’t bind automatically – like enumerations – custom binding comes into play.
MVC is extensible on the part of binding form data or get parameters to your method parameters. You can define your own binders and have them work for certain types.
SmartBinder
Jimmy Bogard wrote a very helpful class called SmartBinder, which you can read all about over here. We use it for all our custom binders. You can see the neccessary code below:
public interface IFilteredModelBinder : IModelBinder { bool IsMatch(Type modelType); new BindResult BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext); }
public class SmartBinder : DefaultModelBinder { private readonly IFilteredModelBinder[] filteredModelBinders; public SmartBinder(IFilteredModelBinder[] filteredModelBinders) { this.filteredModelBinders = filteredModelBinders; } public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { foreach (var filteredModelBinder in filteredModelBinders) { if (filteredModelBinder.IsMatch(bindingContext.ModelType)) { BindResult result = filteredModelBinder.BindModel(controllerContext, bindingContext); bindingContext.ModelState.SetModelValue(bindingContext.ModelName, result.ValueProviderResult); return result.Value; } } return base.BindModel(controllerContext, bindingContext); } }
public class BindResult { public object Value { get; private set; } public ValueProviderResult ValueProviderResult { get; private set; } public BindResult(object value, ValueProviderResult valueProviderResult) { Value = value; ValueProviderResult = valueProviderResult ?? new ValueProviderResult(null, string.Empty, CultureInfo.CurrentCulture); } }
Setting it up with StructureMap
To have MVC use this SmartBinder as the default binder we add the following line to the Application_Start method in the global.asax.cs:
ModelBinders.Binders.DefaultBinder = ObjectFactory.GetInstance<SmartBinder>();
And for setting up the list of binders implementing the IFilteredModelBinder interface neccessary for them to work from within the SmartBinder we add the following StructureMap registry:
public class BinderRegistry : Registry { public BinderRegistry() { For<IFilteredModelBinder>().Add<EnumBinder<SomeEnumeration>>() .Ctor<SomeEnumeration>().Is(SomeEnumeration.FirstValue); For<IFilteredModelBinder>().Add<EnumBinder<AnotherEnumeration>>() .Ctor<AnotherEnumeration>().Is(AnotherEnumeration.FifthValue); } }
Because the SmartBinder is instantiated with ObjectFactory.GetInstance<SmartBinder>() and it expects an array of IFilteredModelBinders, the For<IFilteredModelBinder>().Add<…>() makes sure StructureMap returns all defined binders if an IEnumerable of IFilteredModelBinder is needed for instantiating a class.
For a simple example of an IFilteredModelBinder I’ll show you the very useful EnumBinder.
EnumBinder
For binding enumerations Rupert Bates has this custom binder, which
looks as follows when having it implement the IFilteredModelBinder interface:
public class EnumBinder<T> : DefaultModelBinder, IFilteredModelBinder { private readonly T defaultValue; public EnumBinder(T defaultValue) { this.defaultValue = defaultValue; } public bool IsMatch(Type modelType) { return modelType == typeof (T); } BindResult IFilteredModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { T result = bindingContext.ValueProvider[bindingContext.ModelName] == null ? defaultValue : GetEnumValue(defaultValue, bindingContext.ValueProvider[bindingContext.ModelName].AttemptedValue); return new BindResult(result, null); } private static T GetEnumValue(T defaultValue, string value) { T enumType = defaultValue; if ((!String.IsNullOrEmpty(value)) && (Contains(typeof (T), value))) { enumType = (T) Enum.Parse(typeof (T), value, true); } return enumType; } private static bool Contains(Type enumType, string value) { return Enum.GetNames(enumType).Contains(value, StringComparer.OrdinalIgnoreCase); } }
For MVC 2 the EnumBinder looks a bit different:
public class EnumBinder<T> : DefaultModelBinder, IFilteredModelBinder { private readonly T defaultValue; public EnumBinder(T defaultValue) { this.defaultValue = defaultValue; } public bool IsMatch(Type modelType) { return modelType == typeof(T); } BindResult IFilteredModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { T result = bindingContext.ValueProvider.GetValue(bindingContext.ModelName) == null ? defaultValue : GetEnumValue(defaultValue, bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue); return new BindResult(result, null); } private static T GetEnumValue(T defaultValue, string value) { T enumType = defaultValue; if ((!String.IsNullOrEmpty(value)) && (Contains(typeof(T), value))) { enumType = (T)Enum.Parse(typeof(T), value, true); } return enumType; } private static bool Contains(Type enumType, string value) { return Enum.GetNames(enumType).Contains(value, StringComparer.OrdinalIgnoreCase); } }
We don’t have to explicitly add the binder in Application_Start like Rupert does, because StructureMap handles it for us through the BinderRegistry.
Testing the binder
Of course testing the binder is hard again, because of the ControllerContext and ModelBindingContext, but it can be done with the following code:
[TestFixture] public class EnumBinderTests { private IFilteredModelBinder binder; [SetUp] public void Setup() { binder = new EnumBinder<TestEnum>(TestEnum.ValueB); } [Test] public void IsMatchShouldReturnTrueIfTypeIsSameAsGenericType() { // act bool isMatch = binder.IsMatch(typeof(TestEnum)); // assert Assert.That(isMatch, Is.True); } [Test] public void IsMatchShouldReturnFalseIfTypeIsNotSameAsGenericType() { // act bool isMatch = binder.IsMatch(typeof(string)); // assert Assert.That(isMatch, Is.False); } [Test] public void BindModelShouldReturnEnumValueForWhichValueAsStringIsPosted() { // arrange ControllerContext controllerContext = GetControllerContext(); ModelBindingContext bindingContext = GetModelBindingContext( new ValueProviderResult(null, "ValueA", null)); // act BindResult bindResult = binder.BindModel(controllerContext, bindingContext); // assert Assert.That(bindResult.Value, Is.EqualTo(TestEnum.ValueA)); } [Test] public void BindModelShouldReturnDefaultValueIfNoValueIsPosted() { // arrange ControllerContext controllerContext = GetControllerContext(); ModelBindingContext bindingContext = GetModelBindingContext(null); // act BindResult bindResult = binder.BindModel(controllerContext, bindingContext); // assert Assert.That(bindResult.Value, Is.EqualTo(TestEnum.ValueB)); } [Test] public void BindModelShouldReturnDefaultValueIfUnknownValueIsPosted() { // arrange ControllerContext controllerContext = GetControllerContext(); ModelBindingContext bindingContext = GetModelBindingContext( new ValueProviderResult(null, "Unknown", null)); // act BindResult bindResult = binder.BindModel(controllerContext, bindingContext); // assert Assert.That(bindResult.Value, Is.EqualTo(TestEnum.ValueB)); } [Test] public void BindModelShouldReturnDefaultValueIfDefaultValueAsStringIsPosted() { // arrange ControllerContext controllerContext = GetControllerContext(); ModelBindingContext bindingContext = GetModelBindingContext( new ValueProviderResult(null, "ValueB", null)); // act BindResult bindResult = binder.BindModel(controllerContext, bindingContext); // assert Assert.That(bindResult.Value, Is.EqualTo(TestEnum.ValueB)); } private static ControllerContext GetControllerContext() { return new ControllerContext { HttpContext = MockRepository.GenerateMock<HttpContextBase>() }; } private static ModelBindingContext GetModelBindingContext( ValueProviderResult valueProviderResult) { ValueProviderDictionary dictionary = new ValueProviderDictionary(null) { {"enum", valueProviderResult} }; return new ModelBindingContext { ModelName = "enum", ValueProvider = dictionary }; } } public enum TestEnum { ValueA, ValueB }
You’ll probably want to make some shared methods in a unit test library for mocking different types of contexts, because you’ll need them often when testing an attribute or binder.
If your custom binder has some dependencies it’s also useful to do the unit test setup with AutoMocker, as shown in Poor Man’s RenderAction.