3

I've been successfully WebForms for AJAX calls with relatively complex set of parameters (called using jQuery.ajax). We're attempting to try using the same approach in MVC 3 but seem to be falling at the first hurdle with MVC failing to deserialize Dictionary arrays successfully.

The approach that works without issue in ASP.NET WebForms "classic" is below:

[WebMethod]
public static JQGrid.JQGridData GetListForJQGrid(int? iPageSize, int? iPage, int? iMaxRecords, string sSortField, string sSortOrder,
  Dictionary<string, string> dSearchOptions, Dictionary<string, object>[] aOriginalColumnDefinition, string[] aExtraDataColumns)

And below is the MVC 3 equivalent: (nb exactly the same name/parameters - different return type but I don't think that is relevant)

[HttpPost]
public JSONResult GetListForJQGrid(int? iPageSize, int? iPage, int? iMaxRecords, string sSortField, string sSortOrder,
  Dictionary<string, string> dSearchOptions, Dictionary<string, object>[] aOriginalColumnDefinition, string[] aExtraDataColumns)

With the WebMethod all the data deserializes perfectly. However, when the MVC method is called all the simple parameters deserialize fine but for some unknown reason the array of Dictionary's arrives as an array of nulls.

So, off the back of that a number of questions:

  • Has anyone else experienced problems with MVC 3 deserialization of arrays of dictionaries?
  • Does MVC 3 by default not use System.Web.Script.Serialization.JavaScriptSerializer which is I think what ASP.NET WebMethods use under the bonnet?
  • Can I force MVC 3 to use System.Web.Script.Serialization.JavaScriptSerializer instead of what it is using?
  • Or am I missing something / should my approach be slightly different? Please note that at least for now we'll need to share the client side code between classic ASP.NET WebMethods and MVC 3 and so we want that to remain as is if possible.
  • Finally, I can see there is a possible workaround that could be used looking at this question: POST json dictionary . Is this workaround the only game in town or have things improved since this question was posed?

jQuery AJAX call:

$.ajax(_oJQGProperties.sURL, //URL of WebService/PageMethod used
{
  data: JSON.stringify(oPostData),
  type: "POST",
  contentType: "application/json",
  complete: DataCallback
});

Example JSON.stringify(oPostData):

{
"dSearchOptions":{},
"aOriginalColumnDefinition":
[
{"name":"ID","sortable":false,"hidedlg":true,"align":"right","title":false,"width":40},
{"name":"URL","sortable":false,"hidedlg":true,"align":"left","title":false,"width":250,"link":"javascript:DoSummat(this,'{0}');","textfield":"Name"},
{"name":"Description","sortable":false,"hidedlg":true,"align":"left","title":false,"width":620}
],
"aExtraDataColumns":["Name"],
"_search":false,
"iPageSize":-1,
"iPage":1,
"sSortField":"",
"sSortOrder":"",
"iMaxRecords":0
}
Community
  • 1
  • 1
John Reilly
  • 5,791
  • 5
  • 38
  • 63

3 Answers3

1

I ran into this issue too. After finding this SO post, I thought about upgrading to MVC4, but it's too risky to do all at once in my environment so scratch that.

This link posted in Johnny Reilly's answer looked promising, but it required flattening my dictionary to a string. Because my MVC model is bidirectional (it's used for reads and writes), and I really wanted that dictionary structure I decided to pass on that too. It would have been a real pain to keep two properties for one value. I would have needed to add more tests, watch out for edge cases, etc.

Johnny's JsonValueProviderFactory link seemed promising too, but a bit arcane. I'm also not entirely comfortable monkeying around with a part of MVC like that. I had only a few hours to figure this problem out so I passed on this too.

Then I found this link somewhere, and thought "Yes! this is more like what I want!". In other words attack the model binding problem by using a custom binder. Replace the buggy one with something else, and use MVC's built-in capability to do so. Unfortunately, this did not work as my use case was List of T, and T was my model. This totally did not work with the sample. So I hacked away at it and ultimately failed.

Then, I got a lightbulb moment - JSON.NET does not have this problem. I use it all the time for doing all sorts of things, from cloning objects, to logging, to REST service endpoints. Why not model binding? So I ultimately ended up with this and my problem was solved. I think it should work with just about anything - I trust JSON.NET =)

/// <summary>
/// Custom binder that maps JSON data in the request body to a model class using JSON.NET.
/// </summary>
/// <typeparam name="T">Model type being bound</typeparam>
/// <remarks>
/// This binder is very useful when your MVC3 model contains dictionaries, something that it can't map (this is a known bug, fixed with MVC 4)
/// </remarks>
public class CustomJsonModelBinder<T> : DefaultModelBinder
    where T : class
{
    /// <summary>
    /// Binds the model by using the specified controller context and binding context.
    /// </summary>
    /// <returns>
    /// The bound object.
    /// </returns>
    /// <param name="controllerContext">The context within which the controller operates. The context information includes the controller, HTTP content, request context, and route data.</param><param name="bindingContext">The context within which the model is bound. The context includes information such as the model object, model name, model type, property filter, and value provider.</param><exception cref="T:System.ArgumentNullException">The <paramref name="bindingContext "/>parameter is null.</exception>
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        HttpRequestBase request = controllerContext.HttpContext.Request;
        request.InputStream.Position = 0;
        var input = new StreamReader(request.InputStream).ReadToEnd();
        T modelObject = JsonConvert.DeserializeObject<T>(input);
        return modelObject;
    }
}

To apply the binder, I added an attribute to my model parameter. This causes MVC3 to use my binder instead of the default. Something like this:

public ActionResult SomeAction(
    [ModelBinder(typeof(CustomJsonModelBinder<List<MyModel>>))] // This custom binder works around a known dictionary binding bug in MVC3
    List<MyModel> myModelList, int someId)
    {

One caveat - I was using POST with content type "application/json". If you're doing something like form or multipart data instead it will probably crash horribly.

killthrush
  • 4,859
  • 3
  • 35
  • 38
1

I don't have any experience with binding to a dictionary array, but one possible solution is to use a custom model binder. Scott Hanselman has a blog post on this subject that you might find useful: Splitting DateTime - Unit Testing ASP.NET MVC Custom Model.

Brian
  • 37,399
  • 24
  • 94
  • 109
1

Long time getting to update this but I thought I'd share where we got to. The problem turned out to be a bug - details of which can be found here:

Bug: http://connect.microsoft.com/VisualStudio/feedback/details/636647/make-jsonvalueproviderfactory-work-with-dictionary-types-in-asp-net-mvc

Workaround: POST json dictionary

We used the stated workaround which has been fine. I'm not too clear as to when the fix will be shipped and where exactly the bug lay. (Is it .NET dependant / MVC dependant etc) If anyone else knows I'd love to find out :-)

Update

I haven't heard still if this is shipped (I assume it goes out with MVC 4?) but in the interim this may be an alternative solution:

http://www.dalsoft.co.uk/blog/index.php/2012/01/10/asp-net-mvc-3-improved-jsonvalueproviderfactory-using-json-net/

Update 2

This has now been shipped as a fix with MVC 4. The issue remains unresolved in MVC 3 and so I've now written it up as a blog post here:

http://icanmakethiswork.blogspot.com/2012/10/mvc-3-meet-dictionary.html

Community
  • 1
  • 1
John Reilly
  • 5,791
  • 5
  • 38
  • 63