Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MVC 3 doesn't bind nullable long

I made a test website to debug an issue I'm having, and it appears that either I'm passing in the JSON data wrong or MVC just can't bind nullable longs. I'm using the latest MVC 3 release, of course.

public class GetDataModel
{
    public string TestString { get; set; }
    public long? TestLong { get; set; }
    public int? TestInt { get; set; }
}

[HttpPost]
public ActionResult GetData(GetDataModel model)
{
    // Do stuff
}

I'm posting a JSON string with the correct JSON content type:

{ "TestString":"test", "TestLong":12345, "TestInt":123 }

The long isn't bound, it's always null. It works if I put the value in quotes, but I shouldn't have to do that, should I? Do I need to have a custom model binder for that value?

like image 422
Edgar Avatar asked May 19 '11 15:05

Edgar


4 Answers

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:

enter image description here

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.

like image 74
fretje Avatar answered Nov 08 '22 19:11

fretje


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);
  }
}
like image 31
Daniel Avatar answered Nov 08 '22 20:11

Daniel


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.

like image 2
Edgar Avatar answered Nov 08 '22 20:11

Edgar


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());
like image 2
user1080381 Avatar answered Nov 08 '22 18:11

user1080381