Tuesday, June 8, 2010

ASP.NET MVC Flags Enumeration Model Binder

The default model binder that ships with ASP.NET MVC 2 handles the large majority of databinding scenarios; however, one feature it does not support is combining a set of flags enumeration values into a single value. With the default model binder if you try to post a form with multiple enumeration values for a single model item, the returned model will contain only the last enumeration value provided by the form.

To solve this problem I created a FlagsEnumerationModelBinder to automatically handle the bitwise-or operation required to combine the enumeration values. Given a controller action that receives a model item of an enumeration type with the FlagsAttribute set, on BindModel() the FlagsEnumerationModelBinder will first check that there is more than one enumeration value in the request and then bitwise-or each individual value into a single byte value which is then returned in a call to Enum.Parse(Type, String).

public class FlagEnumerationModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
if (bindingContext == null) throw new ArgumentNullException("bindingContext");

if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) {
var values = GetValue<string[]>(bindingContext, bindingContext.ModelName);

if (values.Length > 1 && (bindingContext.ModelType.IsEnum && bindingContext.ModelType.IsDefined(typeof(FlagsAttribute), false))) {
long byteValue = 0;
foreach (var value in values.Where(v => Enum.IsDefined(bindingContext.ModelType, v))) {
byteValue |= (int)Enum.Parse(bindingContext.ModelType, value);
}

return Enum.Parse(bindingContext.ModelType, byteValue.ToString());
}
else {
return base.BindModel(controllerContext, bindingContext);
}
}

return base.BindModel(controllerContext, bindingContext);
}

private static T GetValue<T>(ModelBindingContext bindingContext, string key) {
if (bindingContext.ValueProvider.ContainsPrefix(key)) {
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(key);
if (valueResult != null) {
bindingContext.ModelState.SetModelValue(key, valueResult);
return (T)valueResult.ConvertTo(typeof(T));
}
}
return default(T);
}
}

The FlagsEnumerationModelBinder assumes that each enumeration value in the request has the same field name. The values can be in string or numeric form.

Careful about using the Html.CheckBox() extension to select your enumeration values in the form because the CheckBox and RadioButton helpers output an additional hidden value for each field that is sent with the request whether or not the field is checked. If you use the helpers you’ll end up with “true” and “false” values in the request which are [probably] not part of your enumeration.

9 comments:

Chadly said...

thanks - this is very helpful

swautier said...

Nathan,

Please forgive my ignorance: Although I'm a seasoned C# developer, I'm a newbie regarding ASP.NET and ASP.NET MVC.

Q: How would you use this to create a set of checkboxes in your Edit form?

TIA,

Lakario said...

@swautier,

You can use a for[each] loop inside your view to accomplish this. Simply loop over the result of Enum.GetValues(Type) http://msdn.microsoft.com/en-us/library/system.enum.getvalues.aspx and for each iteration create a checkbox by hand with the value attribute set to the value of that loop iteration. Do not use Html.CheckBox().

Vincent Calaor said...

I am using your code and it works great. But I am having issue after a postback, It selects other checkbox tha I did not tick. It randomly do that. e.g. when I have 5 items on my enumeration and try to unselect all then just select one, after a post back the first item is also selected aside from the one I selected.

Are you encountering this too?

Vincent Calaor said...

Sorry, the problem was just on my code. Thanks anyways this is big help.

Nathan Taylor said...

I'm glad you found this useful!

dh said...

Great work! Script does not work with nullable enumeration properties.

Nathan Taylor said...

@dh What is the behavior you are experiencing with nullables? It shouldn't be too difficult to address.

Dan Pettersson said...

I had to modify this to be able to post the numeric values, not the string of the enum. See https://dotnetfiddle.net/lGj1cF

Also, se how I add the ModelBinder attribute to the enum to actually get it to work.

Please let me know if I got this wrong...