Saving multiple objects from MVC view

2019-01-24 20:51发布

问题:

I'm writing my first MVC3 application which is a simple order tracking application. I would like to edit the order and the details at the same time. When I edit the order the ActionResult for the Edit returns the order and the associated line (i'm using EF as well).

public ActionResult Edit(int id)
    {            
        // Get the order with the order lines
        var orderWithLines = from o in db.Orders.Include("OrderLines")
                                where o.ID == id
                                select o;

        // Not sure if this is the best way to do this.
        // Need to find a way to cast to "Order" type
        List<Order> orderList = orderWithLines.ToList();
        Order order = orderList[0];

        // Use ViewData rather than passing in the object in the View() method.
        ViewData.Model = order;
        return View();            
    }

The order and the lines display with no issue but when I save the page I do not get any of the lines passed back to the controller. Only the order. Here is the View code.

    @model OrderTracker.Models.Order

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm())
{
    <fieldset>
        <legend>Order</legend>   

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

        <div>
            @Html.LabelFor(model => model.OrderDate)
        </div>
        <div>
            @Html.EditorFor(model => model.OrderDate)
        </div>
        <div>
            @Html.LabelFor(model => model.Description)
        </div>
        <div>
            @Html.EditorFor(model => model.Description)
        </div>                   

        <table>
            <tr>
                <th>
                    Description
                </th>
                <th>
                    Quantity
                </th>
                <th>
                    Weight
                </th>
                <th>
                    Price
                </th>
                <th></th>
            </tr>
        @foreach (var line in Model.OrderLines)
        { 
            <tr>
                <td>
                    @Html.EditorFor(modelItem => line.Description)
                </td> 
                <td>
                    @Html.EditorFor(modelItem => line.Quantity)
                </td> 
                <td>
                    @Html.EditorFor(modelItem => line.Weight)
                </td> 
                <td>
                    @Html.EditorFor(modelItem => line.Price)
                </td>
            </tr>
        }
        </table>


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

    </fieldset>    
}

Can I please get some guidance on the best way to save the line data as well as the order data.

Thanks.

回答1:

The issue that you are facing is related to the names generated by the ICollection<T> controls. Here is a detailed discussion by Phil Haack and a solution by him (in terms of an @Html extension method; download the sample project from the link given at the end of his blog post). This post targets MVC/MVC2; however it is still applicable with MVC3.

Alternatively if you don't want to follow the hack, you can opt for a EditorTemplate for your OrderLine entity model.

Here are the steps.

1) Create Editor template under (Views ->Shared -> EditorTemplates -> OrderLine.cshtml) It is important to create a folder named EditorTemplates under Shared, and the template name should be same as the EntityModel for which you want to create the templete; hence the name OrderLine.cshtml)

2) Code for OrderLine.cshtml

@model OrderTracker.Models.OrderLine
@{
    Layout = null;
}

<!DOCTYPE html>
@Html.HiddenFor(modelItem => Model.id)
<tr>
<td>
    @Html.EditorFor(modelItem => Model.Description)
</td> 
<td>
    @Html.EditorFor(modelItem => Model.Quantity)
</td> 
<td>
    @Html.EditorFor(modelItem => Model.Weight)
</td> 
<td>
    @Html.EditorFor(modelItem => Model.Price)
</td>
</tr>

3) Edit your View with this code (note that I've used EditorFor for OrderLines collection)

@model OrderTracker.Models.Order

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm())
{
    <fieldset>
        <legend>Order</legend>   

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

        <div>
            @Html.LabelFor(model => model.OrderDate)
        </div>
        <div>
            @Html.EditorFor(model => model.OrderDate)
        </div>
        <div>
            @Html.LabelFor(model => model.Description)
        </div>
        <div>
            @Html.EditorFor(model => model.Description)
        </div>                     
        <div>
        <table>
            <tr>
                <th>
                    Description
                </th>
                <th>
                    Quantity
                </th>
                <th>
                    Weight
                </th>
                <th>
                    Price
                </th>
            </tr>
            @Html.EditorFor(model => model.OrderLines)
            </table>
        </div>
        <p>
            <input type="submit" value="Save" />
        </p> 

    </fieldset>    
}

4) Now on post back you will see the values



回答2:

Using MVC it should be rather straight forward as the framework is designed to to turn a form into a model.

[HttpGet]
public ActionResult Edit(int id)
{
    // (you can probably rewrite this using a lambda
    var orderWithLines = from o in db.Orders.Include("OrderLines")
                         select o;

    // Use ViewData rather than passing in the object in the View() method.
    ViewData.Model = orderWithLines.FirstOrDefault(x => x.ID = id);
    return View();
}

[HttpPost]
public ActionResult Edit(OrderTracker.Models.Order model)
{
    if (ModelState.IsValid)
    {
        // call the service layer / repository / db to persist the object graph
        _service.Save(model); // this assumes your view models are the same as your domain
    }
}


回答3:

The issue is with your foreach if you look at the raw html that it produces it will not be generating unique ids for each of the order lines and thus will not be able to bind the model when the form is posted back.

Change the foreach to a for loop and then reference each orderline using the index. This will allow for unique ids to be generated in the form and allow you to bind the to the model when it is posted back.

e.g.

@for (var counter = 0; counter < Model.OrderLines.Count(); counter++)

    { 
        <tr>
            <td>
                @Html.EditorFor(modelItem => Model.OrderLines[counter].Description)
            </td> 
            <td>
                @Html.EditorFor(modelItem => Model.OrderLines[counter].Quantity)
            </td> 
            <td>
                @Html.EditorFor(modelItem => Model.OrderLines[counter].Weight)
            </td> 
            <td>
                @Html.EditorFor(modelItem => Model.OrderLines[counter].Price)
            </td>
        </tr>
    }
    </table>