0

I'm having some problems with one specific part of my ASP.NET site. This page is essentially where a user edits an invoice and then posts the changes.

At this point I'm doing a re-write of an existing page, which works. I cannot figure out what the difference is other than the other one is older MVC 3.

When I first go to the EditInvoice Action:

public ActionResult EditInvoice()
{
    SalesDocument invoice = null;

    try
    {
        invoice = SomeService.GetTheInvoice();
    }
    catch (Exception ex)
    {
        return HandleControllerException(ex);
    }

    return View("InvoiceEdit", invoice.LineItems);
}

The model of the "InvoiceEdit" view is the List

Now the view loads fine, and displays all the document line items in a form:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<List<MyApp.SalesDocumentLineItem>>" %>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <% using (Html.BeginForm()) {%>

    <fieldset>
        <legend>Line Items</legend>
            <div id="lineItems" class="table">
                <div class="row">
                    <div class="colHButton">X</div>
                    <div class="colH">Item</div>
                    <div class="colHPrice">Price</div>
                    <div class="colHQty">Qty</div>
                </div>
              <% foreach (var item in Model.Where(m => m.LineItemType.Id == NexTow.Client.Framework.UnitsService.LineItemTypes.SaleItem || m.LineItemType.Id == NexTow.Client.Framework.UnitsService.LineItemTypes.NonSaleItem))
              { %>    
                <div class="row">
                    <%=Html.Hidden("Model.LineItemNameId", item.LineItemNameId)%>
                    <div class="cellButton"><button onclick="DeleteContainer(event);">X</button></div>
                    <div class="cell"><span class="formData"><%=item.LineItemDescription %></span></div>                
                    <div class="cellPrice">
                        <span class="formData">$</span>
                        <%= Html.TextBox("Model.Price", item.Price, new { style="width:75px;" })%>
                    </div>
                    <div class="cellQty">
                        <%= Html.TextBox("Model.Quantity", item.Quantity, new { style = "width:60px;" })%>
                    </div>
                </div>  
            <%} %>
        </div>
    </fieldset>
    <p>
        <input type="submit" value="Update Invoice" onclick="SequenceFormElementsNames('salesItems');" />
    </p>

    <% } %>
</asp:Content>

This then provides the user the ability to edit the entries, and then the user clicks the "Update Invoice" submit button. This posts to the same view and action using a POST:

[HttpPost]
public ActionResult EditInvoice(List<SalesDocumentLineItem> salesItems)
{
    if (salesItems == null || salesItems.Count == 0)
    {
        return View("ClientError", new ErrorModel() { Description = "Line items required." });
    }

    SalesDocument invoice = null;

    try
    {
        invoice = SomeService.UpdateInvoice();
    }
    catch (Exception ex)
    {
        return HandleControllerException(ex);
    }

    InvoiceModel model = new InvoiceModel();
    model.Document = invoice;

    return View("InvoicePreview", model);
}

However, eventhough this worked in the old application. In the new one, this does not work. When we breakpoint at the final EditInvoice POST action method, the collection of salesItems is NULL. What is happening!?

dav_i
  • 27,509
  • 17
  • 104
  • 136
user1104203
  • 157
  • 1
  • 1
  • 8
  • Look at the `name` attributes of the form elements that get generated in your HTML. It looks like they'll be quite a bit off. I've always seen MVC form posts done with a `for` loop instead of `foreach`, and `Html.TextBoxFor(x => x[i].Price)` for the input fields. That gives the fields unique names, and posts the collection properly. I don't know if there's a way to make your example work. – Joe Enos Jul 08 '13 at 21:39

1 Answers1

2

When you use Html.TextBox(string, object), the first argument is used as the name of the form field. When you post this form back to the server, MVC looks at your action method's argument list and uses the names of the form fields to try and build those arguments. In your case, it tries to build a List<SalesDocumentLineItem>.

It looks like you're using "Model.Something" as the names of your form fields. This probably worked in the past, but I'm guessing something changed in MVC4 such that the framework doesn't know what you're talking about anymore. Fortunately, there's a better way.

Instead of setting the name of the form field using a string, use the strongly-typed HTML helpers like this:

<% for (var i = 0; i < Model.Count; i++) { %>
  <div class="row">
    <%= Html.HiddenFor(model => model[i].LineItemNameId) %>
    <!-- etc... -->
  </div>
<% } %>

These versions of the HTML helpers use a lambda expression to point to a property in your model and say "See that property? That one right there? Generate an appropriate form field name for it." (Bonus: Since the expression is pointing at the property anyway, you don't have to specify a value anymore -- MVC will just use the value of the property the expression represents)

Since lambda expressions are C# code, you will get a compile-time error if you make a typo, and tools like Visual Studio's "Rename" feature will work if you ever change the property's name.

(This answer goes into more detail on exactly how this works.)

Community
  • 1
  • 1
Brant Bobby
  • 14,956
  • 14
  • 78
  • 115
  • This solved my problem instantly. As soon as I changed the HTML helper to the "For" equivalents, it worked. There must be something in MVC4 i was ignoring I appreciate your assistance! – user1104203 Jul 09 '13 at 04:12