2

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.

enter image description here

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

enter image description here

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 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

12 Comments

Thanks for the answer, I will test it. Am I doing this correctly? Is there a better way of handling a list of strings in a listbox?
@Bobby Cannon, look at the blog post I have linked to: blog.stevensanderson.com/2010/01/28/… It illustrates another approach using AJAX.
I guess whats throwing me off is that I expected "return View("EditWidget", widget")" to update / replace the model state. I guess that is not happening?
@Bobby Cannon, return View("EditWidget", widget) passes the updated model to the view, but inside the view you use the TextBoxFor helper which uses the POSTed values, not the one from the model which is passed to this view and which you updated in your action.
@ Darin Dimitrov, So @Model is the updated model but (m => m) references the posted values? So is there a way to do @Html.TextBoxFor against the @Model object?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.