MVC3不能正确识别JSON中的Enum枚举值

一、背景

在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中的参数里的枚举就能被正确的识别到了。

 

四、研究

我觉得这应该是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 方法。

只贴部分关键代码了,通过bindingContextValueProvider 获得属性的相关信息,如果不等于null的话,转到执行BindSimpleModel 方法。

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的源码来进行对比。

 

六、后续

     感谢各位园友的支持,本文上了昨天博客园的首页推荐,也有很多园友给出一些非常有用的建议,特此贴出,以飨园友!

 

@eflay

引用干嘛不升级4。。。 话说MVC里的json转换都用json.net替换掉了。

dotnetgeek回复:文章中,已经提到可以升级MVC4了,但是我今天又发现了另外一个问题,在MVC3中,Dictionary也有问题,而且,升级到MVC4之后,如果Key为int类型的话,会报错。接下来我会新开一文说说这个问题。你可以做个例子试试看。

 

@JeffWong 
非常不错。之前用mvc3,发现里面的JsonResult用的是framework内置的JavaScriptSerializer,对时间处理也有bug

 

@双调

引用{"ID":9999,"Traffic":"1"} 就好了。
理论上number也要带上“,只是很多解析器做了处理。
不要把简单的东西复杂化。


dotnetgeek回复: 亲测确实可以,不过,你不觉得如果由前端传过来的数据没有带引号,就能令MVC内部报异常从而导致数据不正确,是一件很不妥的事情吗?另外,我昨天也发现Dictionary也会有这样的问题,并且前端传过来的数据是带上了双引号的,MVC绑定也不能正确的识别,并且在MVC4里面,如果Dictionary的Key如果为int类型的话,JSON(obj)序列化输出的时候,还会报错。

 

ps:关于Dictionary的问题

       关于Dictionary其实也有相关的问题,并且在MVC4中还有其他问题,这我应该会新开一文来解一下,这里做个提醒的是:

用Dictionary类型序列化之后的JSON格式字符串,提交给action,action是不认得的,经过在stackoverflow找到了解决方法,原来提交的时候,格式需要特殊处理一下,按照KeyValue键值对来的,而非序列化后的格式,各位注意了。

正确被识别Dictionary的JSON: { "ID":"9999","Traffic":"1","Dic":[{"Key":1,"Value":"xyz"},{"Key":2,"Value":42}] }

不能被识别Dictionary的JSON: { "ID":"9999","Traffic":"1","Dic":{"1":"xyz","2":"42"} }  但此格式又是被 JSON(obj) 序列化后的格式。

 

 

 

  

posted @ 2014-04-14 10:14  dotnetgeek  阅读(5542)  评论(27编辑  收藏  举报