ASP.NET MVC Model Binding Related Entities on Same

2019-04-29 16:39发布

问题:

This problem has been driving me crazy for several hours now...

In my domain, I have 2 entities that are related to each other Sku and Item. Each sku can have many items.

public class Sku
{
    private readonly EntitySet<Item> items;
    public Sku()
    {
        items = new EntitySet<Item>(AttachItems, DetachItems);
    }
    public int SkuId { get; set; }
    public string LongDescription { get; set; }
    public EntitySet<Item> Items
    {
        get { return items; }
        set{ items.Assign(value);}
    }
    private void AttachItems(Item entity)
    {
        entity.Sku = this;
    }
    private static void DetachItems(Item entity)
    {
        entity.Sku = null;
    }
}

public class Item
{
    public Sku Sku { get; set; }
    public int ItemId { get; set; }
    public string Category { get; set; }
    public string Description { get; set; }
}

I am building a page that will allow the end-user to update some fields on the sku and some fields on each item at the same time.

<% using (Html.BeginForm("Save", "Merchant", FormMethod.Post, 
       new { enctype = "multipart/form-data" })) { %>       
<fieldset>
    <legend>Sku</legend>
    <p><label for="SkuId">SkuId:</label>
       <%= Html.TextBox("SkuId", Model.SkuId, 
           new{@readonly="readonly",onfocus="this.blur();"}) %></p>
    <p><label for="LongDescription">LongDescription:</label>
       <%= Html.TextBox("LongDescription", Model.LongDescription) %></p>
</fieldset>
<% for (int i = 0; i < Model.Items.Count; i++) { %>       
<fieldset>
    <legend>Item</legend>
    <p><label for="ItemId">ItemId:</label>
       <%= Html.TextBox(string.Format("items[{0}].{1}", i, "ItemId"), 
           Model.Items[i].ItemId, 
           new { @readonly = "readonly", onfocus = "this.blur();" })%></p>
    <p><label for="Category">Category:</label>
       <%= Html.TextBox(string.Format("items[{0}].{1}", i, "Category"), 
           Model.Items[i].Category)%></p>
    <p><label for="Description">Description:</label>
       <%= Html.TextBox(string.Format("items[{0}].{1}", i, "Description"), 
           Model.Items[i].Description)%></p>
</fieldset>
<%} // for-loop %>
    <p><input type="submit" value="Save" /></p>
<%} // form %> 

I have some controller code that works by accepting both a Sku and an EntitySet of Item and then assigning the Items to the Sku.

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult Save(Sku sku, EntitySet<Item> items)
    {
        if (sku != null)
        {
            if (items != null)
            {
                sku.Items.Assign(items);
            }
        }

        // save Sku to repository ...
        // return Details view ...
    }

This works, however I have noticed that it makes two trips through the DefaultModelBinder for each Item in addition to one trip for the Sku. When the Sku is bound, the setter for Items is called, and the binder even passes in a hydrated Items collection with the correct values. However, after the call to items.Assign, Items.Count is 0. This is why I have to re-assign the items in the controller code. I was expecting the items to be transferred over to the Items collection by the binder. This should eliminate the extra trip per item, since the items parameter on my controller method could be removed. Why isn’t this working?

回答1:

You might need to create a custom model binder for this?



回答2:

Hopefully, I am understanding you problem correctly...

Rather than defining your Save action with 2 parameters, have you tried just defining it with a single parameter, of type Sku?

You would then want to redefine the item HTML controls similar to the following example...

<%=
    Html.TextBox
    (
        string.Format("sku.Items[{0}].{1}", i, "ItemId"), 
        Model.Items[i].ItemId, 
        new { @readonly = "readonly", onfocus = "this.blur();" }
    )
%>


This way, you're populating the items directly in the Sku object itself.


Another potential solution would be to add an additional field to your Item class, such as...

Int32 SkuId { get; set; }

This way, you could define an additional hidden field for each item in your view that would be auto-bound to the SkuId of each item back at the controller.

<%=
    Html.Hidden
    (
        string.Format("sku.Items[{0}].{1}", i, "SkuId"), 
        Model.Items[i].SkuId
    )
%>

You could then just update your items collection independently of your Sku object. Regardless of which way you go, the two collections have to explicitly tell Linq to SQL to update the sku and items anyhow.

You could also define your own binder class, but that's probably more work than it's worth in this case. Just follow the ASP.NET MVC conventions, and I think you should be able to find something that will work without feeling like it's a hack.



回答3:

I had a similar issue where EntitySets weren't bound properly in my ViewModel.

What I did was to create another property called Mvc[YourEntitySetPropertyName], as a generic list, that wrapped around the private fields of the EntitySet:

public List<InvoiceLineItem> MvcInvoiceLineItemList
    {
        get { return _invoiceLineItemList.ToList(); }
        set { _invoiceLineItemList.AddRange(value); }
    }

I then used that instead of the EntitySet property on my view markup.

There will be no need to pass the items in your controller method signature- just pass the Sku at that point:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Save(Sku sku)
{
    if (sku != null)
    {
       // save Sku to repository ...
       // return Details view ...
    }

}