Wednesday, March 17, 2010

Maintainable MVC: Binding

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:



[csharp]
public interface IFilteredModelBinder : IModelBinder
{
bool IsMatch(Type modelType);

new BindResult BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext);
}
[/csharp]

[csharp]
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);
}
}
[/csharp]

[csharp]
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);
}
}
[/csharp]

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:



[csharp light="true"]
ModelBinders.Binders.DefaultBinder = ObjectFactory.GetInstance<SmartBinder>();
[/csharp]

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:



[csharp]
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);
}
}
[/csharp]

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:



[csharp]
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);
}
}
[/csharp]

For MVC 2 the EnumBinder looks a bit different:



[csharp highlight="19,22"]
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);
}
}
[/csharp]

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:



[csharp]
[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
}
[/csharp]

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.

No comments:

Post a Comment