0

I've been getting a weird binding issue trying to send list data to an MVC4 controller in an ajax post request via JSON payload.

The payload we're sending is

{
   "assignmentId":"AssignmentId2",
   "shiftId":null,
   "startDate":null,
   "startTime":{
      "hours":0,
      "minutes":0
   },
   "endTime":{
      "hours":0,
      "minutes":0
   },
   "breaksDuration":{
      "hours":0,
      "minutes":0
   },
   "authorised":false,
   "authorisedName":null,
   "mileageDescription":null,
   "mileage":0,
   "expenses":[
      {
         "description":"DADADDAADADADAD",
         "total":"5"
      }
   ],
   "billableDuration":{
      "hours":0,
      "minutes":0
   },
   "expensesComplete":true,
   "expensesTotal":5
}

The expenses list items are not getting bound to the following model structure

public class ShiftApiModel
    {
        public string assignmentId { get; set; }
        public string shiftId { get; set; }
        [Required]
        public DateTime startDate { get; set; }
        [Required]
        public ShortTimeSpan startTime { get; set; }
        [Required]
        public ShortTimeSpan endTime { get; set; }
        public bool authorised { get; set; }
        public string authorisedName { get; set; }
        public ShortTimeSpan breaksDuration { get; set; }
        public decimal mileage { get; set; }
        public string mileageDescription { get; set; }

        private IList<ExpenseApiModel> _expenses = new List<ExpenseApiModel>();
        public IList<ExpenseApiModel> expenses { get { return _expenses; } set { _expenses = value; } }
    }

public class ExpenseApiModel
{
    public string description { get; set; }
    public double total { get; set; }
}

The actual ajax request is as follows:

 $.ajax({
    type: type,
    url: serviceUrl,
    dataType: 'json',
    contentType: 'application/json; charset=utf-8',
    data: (props.data) ? props.data : null,
    success: function (jqXHR, textStatus) {
    this.serviceCallComplete(jqXHR, props.complete, props.error);
    }.bind(this),
    error: function (jqXHR, textStatus, errorThrown) {
    this.serviceCallFailure(jqXHR, props.error);
    }.bind(this)
});

Where props.data is the JSON payload described above.

I've been scratching my head on this and can't see any obvious reason as to why the expenses item wouldn't be getting bound.

Any ideas/ suggestions ?

ActualAl
  • 1,192
  • 10
  • 13

2 Answers2

1

You can't bind to an interface. Use List versus IList:

private List<ExpenseApiModel> _expenses = new List<ExpenseApiModel>();
public List<ExpenseApiModel> expenses { get { return _expenses; } set { _expenses = value; } }
viperguynaz
  • 12,044
  • 4
  • 30
  • 41
  • Thanks for reply Viperguynaz - I thought that myself but I tested in a clean project and still get the same issue. It's looking like an issue with MVC4 or at the very least our install of it. – ActualAl Jan 11 '13 at 17:13
  • Try using a simple array of ExpenseApiModel - and this SO post http://stackoverflow.com/questions/9914614/asp-net-mvc4-wont-properly-deserialize-and-bind-dictionarystring-listcustomty – viperguynaz Jan 11 '13 at 17:42
0

I've had a very similar problem to yours with MVC4 model binding. I have a solution, but without access to the model binder source, I can only speculate on the answer. Probably the solution to your problem is changing the names of "expensesComplete" and "expensesTotal" to something else not starting with "expenses".

My model (yes you can bind to an interface, replacing the IEnumerable with a list or array makes no difference, I see the model binder actually just sticks a List in here) & action stripped down to their barest form:

[Serializable]
public class InvolvedPartyDetails
{
    public long? Key { get; set; }
}

[Serializable]
public class IncidentDetails
{
    public long IncidentNo { get; set; }
    public string InvDisp { get; set; }
    public string ChangeDetails { get; set; }
    public IEnumerable<InvolvedPartyDetails> Inv { get; set; }
}

[HttpPost]
public ActionResult SubmitData(IncidentDetails incident)
{
...

If I submit the following JSON (header included for info, will be the same from here on in). Also JSON put on multiple lines for clarity:

POST http://johnapi.com/AngularTest/SubmitData HTTP/1.1
Accept: application/json, text/javascript, */*; q=0.01
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: http://johnapi.com/AngularTest/MVCCrazy/
Accept-Language: en-GB,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; MDDCJS; rv:11.0) like Gecko
Host: johnapi.com
Content-Length: 72
DNT: 1
Connection: Keep-Alive
Pragma: no-cache

{"IncidentNo":0,
"InvDisp":"Boo",
"ChangeDetails":"COD",
"Inv":[{"Key":0}]}

incident.Inv would be null.

While there were many confusing side effects, I found that re-naming the InvDisp property in the IncidentDetails model so as not to start with Inv solved the problem.

The following JSON, with InvDisp renames to InxDisp in the model resulted in a list of 1 element for incident.Inv:

{"IncidentNo":0,
"InxDisp":"Boo",
"ChangeDetails":"COD",
"Inv":[{"Key":0}]}

An example of confusing side effects is that I could remove the ChangeDetails property from the IncidentDetails class and add a couple of properties to the InvolvedPartyDetails class and it would suddenly start working, e.g. the following JSON would give me incident.Inv with a single item:

{"IncidentNo":0,
"InvDisp":"Boo",
"Inv":[{"LinkType":null,"Key":0,"Name":null}]} 

But removing either property in the InvolvedPartyDetails or putting back the ChangeDetails would again prevent binding, equally, sometimes sending more than one InvolvedPartyDetails in the array would cause things to work, dependent on the other properties present.

However, the single robust solution is the naming.

JohnB
  • 451
  • 5
  • 7