I have a method in my controller that accepts an object as an argument and returns a JsonResult. One of the properties on this object is an enum with three possible values. I assumed that when the client passed in an int for that property it would populate the enum, but it doesn't, it defaults to 0 and the enum is set to the first of it's possible selections.
Any suggestions?
NOTE: This has been resolved in MVC 4. If upgrading to MVC 4 is a viable option for your project then that is all you have to do in order to begin model-binding to enums.
That said, here is the workaround for MVC 3 if you still need it.
The issue is with the default model binder in MVC. The correct integer value makes it to the model binder but the binder is not coded to map to the integer value of the enum. It correctly binds if the value being passed in is a string containing the named value of the enum. The problem with this is that when you parse a C# object into JSON using the Json()
method it sends the integer value as the enum value, not the named value.
The easiest and most transparent fix for this is to override the default model binder and write some custom logic to fix the way it binds enums.
Create a new class, like so.
namespace CustomModelBinders
{
/// <summary>
/// Override for DefaultModelBinder in order to implement fixes to its behavior.
/// This model binder inherits from the default model binder. All this does is override the default one,
/// check if the property is an enum, if so then use custom binding logic to correctly map the enum. If not,
/// we simply invoke the base model binder (DefaultModelBinder) and let it continue binding as normal.
/// </summary>
public class EnumModelBinder : DefaultModelBinder
{
/// <summary>
/// Fix for the default model binder's failure to decode enum types when binding to JSON.
/// </summary>
protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
{
var propertyType = propertyDescriptor.PropertyType;
if (propertyType.IsEnum)
{
var providerValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (null != providerValue)
{
var value = providerValue.RawValue;
if (null != value)
{
var valueType = value.GetType();
if (!valueType.IsEnum)
{
return Enum.ToObject(propertyType, value);
}
}
}
}
return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
}
}
}
Then simply register it in your Global.asax file.
protected override void OnApplicationStarted()
{
base.OnApplicationStarted();
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
// Register your new model binder
ModelBinders.Binders.DefaultBinder = new EnumModelBinder();
}
That's it. Enums will now be correctly bound on JSON objects.
http://www.codetunnel.com/how-to-bind-to-enums-on-json-objects-in-aspnet-mvc-3
What about binding to a hook property on your model?
public class SomeModel
{
public MyEnum EnumValue { get; set; }
public int BindToThisGuy
{
get { return (int) EnumValue; }
set { EnumValue = (MyEnum)value; }
}
}
Ok guys. I've looked up a few ways to do this because I was tired of writing stupid work around to get past this deficiency in the .Net framework. Based on a couple of threads, I have composed the following solution.
Disclaimer, this is not a totally automated solution, so it will not work for all. Given my implementation, it works. Maybe my way will help someone else design something that will work for them.
First, I created an enum respository. The enums don't have to reside here, but they would need to be visible from the repository.
In the repository, I created a class and a public static property to expose a list of enum types.
namespace MyApp.Enums
{
public enum ATS_Tabs { TabOne = 0, TabTwo = 1, TabThree = 2, TabFour = 3, TabFive = 4 };
public class ModelEnums
{
public static IEnumerable<Type> Types
{
get
{
List<Type> Types = new List<Type>();
Types.Add(typeof(ATS_Tabs));
return Types;
}
}
}
}
Next, I created a model binder and implemented the IModelBinder interface (ref. kdawg's comment and link).
namespace MyApp.CustomModelBinders
{
public class EnumModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
ModelState modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
return Enum.ToObject(Type.GetType(bindingContext.ModelType.AssemblyQualifiedName), Convert.ToInt32(valueResult.AttemptedValue));
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}
bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
}
It might help to add some code to ensure the conversion of valueResult.AttemptedValue doesn't fail.
Next, I looped through the list of enum types I created above and added model binders for them (...in Global.asax.cs).
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
foreach (Type type in ModelEnums.Types)
{
ModelBinders.Binders.Add(type, new EnumModelBinder());
}
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
I admit, this it not the most intuitive way, but it works great for me. Feel free to let me know if I can optimize this.