Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC 3 EF CodeFirst - DBContext item editing

I have been frustrated by this for like a day...please help.

I have an invoice section in which I'm able to add new items dynamically by appending/removing DOM elements using JQuery...i have already figured out how to name these elements correctly so they are consequently and properly mapped to the model and send to the action parameter.

So let's say I have an action in my controller called Edit with param of type Invoice

[HttpPost]
public virtual ActionResult Edit(Invoice invoice) {
    if (ModelState.IsValid) {
         //code discussed below
    }
    return RedirectToAction("Index");
}

The invoice parameter contains all the data I want. I don't have a problem with this. The following is the object model.

public class Invoice
{
    [Key]
    public int ID { get; set; }
    public InvoiceType InvoiceType { get; set; }
    public string Description { get; set; }
    public int Amount { get; set; }
    public virtual ICollection<InvoiceItem> InvoiceItems { get; set; }
}

Note the InvoiceItems collection - this contains a collection of the dynamically inserted invoice items. The following is the InvoiceItem model

[Table("InvoiceItems")]
public class InvoiceItem
{
    [Key]
    public int ID { get; set; }

    [ForeignKey("Invoice")]
    public int InvoiceID { get; set; }
    public Invoice Invoice { get; set; }

    public string Description { get; set; }
    public int Amount { get; set; }
}

And that's where I am stuck.

I can't insert/update the items in the Invoice collection InvoiceItems into the database by any chance. I've been googling a lot and I've found these solutions - none of these works unfortunately.

Solution #1 - some weird code with SetValues:

Invoice origInvoice = db.Invoices.Single(x => x.ID == invoice.ID);
var entry = db.Entry(origInvoice);
entry.OriginalValues.SetValues(invoice);
entry.CurrentValues.SetValues(invoice);
db.SaveChanges();

Solution #2 - adding a new update method to my DbContext:

public void Update<T>(T entity) where T : class
{
   this.Set<T>().Attach(entity);
   base.ObjectContext.ObjectStateManager.ChangeObjectState(entity,System.Data.EntityState.Modified);
 }

Solution #3 - attaching an existing but modified entity to the context according to http://blogs.msdn.com/b/adonet/archive/2011/01/29/using-dbcontext-in-ef-feature-ctp5-part-4-add-attach-and-entity-states.aspx

db.Entry(invoice).State = EntityState.Modified;
db.SaveChanges();

How solution #1 works: updates all the properties except newly added InvoiceItems. SetValues() ignores the ICollection property. This would work if I added this line just before db.SaveChanges();

origEntry.Entity.InvoiceItems = invoice.InvoiceItems;

...but I don't believe that this is the correct approach at all.

How solution #2 works: doesn't work at all because there is no ObjectContext property in the DbContext class.

How solution #3 works: this solution will update the invoice but otherwise ignores InvoiceItems.


Additional info: following is the piece of javascript that is used to generate the item elements which are mapped to the InvoiceItem properties.

    window.InvoiceItemIndex++;

    var str = $.tmpl("itemRowTmpl", {
        itemIndexes: '<input type="hidden" name="Invoice.InvoiceItems.Index" value="' + window.InvoiceItemIndex + '"/> \
                      <input type="hidden" name="Invoice.InvoiceItems[' + window.InvoiceItemIndex + '].ID" value="0"/>\
                      <input type="hidden" name="Invoice.InvoiceItems[' + window.InvoiceItemIndex + '].InvoiceID" value="@Model.Invoice.ID"/>',
        itemDescription: '<textarea cols="150" name="Invoice.InvoiceItems[' + window.InvoiceItemIndex + '].Description" rows="2"></textarea>',
        itemAmount: '<input type="text" class="InvoiceItemAmount" style="text-align:right;" name="Invoice.InvoiceItems[' + window.InvoiceItemIndex + '].Amount" />',
    });

The final question: what am I doing wrong? What is wrong with having a new collection of items in relation to another model automatically inserted into the database? Why is it being ignored, when the parent model is updated sucessfully?

Edit 1: corrections

like image 830
Mirek Avatar asked Aug 13 '11 11:08

Mirek


1 Answers

DbEntityEntry.CurrentValues.SetValues only updates scalar and complex properties. It doesn't care about navigation properties. That's the reason why the changes in your InvoiceItems collection don't get written to the database. The same problem is with setting the state of the Invoice to Modified. This only marks scalar and complex properties as modified but not any navigation properties.

Unfortunately EF doesn't offer a simple method which would analyze for you which items in a navigation collection have been added, deleted or only modified compared to the original state in the database if the changes have been done while the entities were detached from a context (on a web page in your example).

It means that you have to do this comparison between original state in the database and the new changed state yourself.

Code examples how to do that are in the answers here for example: The relationship could not be changed because one or more of the foreign-key properties is non-nullable The examples fit quite well to your situation, just replace ParentItem by Invoice and ChildItems by InvoiceItems.

You can also take a look at this question from a few days ago for additional infos: WCF, Entity Framework 4.1 and Entity's State There are actually many other questions which are concerned with the same problem as the lack of support for updating detached object graphs (which is a quite common scenario) is one of the more unpleasant limitations of Entity Framework.

(One note: In a MVC controller there is the UpdateModel method. I'm not sure if this method is capable to update a complete object graph (add, delete and change items in a collection). Personally I don't use it, and if I would use it then not for working with the DB model, just for ViewModel updates.)

like image 174
Slauma Avatar answered Nov 03 '22 04:11

Slauma