一、背景
在MVC3项目里,如果Action的参数中有Enum枚举作为对象属性的话,使用POST方法提交过来的JSON数据中的枚举值却无法正确被识别对应的枚举值。
二、Demo演示
为了说明问题,我使用MVC3项目创建Controller,并且创建如下代码演示:
- //交通方式枚举
- public enum TrafficEnum
- {
- Bus = 0,
- Boat = 1,
- Bike = 2,
- }
- public class Person
- {
- public int ID { get; set; }
- public TrafficEnum Traffic { get; set; }
- }
- public class DemoController : Controller
- {
- public ActionResult Index(Person p)
- {
- return View();
- }
- }
网站生成成功之后,就可以使用Fiddler来发送HTTP POST请求了,注意需要的是,要在Request Headers加上请求头content-type:application/json,这样才能通知服务器端Request Body里的内容为JSON格式。
点击右上角的Execute执行HTTP请求,在程序断点情况下,查看参数p,属性ID已经正确的被识别到了值为9999,而枚举值属性Traffic却被错认为枚举中的首个值Bus,这俨然是错误的,纵使你将Traffic修改成Bike,也就是值等于2,结果也是一样。
三、解决方法
方法一:
升级MVC4,亲测在MVC4项目下,这个问题已经被修复了;
方法二:
假若因为各种原因,项目不想或者不能升级为MVC4,可以在MVC3项目上做些改动,亦可修复这个问题,
1、在项目中,新建一个类,加入以下代码,需要引用一下 using System.ComponentModel; using System.Web.Mvc; 命名空间;
- /// <summary>
- /// 处理在MVC3下,提交的JSON枚举值在Controller不能识别的问题
- /// </summary>
- public class EnumConverterModelBinder : DefaultModelBinder
- {
- 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);
- }
- }
2、在Global.asax的Application_Start方法中,进行EnumConverterModelBinder类的实例化操作:
- protected void Application_Start()
- {
- //处理在MVC3下,提交的JSON枚举值在Controller不能识别的问题
- ModelBinders.Binders.DefaultBinder = new EnumConverterModelBinder();
- }
进行配置改造之后,我再次生成网站,重新发送HTTP请求看,MVC Action中的参数里的枚举就能被正确的识别到了。
#p#
四、研究
我觉得这应该是mvc3里面一个小小的缺陷吧,随着mvc的升级,这已经在新版本里被完善修复了,可还用着mvc3的人如果在项目中遇到这个问题,可以研究一下。
遇到一个问题,去百度谷歌找解决方案是可以,但是复制粘贴完代码之后,最好问下自己,为什么这样可以解决问题。
从现象和解决代码中猜想,应该是在MVC生命周期中的Model Binders 这一环节出了问题。
因为MVC已经开源了,所以我尝试着调试源码,首先下载MVC3的源码,其他项目可以移除,只保留红色框中的项目即可,然后新建一个MVC3测试项目,并且将此测试项目的system.web.mvc引用移除,转而引用本解决方案中的system.web.mvc 项目,这样子,我们才可以对MVC源码进行调试操作。
搜回来的代码中可知,我们自定义的类继承DefaultModelBinder父类,并且重写了GetPropertyValue方法,那我们就从这点开始,在MVC3源码中的System.Web.MVC项目中找到该类,在此方法上插入断点。
F5调试程序,发送一个POST请求。
其实BindProperty方法是会被多次执行的,BindProperties方法会对请求的实体类的属性进行遍历,每一个属性都要经过BindProperty方法的处理;
现在已经截获到第一个属性ID了。
紧接着,程序进入propertyBinder.BindModel 方法。
只贴部分关键代码了,通过bindingContext的ValueProvider 获得属性的相关信息,如果不等于null的话,转到执行BindSimpleModel 方法。
#p#
在BindSimpleModel方法里,首先通过Type.IsInstanceOfType方法判断确定指定的对象是否是当前 Type 的实例,如果是,则直接返回rawValue,这里的属性类型是Int32类型,返回True符合条件,所以直接把rawValue给返回去了。
第一个Int32类型属性的部分关键代码执行到这里就已经确认到值了,接下来,我们看出了问题的Enum枚举类型属性。
循环来到了第二个属性了,这时我留意到有个Model属性,对比Int32类型执行的时候,这个属性当时为0,而此时则为Bus,可见这是一个默认值,指定枚举中值为0的那个类型(即使你不为枚举显式指定值),同样的,经过BindModel方法来到了BindSimpleModel方法。
此时,对比Int32类型的属性ID,这次ModelType.IsInstanceOfType(valueProvideResult.RawValue)为False,并且接下来不是string类型就执行以下的判断,也不是数组类型,所以,来到了最后一个,根据绿色的注释可以看出,这应该是一个判断是否collection集合类型的方法,Enum都不是,所以,返回了Null。
这时,Type collectionType变量为Null,执行最后一个case 3
在ConvertProviderResult方法里,也进行了一系列的类型判断转换,目的就是将JSON中的数字类型转换成枚举值,但是执行过程中抛出异常了,原因是
“No type converter can convert between these types ” 也就是说,在MVC3的机制中,并没有相应的type converter来处理数值与枚举的对应。
经过以上这些处理方法,都没完成把对应的值确认下来,怎么给原来的BindProperty 老大方法交差呢,所以,小的只好将Value=Null 和 modelState.Errors 模型错误状态信息如实带回去了,让老大决定怎么做,老大后面处理这里有点绕,但是我看源码估计也是拿默认值来充当Value了,所以就造成了JSON传过来的值与对应枚举的值不对应的情况,无论传什么值,结果都是第一个枚举的值。
五、总结
这篇文章只是我在工作上遇到的一个小问题,然后有点小兴趣就从源码的角度上来研究和分析,缺乏理论的依据,因为之前没有很深入的去研究MVC的底层运行机制与生命周期,所以这方面还需要得加强学习一下,如果你也有兴趣,可以下载我修改好的源码来分析一下,甚至可以下载MVC4的源码来进行对比。