Reputation: 1717
I have a controller action which is to receive an integer and an object, containing various properties, one of which is a generic list of objects. When I post JSON to the action with a populated list, everything maps correctly and I get a list containing the object that I have posted. If the array is empty however, the MVC action binds the property to a null intead of an empty list. I want the empty array to map to an empty array and not to a null, as the empty array in this case means that there is nothing in the collection, and a null means that the database should be checked to see if there is anything previously saved in the collection, but I can't figure out what I need to change to get it to map properly. We are using Json.Net to do object serialization for returning objects, but I don't think it's being used for object deserialization on model binding.
Objects being passed:
public class ObjectInList
{
public decimal Value1 { get; set; }
public decimal Value2 { get; set; }
}
public class Criteria
{
public decimal? ANullableNumber { get; set; }
public IList<ObjectInList> ObjectsList { get; set; }
}
Json request: "{\"id\":137,\"criteria\":{\"ObjectsList\":[]}}"
Controller Action:
public ActionResult ProcessCriteria(int id, Criteria criteria)
{
return Json(_service.ProcessCriteria(id, criteria));
}
It is in the controller action that I am getting a null instead of an empty list in the criteria object. It happens whether I send nulls for the other properties or not. Not sure if it's down to the object being an IList and not an IEnumerable? (The Json method wrapping the service call is our wrapper to return a json result using Json.Net to serialise the response - the null is in the criteria object received, not in the return.)
I'm guessing it's something pretty simple that I'm missing, but I can't work out what, any help greatly appreciated.
Upvotes: 26
Views: 11152
Reputation: 4346
I have an answer for you that will work at the framework level. In my project, I was working with data that was a bit larger than the default values would support. Thus, I created my own ValueProviderFactory. It turns out, if an array has no items in it, the provider skipped over that entry entirely. Instead, we just have to tell it that no items are in the array. Here is the code you will need.
First, the global.asax Application_Start:
public void Application_Start()
{
ValueProviderFactories.Factories.Remove(ValueProviderFactories.Factories.OfType<System.Web.Mvc.JsonValueProviderFactory>().FirstOrDefault());
ValueProviderFactories.Factories.Add(new LargeValueProviderFactory());
Second, here is the other class you will need:
using System;
using System.Collections.Generic;
using System.Collections;
using System.Web.Mvc;
using System.IO;
using System.Web.Script.Serialization;
using System.Globalization;
public sealed class LargeValueProviderFactory : System.Web.Mvc.ValueProviderFactory
{
public override System.Web.Mvc.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 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]);
}
if (l.Count == 0)
backingStore[prefix] = value;
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();
serializer.MaxJsonLength = Int32.MaxValue;
object jsonData = serializer.DeserializeObject(bodyText);
return jsonData;
}
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;
}
}
Upvotes: 1
Reputation: 5832
One way of resolving this issue is assigning a new instance as a default value for your ObjectsList
like this:
public class Criteria
{
public decimal? ANullableNumber { get; set; }
public IList<ObjectInList> ObjectsList { get; set; } = new List<ObjectInList>();
}
This will create an empty List
instead of null
if there's no values in your JSON array.
Upvotes: 3
Reputation: 2451
I would think the actual problem is in DefaultModelBinder.cs line 711 where it returns null if the built objectList
contains nothing. Check this out: https://lostechies.com/jimmybogard/2013/11/07/null-collectionsarrays-from-mvc-model-binding/
Upvotes: 1
Reputation: 755
This is because your never define nullable properties value in 'Criteria' class; if never define, it will be null.
for example:
public class Criteria {
public decimal? ANullableNumber { get; set; }
public IList<ObjectInList> ObjectsList { get; set; }
}
public class Criteria1 {
private IList<ObjectInList> _ls;
private decimal? _num;
public decimal? ANullableNumber {
get {
if (_num == null) return 0;
return _num;
}
set {
_num = value;
}
}
public IList<ObjectInList> ObjectsList {
get {
if (_ls == null) _ls = new List<ObjectInList>();
return _ls;
}
set {
_ls = value;
}
}
}
public class HomeController : Controller {
public ActionResult Index() {
var dd = new Criteria();
return Json(dd); //output: {"ANullableNumber":null,"ObjectsList":null}
}
public ActionResult Index1() {
var dd = new Criteria1();
return Json(dd); //output: {"ANullableNumber":0,"ObjectsList":[]}
}
}
Upvotes: -2
Reputation: 2298
ok, i was facing this issue almost 5 hours trying find the solution then i found myself looking in the MVC source code. and i found that this is a problem with the Mvc Source code in System.Web.Mvc.ValueProviderResult at Line 173:
else if (valueAsArray != null)
{
// case 3: destination type is single element but source is array, so extract first element + convert
if (valueAsArray.Length > 0)
{
value = valueAsArray.GetValue(0);
return ConvertSimpleType(culture, value, destinationType);
}
else
{
// case 3(a): source is empty array, so can't perform conversion
return null;
}
}
as you can see if source is empty array it will return null.
so i have to find a way around it, and then i remember how in the good old days we was doing deserialization: this is how you will get what you want:
public ActionResult ProcessCriteria(int id, Criteria criteria)
{
var ser = new System.Web.Script.Serialization.JavaScriptSerializer();
StreamReader reader = new StreamReader(System.Web.HttpContext.Current.Request.InputStream);
reader.BaseStream.Position = 0;
criteria = ser.Deserialize<Criteria>(reader.ReadToEnd());
return Json(_service.ProcessCriteria(id, criteria));
}
Upvotes: 13
Reputation: 4180
Here is what I posted as a comment:
public class Criteria
{
public decimal? ANullableNumber { get; set; }
private IList<ObjectInList> _objectsList = new List<ObjectInList>();
public IList<ObjectInList> ObjectsList
{
get { return _objectsList; }
set {
if(value != null)
_objectsList = value;
}
}
}
Upvotes: 0