locked
JsonValueProviderFactory RRS feed

  • Question

  • User479730335 posted

    I have a situation where I am sending JSON to an action and using the JsonValueProviderFactory to bind the JSON to my contract.  For some reason, enum values are not being bound when the model is deserialized.

    The contract (action parameter) is a generic that looks something like the following...

    public class Request<ContractType>
    {
    public string UserName{get;set;}
    public string Password {get;set;}
    public ContractType Data {get;set;}

    public class LinkContract
    {
    public MyEnum MyEnumProperty {get;set;}

    ... and so the action is defined as ...

     [HttpPost]
    public JsonResult CreateLink(Request<LinkContract> request)
    {
    ...


    For the most part this is working great, except that any enum properties are not populated correctly after the default model binder runs.  I created a custom model binder and checked the DictionaryValueProvider in the bindingContext and it looks like it contains the correct property name and values for enums, but after the binder runs, the enums are all just set to 0 in the contract.


    Any thoughts as to what I did that's getting in the way?

    Friday, November 12, 2010 2:21 PM

Answers

  • User479730335 posted

    And to sum this all up, this little change to DefaultModelBinder seems to get everything working for me.

    public class EnumConverterModelBinder : DefaultModelBinder
    {
    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.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);
    }
    }


    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);
    }
    }


    Basically, if the model's property is an enum and the value in the value provider is not, convert it.  Throw some extra error handling around this and you've got our code.  There's probably a better solution involving getting the correct value in the value provider in the first place, but Reflector doesn't seem to want to run for me so I can't dig into it and this works well enough for now.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Monday, November 15, 2010 1:58 PM

All replies

  • User-1620313041 posted

    Maybe your problem is the type herarchy in the Action side, that probabley doesn't match the herarchy of the View side. Specifically you have the following prefixes:

     

    Username

    Password

    Data.MyEnumProperty

     

    This means your enum comes after a first Data prefix. Maybe that if you use a Request<MyEnum> instead of a Request<LinkContract> you delete the Data prefix of your MyEnumProperty and the two herarchy now match.

     

    Friday, November 12, 2010 5:29 PM
  • User479730335 posted

    Thanks for the thoughts on the code.

    Though it looks like I oversimplified my example a little bit.  Sorry about that.  Never know when too much information is just confusing the issue.

    Anyway, I don't think your suggestion is going to fix my problem.  My LinkContract class has properties in it other than just the enumeration value.  Those other properties are successfully populated when I receive the Link contract in the Action method.  A more complete example would be...

    [DataContract]
    public class LinkContract
    {
    [DataMember]
    public MyEnum MyEnumProperty {get;set;}

    [DataMember]
    public string Name {get;set;}

    [DataMember]
    public string Location {get;set;}

    }


    A more complete view of the process is that while in some cases I will be using JQuery to connect to the web services, other times we will be connecting from compiled code.  I haven't tested the former for this service yet, the latter case is where the failure is happening.  I am using the same contract dll file on both sides of the application.  On the client side I have tried using both DataContractJsonSerializer and JavaScriptSerializer to serialize the contract with the same results.  All of the values on the LinkContract are populated except for any enum values.

    I have also tried changing the value in the DictionaryValueProvider from the int value of the enum to the string value of the enum without affect.  This makes me think that enums aren't handled correctly in this crazy setup I've got here.  But I haven't confirmed that yet.

    I have seen some talk on the web suggesting that I could use the JavaScriptSerializer to modify how enums are handled, but unless I want to write my own model binder for MVC, I'm not sure that this helps me, or is even necessary.


    Francesco, your answer gave me some ideas though.  I'm going to simplify the contract and see if I can get the enum value through at all.  I'm pretty sure I've done that before.  Here's hoping.

    Monday, November 15, 2010 11:33 AM
  • User479730335 posted

    What I've just found out is that if I push the correct enum value into the DictionaryValueProvider, the LinkContract is populated correctly by the DefaultModelBinder.

    If I had to take a guess, I'd say that the JsonValueProviderFactory is not quite in sync with DefaultModelBinder yet.  That is, the value provider is supplying integers for enums instead of enum types.

    Guess I'll try extending one or the other of these for now.


    Thanks for reading.

    Monday, November 15, 2010 11:49 AM
  • User479730335 posted

    And to sum this all up, this little change to DefaultModelBinder seems to get everything working for me.

    public class EnumConverterModelBinder : DefaultModelBinder
    {
    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.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);
    }
    }


    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);
    }
    }


    Basically, if the model's property is an enum and the value in the value provider is not, convert it.  Throw some extra error handling around this and you've got our code.  There's probably a better solution involving getting the correct value in the value provider in the first place, but Reflector doesn't seem to want to run for me so I can't dig into it and this works well enough for now.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Monday, November 15, 2010 1:58 PM
  • User-347312995 posted

    Where did you make this change? I only have Microsoft.Web.Mvc ..where is the source for this?

    Saturday, November 20, 2010 7:39 PM
  • User479730335 posted

    Here's a summary of the problem and solution.  

    I ran into this problem when I found a solution to sending JSON to MVC Actions in this post.  The post describes the simple steps to hook it up and how to pass JSON to an action.  The JsonValueProviderFactory is included in the MVC Futures library.  

    The problem is that the JsonValueProvider does not create enum values in the value dictionary (it creates ints) and the default model binder doesn't seem to know what to do when ints are provided for enum values.  Probably for performance reasons.

    The EnumConverterModelBinder code I posted is a custom class I created that checks if the property being bound to is an Enum and the data in the value dictionary is not.  In this case only, it converts the dictionary value and returns it to be populated in the model.  If those conditions are not met, the default implementation is used.  I tried to do this in a way that minimizes the performance impact on all other property types.

    You can set the default model binder in Global.asax.  This is from memory at the moment so it may not be 100% correct, but in the Application_Start method, add the following line.

    Models.DefaultBinder = new EnumConverterModelBinder()



    Hope that helps

    Tuesday, November 23, 2010 7:38 AM