Handling array's in post back data - MVC3

2019-05-24 13:28发布

问题:

I'm current a WebForms developer that is trying to move to MVC. I'm super excited about MVC and I'm really have fun but I'm running into a weird issue. So what I'm trying to do is create an advanced editor for a "widget". I've posted the code below.

Everything appears to work fine when you add the first 4-5 items but the problem occurs when you delete the 2nd item. Here is a visual example.

First add the 4 values.

But the problem occurs when we delete the 2nd value. We end up with this...

What I cannot seem to understand is why does this property act different between the two following lines of code.

@Model.Values[i]
@Html.TextBoxFor(m => m.Values[i])

My guess is that the @Model and (m =>m) do not reference the same object?

Here is my widget class.

public class Widget
{
    #region Constructor

    public Widget()
    {
        ID = 0;
        Name = string.Empty;
        Values = new List<string>();
    }

    #endregion

    #region Properties

    [Required]
    [Display(Name = "ID")]
    public int ID { get; set; }

    [Required]
    [Display(Name = "Name")]
    public string Name { get; set; }

    [Required]
    [Display(Name = "Values")]
    public List<string> Values { get; set; }

    #endregion
}

My controller looks like this.

public ViewResult EditWidget(int id)
{
    return View(_widgets.GetWidgetByID(id));
}

[HttpPost]
public ActionResult EditWidget(Widget widget)
{
    if (!TryUpdateModel(widget))
    {
        ViewBag.Message = "Error...";
        return View(widget);
    }

    if (Request.Form["AddWidgetValue"] != null)
    {
        widget.Values.Add(Request.Form["TextBoxWidgetValue"]);
        return View("EditWidget", widget);
    }

    if (Request.Form["DeleteWidgetValue"] != null)
    {
        widget.Values.Remove(Request.Form["ListBoxWidgetValues"]);
        return View("EditWidget", widget);
    }

    _widgets.UpdateWidget(widget);
    _widgets.Save();

    return RedirectToAction("Index");
}

And finally my view.

@model MvcTestApplication.Models.Widget

@{
    ViewBag.Title = "EditWidget";
}

<h2>EditWidget</h2>

<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Widget</legend>

        @Html.HiddenFor(model => model.ID)

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>

        @for (var i = 0; i < Model.Values.Count; i++)
        {
            @Model.Values[i]
            @Html.TextBoxFor(m => m.Values[i])
            @Html.HiddenFor(m => m.Values[i])
            <br />
        }

        @Html.ListBox("ListBoxWidgetValues", new SelectList(Model.Values), new { style = "width: 100%" })<br />
        @Html.TextBox("TextBoxWidgetValue", string.Empty, new { style = "width: 100%" })
        <input type="submit" value="Add" id="AddWidgetValue" name="AddWidgetValue" class="submitButton" />
        <input type="submit" value="Delete" id="DeleteWidgetValue" name="DeleteWidgetValue" class="submitButton" />

        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

回答1:

The reason for this happening is because HTML helpers first look at ModelState posted values when binding and then in model. This means that if inside your POST controller action you try to modify some value and this same value was part of the initial post request the HTML helper will use the initial value and not the one you modified.

For example in your EditWidget action you are doing this:

if (Request.Form["DeleteWidgetValue"] != null)
{
    widget.Values.Remove(Request.Form["ListBoxWidgetValues"]);
    return View("EditWidget", widget);
}

You should remove the initially posted value from the model state:

if (Request.Form["DeleteWidgetValue"] != null)
{
    var itemToRemove = Request.Form["ListBoxWidgetValues"];
    var index = widget.Values.IndexOf(itemToRemove);
    ModelState.Remove("Values[" + index + "]");
    widget.Values.Remove(itemToRemove);
    return View("EditWidget", widget);
}

So the POST request contained:

Values[0] = 1
Values[1] = 2
Values[2] = 3
Values[3] = 4

Inside the POST action you removed for example the second item so you should also remove it from the model state or the TextBoxFor helper will still use the old one.

You may also find the following blog post useful. It's for ASP.NET MVC 2 WebForms but it would be trivial to adapt it to Razor.