valverij
valverij

Reputation: 4941

RouteValueDictionary values changing from strings to arrays on POST

I have a RouteValueDictionary that is attached to an object being passed to a view. This object contains an array of other objects called SelectedItems that is used to populate a grid with check boxes that a user can select. Since this object is then passed back to the controller and routed differently depending on how the user interacts with the UI, it also contains a RouteValueDictionary called ReturnValues. The basic setup of the class looks more or less like this:

[Serializable]
public class ItemSelection
{
    public object[] SelectedItems { get; set; }
    public RouteValueDictionary ReturnValues { get; set; }

    public ItemSelection()
    {
        ReturnValues = new RouteValueDictionary()
        {
            { "key1", "routeValue1" },
            { "key2", "routeValue2" },
            { "key3", "routeValue3" }
        }
    }
}

This would have worked perfectly, except the ReturnValues property was coming back "null" in the POST

After reading this post regarding the mapping of dictionaries, I came up with the following solution on the view:

<%
    foreach(var key in Model.Keys)
    {
        Html.Render(Html.HiddenFor(m => m[key]));
    }
%>

This renders a bunch of hidden inputs to the html that look like this for each item:

<input id="ReturnValues__key1_" name="ReturnValues.[key1]" type="hidden" value="routeValue1">

I have also tried something similar to this post:

<%
    for (var i = 0; i < Model.Count; i++)
    {
        Html.Hidden("ReturnValues[" + i + "].Key", Model.ElementAt(i).Key);
        Html.Hidden("ReturnValues[" + i + "].Value", Model.ElementAt(i).Value);
    }
%>

Which renders this:

<input id="ReturnValues_ReturnValues_0__Key" name="ReturnValues.ReturnValues[0].Key" type="hidden" value="key1">
<input id="ReturnValues_ReturnValues_0__Value" name="ReturnValues.ReturnValues[0].Value" type="hidden" value="routeValue1">

Both of these work somewhat, except when the RouteValueDictionary gets posted back to the controller, the Values property goes from being a collection of Strings to a collection of String[1] (i.e., the strings become arrays). So, drawn out, the dictionary entries go from looking like this:

{ "key1", "routeValue1" }

To this:

{ "key1", { "routeValue1" } }

I've toyed around with it for hours, but I cannot seem to find out what the issue is. Since the code in my controller is routing to the ToString() of the associated value, my full route ends up looking like /area/controller/System.String[]/

Is there something I'm missing when mapping my RouteValueDictionary to hidden fields?

UPDATE: I ended up making a custom model binder for RouteValueDictionary objects. (see answer below)

Upvotes: 2

Views: 1909

Answers (1)

valverij
valverij

Reputation: 4941

I ended up making a custom model binder for RouteValueDictionary objects. Here are the relevant methods:

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
    {
        RouteValueDictionary baseModel = (bindingContext.Model as RouteValueDictionary) ?? (base.BindModel(controllerContext, bindingContext) as RouteValueDictionary);

        if (baseModel != null)
        {    // start off with a direct copy of the bound model            
            var returnModel = new RouteValueDictionary();
            baseModel.ForEach(x => returnModel[x.Key] = item.Value);

            // Sometimes the DefaultModelBinder turns the RouteValueDictionary.Values property into a collection of single-item arrays. This resets them to the correct values.
            foreach (var key in baseModel.Keys)
            {
                returnModel[key] = GetValue(bindingContext, key); // GetValue method body below                        
            }

            return returnModel;
        }
    }

    return null;
}

private object GetValue(ModelBindingContext context, string key)
{
    var name = String.Format("{0}[{1}]", String.IsNullOrEmpty(context.ModelName) ? String.Empty : context.ModelName, key);
    var attemptedResult = context.ValueProvider.GetValue(name);

    if (attemptedResult != null && attemptedResult.AttemptedValue != null)
    {
        return attemptedResult.AttemptedValue;        
    }

    return null;
}

Upvotes: 1

Related Questions