MVC 3 doesn't bind nullable long

邮差的信 提交于 2019-12-02 22:19:47

I created a testproject just to test this. I put your code into my HomeController and added this to index.cshtml:

<script type="text/javascript">
    $(function () {
        $.post('Home/GetData', { "TestString": "test", "TestLong": 12345, "TestInt": 123 });
    });
</script>

I put a breakpoint in the GetData method, and the values were binded to the model like they should:

So I think there's something wrong with the way you send the values. Are you sure the "TestLong" value is actually sent over the wire? You can check this using Fiddler.

If you don't want to go with Regex and you only care about fixing long?, the following will also fix the problem:

public class JsonModelBinder : DefaultModelBinder {     
  public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)  
  {
        var propertyType = propertyDescriptor.PropertyType;
        if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
            var provider = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (provider != null 
                && provider.RawValue != null 
                && Type.GetTypeCode(provider.RawValue.GetType()) == TypeCode.Int32) 
            {
                var value = new System.Web.Script.Serialization.JavaScriptSerializer().Deserialize(provider.AttemptedValue, bindingContext.ModelMetadata.ModelType);
                return value;
            }
        } 

        return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
  }
}
Edgar

My colleague came up with a workaround for this. The solution is to take the input stream and use a Regex to wrap all numeric variables in quotes to trick the JavaScriptSerializer into deserialising the longs properly. It's not a perfect solution, but it takes care of the issue.

This is done in a custom model binder. I used Posting JSON Data to ASP.NET MVC as an example. You have to take care, though, if the input stream is accessed anywhere else.

public class JsonModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (!IsJSONRequest(controllerContext))
            return base.BindModel(controllerContext, bindingContext);

        // Get the JSON data that's been posted
        var jsonStringData = new StreamReader(controllerContext.HttpContext.Request.InputStream).ReadToEnd();

        // Wrap numerics
        jsonStringData = Regex.Replace(jsonStringData, @"(?<=:)\s{0,4}(?<num>[\d\.]+)\s{0,4}(?=[,|\]|\}]+)", "\"${num}\"");

        // Use the built-in serializer to do the work for us
        return new JavaScriptSerializer().Deserialize(jsonStringData, bindingContext.ModelMetadata.ModelType);
    }

    private static bool IsJSONRequest(ControllerContext controllerContext)
    {
        var contentType = controllerContext.HttpContext.Request.ContentType;
        return contentType.Contains("application/json");
    }
}

Then put this in the Global:

ModelBinders.Binders.DefaultBinder = new JsonModelBinder();

Now the long gets bound successfully. I would call this a bug in the JavaScriptSerializer. Also note that arrays of longs or nullable longs get bound just fine without the quotes.

You can use this model binder class

public class LongModelBinder : IModelBinder
{
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (string.IsNullOrEmpty(valueResult.AttemptedValue))
        {
            return (long?)null;
        }
        var modelState = new ModelState { Value = valueResult };
        object actualValue = null;
        try
        {
            actualValue = Convert.ToInt64(
                valueResult.AttemptedValue,
                CultureInfo.InvariantCulture
            );
        }
        catch (FormatException e)
        {
            modelState.Errors.Add(e);
        }
        bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
        return actualValue;
    }
}

In Global.asax Application_Start add these lines

ModelBinders.Binders.Add(typeof(long), new LongModelBinder());
ModelBinders.Binders.Add(typeof(long?), new LongModelBinder());

I wanted to incorporate the solution presented by Edgar but still have the features of the DefaultModelBinder. So instead of creating a new model binder I went with a different approach and replaced the JsonValueProviderFactory with a custom one. There's only a minor change in the code from the original MVC3 source code:

public sealed class NumericJsonValueProviderFactory : ValueProviderFactory
{

    private static void AddToBackingStore(Dictionary<string, object> backingStore, string prefix, object value)
    {
        IDictionary<string, object> d = value as IDictionary<string, object>;
        if (d != null)
        {
            foreach (KeyValuePair<string, object> entry in d)
            {
                AddToBackingStore(backingStore, MakePropertyKey(prefix, entry.Key), entry.Value);
            }
            return;
        }

        IList l = value as IList;
        if (l != null)
        {
            for (int i = 0; i < l.Count; i++)
            {
                AddToBackingStore(backingStore, MakeArrayKey(prefix, i), l[i]);
            }
            return;
        }

        // primitive
        backingStore[prefix] = value;
    }

    private static object GetDeserializedObject(ControllerContext controllerContext)
    {
        if (!controllerContext.HttpContext.Request.ContentType.StartsWith("application/json", StringComparison.OrdinalIgnoreCase))
        {
            // not JSON request
            return null;
        }

        StreamReader reader = new StreamReader(controllerContext.HttpContext.Request.InputStream);
        string bodyText = reader.ReadToEnd();
        if (String.IsNullOrEmpty(bodyText))
        {
            // no JSON data
            return null;
        }

        JavaScriptSerializer serializer = new JavaScriptSerializer();

        // below is the code that Edgar proposed and the only change to original source code
        bodyText = Regex.Replace(bodyText, @"(?<=:)\s{0,4}(?<num>[\d\.]+)\s{0,4}(?=[,|\]|\}]+)", "\"${num}\""); 

        object jsonData = serializer.DeserializeObject(bodyText);
        return jsonData;
    }

    public override IValueProvider GetValueProvider(ControllerContext controllerContext)
    {
        if (controllerContext == null)
        {
            throw new ArgumentNullException("controllerContext");
        }

        object jsonData = GetDeserializedObject(controllerContext);
        if (jsonData == null)
        {
            return null;
        }

        Dictionary<string, object> backingStore = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        AddToBackingStore(backingStore, String.Empty, jsonData);
        return new DictionaryValueProvider<object>(backingStore, CultureInfo.CurrentCulture);
    }

    private static string MakeArrayKey(string prefix, int index)
    {
        return prefix + "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
    }

    private static string MakePropertyKey(string prefix, string propertyName)
    {
        return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
    }
}

Then to register the new value provider you need to add the following lines to your Global.asax:

ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new NumericJsonValueProviderFactory());
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!