4

I have created the following simplified scenario to best describe my question. I have these two C# classes:

public class ClassOne
{
    public Dictionary<string, Guid> Dict { get; set; }
}

public class ClassTwo
{
    public Dictionary<Guid, string> Dict { get; set; }
}

I also have the following two actions:

    public ActionResult ActionOne(ClassOne model)
    {

        // do some work here...

        return RedirectToAction("Index");

    }

    public ActionResult ActionTwo(ClassTwo model)
    {

        // do some work here...

        return RedirectToAction("Index");

    }

And finally the following JavaScript:

        var MyObject = {}

        MyObject.Dict = {}

        MyObject.Dict["c5a9f7a4-312a-45bd-9fc6-1e41fcd89764"] = "c5a9f7a4-312a-45bd-9fc6-1e41fcd89764";
        MyObject.Dict["dc992a24-5613-4381-b199-7d4ebadb0635"] = "dc992a24-5613-4381-b199-7d4ebadb0635";
        MyObject.Dict["c01d8501-e121-4b2d-80c5-8305bcec7aff"] = "c01d8501-e121-4b2d-80c5-8305bcec7aff";

        $.ajax({
            url: $(this).attr("action"),
            type: "POST",
            data: JSON.stringify(MyObject),
            contentType: "application/json; charset=utf-8",
            beforeSend: function () {
                // do nothing
            },
            success: function (data) {
                // success
            },
            error: function (jqXHR, textStatus, errorThrown) {
                console.log(textStatus, errorThrown);
            }
        });

I have left out the JavaScript that routes the Ajax call to either ActionOne or ActionTwo to keep it simple. When the Ajax call is routed to ActionOne I get the following:

ActionOne

Which is what I expect. However when routing the Ajax call to ActionTwo (and therefore am expecting an instance of ClassTwo to have been created with it's Dictionary<Guid, string>) I get the following:

ActionTwo

Which is not what I am expecting. Am I missing something fundamental here? Why is the dictionary failing to be populated? Any guidance would be greatly appreciated. Thanks.

tereško
  • 58,060
  • 25
  • 98
  • 150
Luke Ellis
  • 195
  • 2
  • 9
  • call me curious, but if you stick jQuery.ajaxSettings.traditional = true; just before you do the post, does it work? – Dylan Corriveau Apr 29 '14 at 15:18
  • @DylanCorriveau I'm afraid not... – Luke Ellis Apr 29 '14 at 15:27
  • Have you tried stepping through the `DefaultModelBinder` class in System.Web.Mvc? I am assuming you don't have your own, custom model binder. It would appear that that class is having trouble with your dictionary. – Sven Grosen Apr 29 '14 at 16:05
  • I think the JS object that you are sending is not being parse properly. Since dictionary key value is type GUID, its may be failing to convert it automatically. I would suggest convert the key from string to GUID in constructor of the type. – qamar Apr 29 '14 at 16:44
  • Does it work if you use string instead of Guid in your data model? –  Apr 29 '14 at 17:04

1 Answers1

2

Alright, I've identified the problem, but don't have a good answer on how to fix this outside of doing your own custom model binding for this class that has a Dictionary<Guid, string> property (or at least doing custom binding on that specific property).

In DefaultModelBinder, in the case of either ClassOne or ClassTwo, it is correctly identifying it as containing a Dictionary<TKey, TValue> property and then falling into this if block inside of UpdateDictionary():

if (modelList.Count == 0)
{
    IEnumerableValueProvider enumerableValueProvider = bindingContext.ValueProvider as IEnumerableValueProvider;
    if (enumerableValueProvider != null)
    {
        IDictionary<string, string> keys = enumerableValueProvider.GetKeysFromPrefix(bindingContext.ModelName);
        foreach (var thisKey in keys)
        {
            modelList.Add(CreateEntryForModel(controllerContext, bindingContext, valueType, valueBinder, thisKey.Value, thisKey.Key));
        }
    }
}

// replace the original collection
object dictionary = bindingContext.Model;
CollectionHelpers.ReplaceDictionary(keyType, valueType, dictionary, modelList);
return dictionary;

The problem for ClassTwo comes in the call to CollectionHelpers.ReplaceDictionary(), which ultimately ends up calling this:

private static void ReplaceDictionaryImpl<TKey, TValue>(IDictionary<TKey, TValue> dictionary, IEnumerable<KeyValuePair<object, object>> newContents)
{
    dictionary.Clear();
    foreach (KeyValuePair<object, object> item in newContents)
    {
        if (item.Key is TKey)
        {
            // if the item was not a T, some conversion failed. the error message will be propagated,
            // but in the meanwhile we need to make a placeholder element in the dictionary.
            TKey castKey = (TKey)item.Key; // this cast shouldn't fail
            TValue castValue = (item.Value is TValue) ? (TValue)item.Value : default(TValue);
            dictionary[castKey] = castValue;
        }
    }
}

For ClassTwo, the keys in the newContents IEnumerable are of type string, and they cannot be implicitly converted to the Guid type, so each item fails the if (item.Key is TKey) check, and therefore nothing gets added to the dictionary.

This invaluable MSDN magazine article under the sub-heading "Where Model Binding Seems to Fall Down" lays out exactly why you are failing here as your JSON POST data falls victim to that (you are only getting into this code block because the binding context can't find items like Dict[0].key.

Here is how you need to format your JSON if you want the default model binder to recognize your types (i.e. if you post this, your ClassTwo instance will have three records in its Dict property):

"{"Dict[0].key": "c5a9f7a4-312a-45bd-9fc6-1e41fcd89764", "Dict[0].value": "c5a9f7a4-312a-45bd-9fc6-1e41fcd89764", "Dict[1].key": "dc992a24-5613-4381-b199-7d4ebadb0635", "Dict[1].value": "dc992a24-5613-4381-b199-7d4ebadb0635", "Dict[2].key": "c01d8501-e121-4b2d-80c5-8305bcec7aff", "Dict[2].value": "c01d8501-e121-4b2d-80c5-8305bcec7aff"}"

This works just fine for ClassOne because your key is already a string, and it can correctly figure out the type of the item (Guid). The problem is that for ClassTwo, since the key isn't a string (and isn't implicitly convertible to string), you never get any values in your dictionary.

If you don't want to try to massage your JSON into a format that the DefaultModelBinder can handle, I'd instead suggest creating a custom model binder for the Dictionary<Guid, string> type as outlined in this SO answer.

Community
  • 1
  • 1
Sven Grosen
  • 5,616
  • 3
  • 30
  • 52