rjacobsen0
rjacobsen0

Reputation: 1455

HTML Helper for model binding to a dictionary

I am writing a general purpose html helper that passes a dictionary back to my controller. (Perhaps this already exists?)

Controller:

    [HttpPost]
    public ActionResult Odometer(IDictionary<String, int> Odometers, String UserInputInJson)
    {
        ...
    }

and here is the helper as it stands now. It is working without the name (commented out).

Helper:

    public static MvcHtmlString EditorToSubmitDictionaryEntryFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> item, String key)
    {
        var url = new UrlHelper(helper.ViewContext.RequestContext);
        ModelMetadata metadata = ModelMetadata.FromLambdaExpression(item, helper.ViewData);
        string value = metadata.Model.ToString();
        // string name = metadata.PropertyName + "s"; // make it plural
        Type type = typeof(TProperty);

        // build the first hidden Index tag: <input name="Index" id="Index" type="hidden" value="1D6H983G3DL784930"/>
        var indexAnchorBuilder = new TagBuilder("input");
        indexAnchorBuilder.MergeAttribute("name", "Index");
        indexAnchorBuilder.MergeAttribute("id", "Index");
        indexAnchorBuilder.MergeAttribute("type", "hidden");
        indexAnchorBuilder.MergeAttribute("value", key);
        string indexAnchorHtml = indexAnchorBuilder.ToString(TagRenderMode.Normal);

        // <input name="[1D6H983G3DL784930].Value" class="text-box single-line" type="number" value="12345"/>
        // <input name="Odometers[1D6H983G3DL784930].Value" class="text-box single-line" id="Odometers[1D6H983G3DL784930].Value" type="number" value="12345"/>
        var valueAnchorBuilder = new TagBuilder("input");
        valueAnchorBuilder.MergeAttribute("name", /*name +*/ "["+ key +"].Value");
        valueAnchorBuilder.MergeAttribute("id", /*name +*/ "[" + key + "].Value");
        valueAnchorBuilder.MergeAttribute("class", "text-box single-line");
        if (type == typeof(int) || type == typeof(int?) || type == typeof(double) || type == typeof(double?) || type == typeof(decimal) || type == typeof(decimal?) || type == typeof(float) || type == typeof(float?))
            valueAnchorBuilder.MergeAttribute("type", "number");
        else
            valueAnchorBuilder.MergeAttribute("type", "text");
        valueAnchorBuilder.MergeAttribute("value", value);
        string valueAnchorHtml = valueAnchorBuilder.ToString(TagRenderMode.Normal);

        // <input name="[1D6H983G3DL784930].Key" id="[1D6H983G3DL784930].Key" type="hidden" value="1D6H983G3DL784930"></input>"
        // <input name="Odometers[1D6H983G3DL784930].Key" id="Odometers[1D6H983G3DL784930].Key" type="hidden" value="1D6H983G3DL784930"></input>"
        var keyAnchorBuilder = new TagBuilder("input");
        keyAnchorBuilder.MergeAttribute("name", /*name +*/ "[" + key + "].Key");
        keyAnchorBuilder.MergeAttribute("id", /*name +*/ "[" + key + "].Key");
        keyAnchorBuilder.MergeAttribute("type", "hidden");
        keyAnchorBuilder.MergeAttribute("value", key);
        string keyAnchorHtml = keyAnchorBuilder.ToString(TagRenderMode.Normal);

        return MvcHtmlString.Create(indexAnchorHtml + valueAnchorHtml + keyAnchorHtml);
    }

And here's how I'm calling this helper... in a view and a partial view.

@using (Html.BeginForm("Odometer", "DEQToo", FormMethod.Post))
{
//this is here so we can keep track of what the user searched for, in case the session times out and we have to re-build the page.
@Html.HiddenFor(model => model.UserInputInJson)
<table class="footable table">
    <thead>
        <tr>
            <th>@Html.DisplayNameFor(model => model.vCollect[""].Odometer)</th>
            <th data-hide="phone">@Html.DisplayNameFor(model => model.vCollect[""].VIN)</th>
            <th data-hide="phone,tablet" data-type="numeric">@Html.DisplayNameFor(model => model.vCollect[""].VehicleYear)</th>
            <th data-hide="phone">@Html.DisplayNameFor(model => model.vCollect[""].Make)</th>
            <th data-hide="phone,tablet">@Html.DisplayNameFor(model => model.vCollect[""].Model)</th>
        </tr>
    </thead>
    <tbody>
        @if (Model != null)
        {
            foreach (VehicleWithMostRecentTestOutput v in Model)
            {
                @Html.Partial("_OdometerRowPartial", v)
            }
        }
  </tbody>
</table>
<input type="submit" value="@DEQTooResource.resCorrectOdometer" id="OdometerListSubmit" />
}

And the partial view where I call EditorToSubmitDictionaryEntryFor():

<tr>
    <td>@Html.EditorToSubmitDictionaryEntryFor(model => model.Odometer, Model.VIN)</td>
    <td>@Html.DisplayFor(model => model.VIN) </td>
    <td>@Html.DisplayFor(model => model.VehicleYear)</td>
    <td>@Html.DisplayFor(model => model.Make)</td>
    <td>@Html.DisplayFor(model => model.Model)</td>
</tr>

I want to use the name so it will be more general purpose and I can have more than one of these on a page. When I un-comment name, adding it to the name and id attributes of both the key and value, then model state is invalid and I don't get user input passed in to the controller.

Can anyone explain why this is happening and give suggestions of what to do about it? Any help would be appreciated.

Upvotes: 2

Views: 521

Answers (1)

James P
James P

Reputation: 2256

You just require something like:

valueAnchorBuilder.MergeAttribute("id", id + "["+ key +"]");

You just want Odometers[1D6H983G3DL784930] without the .Value, not Odometers[1D6H983G3DL784930].Value

The Item property is another name for the indexer, so you can omit its name when accessing elements.

Upvotes: 1

Related Questions