Reputation: 4941
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
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