Alberto Alberto - 2 months ago 11
ASP.NET (C#) Question

Missing order details from edited order

Why I get the order inside the HttpPost

Edit()
method with empty OrderDetails collection? Is it expected in ASP.NET MVC?

Thanks.

Orders controller:

public ActionResult Edit(int id)
{
Order order = GetOrderById(id);

return View(order);
}

[HttpPost]
public ActionResult Edit(Order order)
{
if (ModelState.IsValid)
{
context.Entry(order).State = EntityState.Modified;
context.SaveChanges();
// HERE order.OrderDetails is empty even if it was filled with a number of items
}

return View(order);
}

private Order GetOrderById(int id)
{
return context.Orders.Single(x => x.orderID == id);
}


Model:

public partial class Order
{
public Order()
{
this.OrderDetails = new HashSet<OrderDetail>();
}

public int orderID { get; set; }
public int customerID { get; set; }
public string promoCodeID { get; set; }

public virtual Customer Customer { get; set; }
public virtual ICollection<OrderDetail> OrderDetails { get; set; }
public virtual PromoCode PromoCode { get; set; }
}


EDIT:

View:

@using Corporate.Models
@model Corporate.Models.Order

@{
Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>@ViewBag.Title</h2>

@using (Html.BeginForm("Edit", "Orders", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
@Html.Partial("_CreateOrEdit", Model)
<br />
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Update" />
</div>
</div>

@Html.HiddenFor(model => model.taxes)
}


Partial
_CreateOrEdit
full of stuff like this, nothing else:

<div class="form-group">
@Html.LabelFor(m => m.orderID, new { @class = "col-md-2 control-label" })
<div class="col-md-10">
@Html.TextBoxFor(m => m.orderID, new { @class = "form-control datepicker"})
@Html.ValidationMessageFor(m => m.orderID)
</div>
</div>

Answer

Taking one more stab at this, as I expect the real "issue" here is a misunderstanding on how MVC handles data as you move through an application - leaving my previous answer intact as there is some useful information there, though I'll gladly drop it if asked.

For starters, stephen.vakil's comment on my original answer has some useful information (emphasis mine):

The only thing that will be in your posted object are things that the model binder can map from your html fields. Once your view is rendered, whatever objects you had are no longer in memory. When you submit the form, the mvc API will take anything it finds in Request.Form and try to match it against fields in your model. Everything else is left blank (or, in this case, as a new empty HashSet). If you want your OrderDetails to be there on post, either load the object again, or persist the data in some way (via Hidden or otherwise).

Basically, when you load a View, you're performing a GET request. In your case, your GET method takes in an order ID:

public ActionResult Edit(int id)
{
   Order order = GetOrderById(id);

   return View(order);
} //get the order by ID, and pass it down to the view

With this GET request, your View page will hold the data you expect (assuming an order was found for the ID given) and, assuming your view is set up for it, you should be able to see the OrderDetails - so all is well here.

Note that at this point, your GET request has performed its duties and tossed aside the Order object it was holding - as far as the controller is concerned, there's no use for this anymore.

For the next bit, here's a simplified view to work with:

@using Corporate.Models
@model Corporate.Models.Order

@{
   Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>@ViewBag.Title</h2>

@using (Html.BeginForm("Edit", "Orders", FormMethod.Post))
{       
    @Html.TextBoxFor(model => model.orderID)
    @Html.TextBoxFor(model => model.customerID)
    <input type="submit" value="Submit" />
}

When you click Submit on this view, your POST method gets called:

[HttpPost]
public ActionResult Edit(Order order)
{
   if (ModelState.IsValid)
   {                                 
       context.Entry(order).State = EntityState.Modified;
       context.SaveChanges();
       // HERE order.OrderDetails is empty even if it was filled with a number of items
   }

   return View(order);
}

When we get here, a new Order object is created from the data in your form! In the case of the simplified view, this means your new Order object only has the orderID and customerID properties (and an empty HashSet, generated from the Order constructor) - anything else that got loaded into the view is dropped because it was not part of the submitted data.


Posting all of your data back to the controller

If you want this data to persist into the POST request, you need to tell your form to store it.

There are a number of ways to do this, likely the easiest being @Html.HiddenFor(). HiddenFor essentially just holds a reference to a particular property so it isn't lost, even if you don't want that data visible on the form. You can still display the data however you choose, but unless your form is explicitly told to, it won't pass the information back on post.

Taking the same view, let's add some additional properties:

@using Corporate.Models
@model Corporate.Models.Order

@{
   Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>@ViewBag.Title</h2>

@using (Html.BeginForm("Edit", "Orders", FormMethod.Post))
{       
    @Html.TextBoxFor(model => model.orderID)
    @Html.TextBoxFor(model => model.customerID)
    <input type="submit" value="Submit" />

    //these values will all be POSTed back exactly as they were passed down in the GET request  
    @Html.HiddenFor(model => model.promoCodeID)
    @Html.HiddenFor(model => model.Customer.Prop1)
    @Html.HiddenFor(model => model.Customer.Prop2) //etc - these objects need all of their properties bound to the form
    @Html.HiddenFor(model => model.OrderDetails.Prop1)
    @Html.HiddenFor(model => model.PromoCode.Prop1)
}

Notice that I've added HiddenFors to the remaining properties in your model. Now, the form has explicitly been told about the properties, and will pass them into your POST request. So when the POST request creates a new Order object, it will have these properties to work with.

Note that for complex objects (Customer for example, thanks @stephen muecke) you'll need to bind the individual properties to post back to the form - see an example here.

So with that, your controller will receive all the data it needs to fully recreate the object, even if you don't want the user editing some or all of it.

Boiling all of this down into one sentence, your form will only pass back the data it's explicitly told to pass back - either by using Hidden or some other method.


As a final note here, please understand that your @Html.Partial() is not technically a part of your form - it's more complicated than that, but that's the important bit to take away.

If you want the information in your Partial to be posted, you should consider making it an EditorTemplate (described in my original answer, and elsewhere on this site). An EditorTemplate is essentially a fancy form field, and any information stored there will be passed back on POST.