• 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) 序列化后的格式。

      

  • 相关阅读:
    sqlplus中文问号
    mysql8.0 Authentication plugin 'caching_sha2_password' cannot be loaded
    Idea2018激活
    bzoj-5049-线段树
    HDU-6070-二分+线段树
    Aizu-2200-floyd+dp
    bzoj-4565-区间dp+状压
    bzoj-3195-状压dp
    bzoj-4870-组合dp+矩阵幂
    swiper使用心得
  • 原文地址:https://www.cnblogs.com/waynechan/p/3654612.html
Copyright © 2020-2023  润新知