MVC3 Razor “For” model - contents duplicated

2019-07-28 05:45发布

问题:

It has been intriguing that my MVC3 razor form renders duplicated values inside a foreach code block in spite of correctly receiving the data from the server. Here is my simple form in MVC3 Razor...

-- sample of my .cshtml page

@model List<Category>
@using (@Html.BeginForm("Save", "Categories", FormMethod.Post))
{
foreach (Category cat in Model)
 {
        <span>Test: @cat.CategoryName</span>

        <span>Actual: @Html.TextBoxFor(model => cat.CategoryName)</span>
        @Html.HiddenFor(model => cat.ID)
        <p>---</p>            
 }

  <input type="submit" value="Save" name="btnSaveCategory" id="btnSaveCategory" />
}

My controller action looks something like this -

 [HttpPost]
    public ActionResult Save(ViewModel.CategoryForm cat)
    {
       ... save the data based on posted "cat" values (I correctly receive them here)

       List<Category> cL = ... populate category list here
       return View(cL);
    }

The save action above returns the model with correct data.

After submitting the form above, I expect to see values for categories similar to the following upon completing the action...

Test: Category1, Actual:Category1
Test: Category2, Actual:Category2
Test: Category3, Actual:Category3
Test: Category4, Actual:Category4

However @Html.TextBoxFor duplicates the first value from the list. After posting the form, I see the response something like below. The "Actual" values are repeated even though I get the correct data from the server.

Test: Category1, Actual:Category1
Test: Category2, Actual:Category1
Test: Category3, Actual:Category1
Test: Category4, Actual:Category1

What am I doing wrong? Any help will be appreciated.

回答1:

The helper methods like TextBoxFor are meant to be used with a ViewModel that represent the single object, not a collection of objects.

A normal use would be:

@Html.TextBoxFor(c => c.Name)

Where c gets mapped, inside the method, to ViewData.Model.

You are doing something different:

@Html.TextBoxFor(c => iterationItem.Name)

The method internall will still try to use the ViewData.Model as base object for the rendering, but you intend to use it on the iteration item. That syntax, while valid for the compiler, nets you this problem.

A workaround is to make a partial view that operates on a single item: inside that view you can use html helpers with correct syntax (first sample), and then call it inside the foreach, passing the iteration item as parameter. That should work correctly.



回答2:

A better way to do this would be to use EditorTemplates.

In your form you would do this:

@model List<Category>
@using (@Html.BeginForm("Save", "Categories", FormMethod.Post))
{
    @Html.EditorForModel()
    <input type="submit" value="Save" name="btnSaveCategory" id="btnSaveCategory" />
}

Then, you would create a folder called EditorTemplates, either in the ~/Views/Shared folder or in your Controllers View folder (depending on whether you want to share the template with the whole app or just this controller), and in the EditorTemplates folder, create a Category.cshtml file which looks like this:

@model Category
<span>Test: @Model.CategoryName</span>

<span>Actual: @Html.TextBoxFor(model => model.CategoryName)</span>
@Html.HiddenFor(model => model.ID)
<p>---</p>            

MVC will automatically iterate over the collection and call your template for each item in it.



回答3:

I've noticed that using foreach loops within Views causes the name attributes of text boxes to be rendered the same for every item in the collection. For your example, every text box will be rendered with the following ID and Name attributes:

<input id="cat_CategoryName" name="cat.CategoryName" value="Category1" type="text">

When your controller receives the form data collection, it won't be able reconstruct the collection as different values.

The solution

  1. A good pattern I've adopted is to bind your View to the same class you want to post back. In the example, model is being bound to List<Category> but the controller Save method receives a model ViewModel.CategoryForm. I would make them both the same.

  2. Use a for loop instead of a foreach. The name/id attributes will be unique and the model binder will be able to distinguish the values.

My final code:

View

@model CategoryForm
@using TestMvc3.Models

@using (@Html.BeginForm("Save", "Categories", FormMethod.Post))
{
    for (int i = 0; i < Model.Categories.Count; i++)
    {
        <span>Test: @Model.Categories[i].CategoryName</span>

        <span>Actual: @Html.TextBoxFor(model => Model.Categories[i].CategoryName)</span>
        @Html.HiddenFor(model => Model.Categories[i].ID)
        <p>---</p>            
    }

    <input type="submit" value="Save" name="btnSaveCategory" id="btnSaveCategory" />
}

Controller

public ActionResult Index()
{
    // create the view model with some test data
    CategoryForm form = new CategoryForm()
    {
        Categories = new List<Category>()
    };

    form.Categories.Add(new Category() { ID = 1, CategoryName = "Category1" });
    form.Categories.Add(new Category() { ID = 2, CategoryName = "Category2" });
    form.Categories.Add(new Category() { ID = 3, CategoryName = "Category3" });
    form.Categories.Add(new Category() { ID = 4, CategoryName = "Category4" });

    // pass the CategoryForm view model
    return View(form);
}

[HttpPost]
public ActionResult Save(CategoryForm cat)
{
    // the view model will now have the correct categories
    List<Category> cl = new List<Category>(cat.Categories);

    return View("Index", cat);
}