Alexandre Severino Alexandre Severino - 5 months ago 9
jQuery Question

Cannot post complex object

I have the function on my view:

function setMessengerState() {
var serviceURL = $("#messenger-set-state-url").val();

var data = {
messengerState: g_messengerState,
};

$.ajax({
type: "POST",
url: serviceURL,
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: successFunc,
error: errorFunc
});

function successFunc(data, status) {
console.log("saved");
}

function errorFunc(data, status) {
console.log("failed?");
}
}


This is how the
JSON.stringify(data)
is formated on Chrome debugger:

"{"messengerState":{"IsOpen":true,"ConversationStates":[{"PartnerId":"64c71990-9ddc-4967-8821-a8e5936560a3","IsEnabled":true},{"PartnerId":"64c71990-9ddc-4967-8821-a8e5936560a3","IsEnabled":true}]}}"


And this is the value of
$("#messenger-set-state-url").val()
:

"/Messenger/Messenger/SetMessengerState"


The controller method:

[HttpPost]
public async Task<ActionResult> SetMessengerState(MessengerStateInfo messengerState)
{
var user = User.ApplicationUser();

if (user == null)
return null;

bool success = await MvcApplication.Messenger.SetState(user, messengerState) != null;

return Json(success, JsonRequestBehavior.DenyGet);
}


And, finally, this is
MessengerStateInfo
and
ConversationStateInfo
:

public class MessengerStateInfo
{
public bool IsOpen { get; set; }
public ICollection<ConversationStateInfo> ConversationStates { get; set; }

public MessengerStateInfo()
{
ConversationStates = new ConversationStateInfo[0];
}
}

public class ConversationStateInfo
{
public string PartnerId { get; set; }
public bool IsEnabled { get; set; }
}


I cannot find where I'm doing it wrong. The post never gets to the controller method. I tried before with basic (
string
) parameters and it works just fine, but it simply doesn't get through with complex objects.

Answer

Thank you for writing a detailed question that allows reproducing the problem.

The model binder expects your ICollection to be writable, but arrays are not writable in this manner. Do a simple experiment:

ICollection<int> a = new int[0];
a.Clear();

A "Collection is read-only" exception will be thrown.

Now, how do you fix this. Change your MessengerStateInfo class definition to the following:

public class MessengerStateInfo
{
    public bool IsOpen { get; set; }
    public ICollection<ConversationStateInfo> ConversationStates { get; set; }
}

Here we removed the constructor, which allows the model binder to create a new instance of List<> type. This one, of course will be read-write and the binding succeeds.

Here are relevant code snippets from the model binder source code. This one is from System.Web.ModelBinding.CollectionModelBinder<TElement> class:

protected virtual bool CreateOrReplaceCollection(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext, IList<TElement> newCollection)
{
    CollectionModelBinderUtil.CreateOrReplaceCollection<TElement>(bindingContext, newCollection, () => new List<TElement>());
    return true;
}

And this one is one is from System.Web.ModelBinding.CollectionModelBinderUtil:

public static void CreateOrReplaceCollection<TElement>(ModelBindingContext bindingContext, IEnumerable<TElement> incomingElements, Func<ICollection<TElement>> creator)
{
    ICollection<TElement> model = bindingContext.Model as ICollection<TElement>;
    if ((model == null) || model.IsReadOnly)
    {
        model = creator();
        bindingContext.Model = model;
    }
    model.Clear();
    foreach (TElement local in incomingElements)
    {
        model.Add(local);
    }
}

As you can clearly see from this code, if the collection not empty Clear method is called on it, which, in your case leads to an exception. If, on the other hand the collection is null, new List<> is executed which results in a brand new (writable) object.

Note: implementation details may differ depending on software version. The code above may differ from the actual code in the version of the library that you are using. The principle remains the same though.

Here is a tip how to find the reason faster than typing the question to Stackoverflow.

Put a breakpoint on public async Task<ActionResult> SetMessengerState(MessengerStateInfo messengerState) and observe that the breakpoint is not hit. Open chrome console and take notion of the error icon.

enter image description here

Click on the icon to see the error message at the bottom. Now click on then link in the error message, you'll see this screen:

enter image description here

Finally click on the request in the "Name" column. You will see this:

enter image description here

This gets you the actual error message with the stack trace. In most cases you will be able to tell what's wrong from this message. In this particular case you will immediately see that an array is tried to be cleared and fails.

Comments