views:

237

answers:

1

I'm wanting to do a simple edit form for our Issue Tracking app. For simplicity, the HttpGet Edit action looks something like this:

    // Issues/Edit/12
    public ActionResult Edit(int id)
    {
        var thisIssue = edmx.Issues.First(i => i.IssueID == id);
        return View(thisIssue);
    }

and then the HttpPost action looks something like this:

    [HttpPost]
    public ActionResult Edit(int id, FormCollection form)
    {
        // this is the dumb part where I grab the object before I update it.
        // concurrency is sidestepped here.
        var thisIssue = edmx.Issues.Single(c => c.IssueID == id);

        TryUpdateModel(thisIssue);
        if (ModelState.IsValid)
        {
            edmx.SaveChanges();

            TempData["message"] = string.Format("Issue #{0} successfully modified.", id);
            return RedirectToAction("Index");
        }

        return View(thisIssue);
    }

Which works wonderfully. However, the concurrency check doesn't work because in the Post, I'm re-retreiving the current entity right before I attempt to update it. However, with EF, I don't know how to use the fanciness of SaveChanges() but attach my thisIssue to the context. I tried to call edmx.Issues.Attach(thisIssue) but I get

The object cannot be attached because it is already in the object context. An object can only be reattached when it is in an unchanged state.

How do I handle concurrency in MVC with EF and/or how do I properly Attach my edited object to the context?

Thanks in advance

A: 

What you are doing is tricky, but can be made to work. Let's presume your timestamp field is called ConcurrencyToken. Obviously, you must include this value in the View and submit it with your form. But you can't simply assign that to the value to thisIssue.ConcurrencyToken in the POST because the EF will remember both the "old" value (the value you fetched from the DB with your call to Single() as well as the "new" value (from your form) and use the "old" value in the WHERE clause. So you need to lie to the EF and assign the correct value. Try this:

    var thisIssue = edmx.Issues.Single(c => c.IssueID == id);
    TryUpdateModel(thisIssue); // assign ConcurrencyToken
    var ose = Context.ObjectStateManager.GetObjectStateEntry(entityToUpdate);
    ose.AcceptChanges();       // pretend object is unchanged
    TryUpdateModel(thisIssue); // assign rest of properties

You can optimize this by binding only ConcurrencyToken instead of calling TryUpdateModel twice, but this should get you started.

Craig Stuntz
well, that sure works, but it definitely feels kludgy. Is there not a way to just say this is my issue object; attach this issue object to issue #12 and update issue #12 with the values stored here? That way, it puts it on the database to make sure that issue #12 still has the same timestamp...Also, can you elaborate further on how I'd only bind the ConcurrencyToken field?
Jorin
You bind only one field by passing a whitelist; there's an overload for that. To do what you ask regarding updating, you'd create a stub object, attach it to the context, and update it. This prevents fetching the newer `ConcurrencyToken` from the DB.
Craig Stuntz
Sorry, I'm sure I'm just being dense, but can you spell out something on how to stub something out and attach it. I tried this and it didnt work [HttpPost] public ActionResult Edit(int id, Issue issue) { Issue newIssue = new Issue(); newIssue.IssueID = id; newIssue.EntityKey = new System.Data.EntityKey("MayflyEntities.Issues", "IssueID", id); edmx.Issues.Attach(newIssue); newIssue = issue; edmx.SaveChanges(); }
Jorin
Here's an example: http://blogs.msdn.com/alexj/archive/2009/06/19/tip-26-how-to-avoid-database-queries-using-stub-entities.aspx
Craig Stuntz
Awesome! Thanks!
Jorin