views:

443

answers:

3

I have a many-to-many relationship between Issues and Scopes in my EF Context. In ASP.NET MVC, I bring up an Edit form that allows the user to edit a particular Issue. At the bottom of the form, is a list of checkboxes that allow them to select which scopes apply to this issue. When editing an issue, it likely will always have some Scopes associated with it already--these boxes will be checked already. However, the user has the opportunity to check more scopes or remove some of the currently checked scopes. My code looked something like this to save just the Issue:

            using (var edmx = new MayflyEntities())
            {
                Issue issue = new Issue { IssueID = id, TSColumn = formIssue.TSColumn };
                edmx.Issues.Attach(issue);

                UpdateModel(issue);

                if (ModelState.IsValid)
                {
                    //if (edmx.SaveChanges() != 1) throw new Exception("Unknown error. Please try again.");
                    edmx.SaveChanges();
                    TempData["message"] = string.Format("Issue #{0} successfully modified.", id);
                }
            }

So, when I try to add in the logic to save the associated scopes, I tried several things, but ultimately, this is what made the most sense to me:

            using (var edmx = new MayflyEntities())
            {
                Issue issue = new Issue { IssueID = id, TSColumn = formIssue.TSColumn };
                edmx.Issues.Attach(issue);

                UpdateModel(issue);

                foreach (int scopeID in formIssue.ScopeIDs)
                {
                    var thisScope = new Scope { ID = scopeID };
                    edmx.Scopes.Attach(thisScope);
                    thisScope.ProjectID = formIssue.ProjectID;
                    if (issue.Scopes.Contains(thisScope))
                    {
                        issue.Scopes.Attach(thisScope); //the scope already exists
                    }
                    else
                    {
                        issue.Scopes.Add(thisScope); // the scope needs to be added
                    }
                }

                if (ModelState.IsValid)
                {
                    //if (edmx.SaveChanges() != 1) throw new Exception("Unknown error. Please try again.");
                    edmx.SaveChanges();
                    TempData["message"] = string.Format("Issue #{0} successfully modified.", id);
                }
            }

But, unfortunately, that just throws the following exception:

An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.

What am I doing wrong?

A: 

Warning, this is just from the top of my head, I didn't try it.

I don't think you can set a foreign key just like you do with ProjectID.

you need to retrieve the project and add it to the scope.Project navigation property. EF will take care of the relations when you insert it.

Now again, I didn't try and might be wrong, but worth a try. Maybe this helps getting you on the way..

Stephane
no, don't think so. I just changed the line `thisScope.ProjectID = formIssue.ProjectID;` to `thisScope.Project = issue.Project;` and still got the same error. is that what you meant?
Jorin
Ok, I'll try by myself and post the code I come up with if you didn't solve the issue by yourself.
Stephane
nope, I've exhausted myself trying different options. In my test case, i have 3 scopes, 2 of which are already associated with this issue. I make it so all 3 are checked on my form, so the 2 existing should get updated and 1 should get associated with the issue. I get varying results based upon what i try, but nothing that does what it's supposed to. Strangely, with the `.Add` method, sometime it tries to add a new scope to the database instead of just "adding" a new association with this issue, but i can't make sense of how to get it to work correctly still.
Jorin
A: 

**Disclaimer: I am relatively new to EF, take my answer with a bucket of salt.

If this is an edit form, I think your issue object should not be a "new" issue, it should pull the issue out of the data store. As far as I can see by doing:

Issue issue = new Issue { IssueID = id, TSColumn = formIssue.TSColumn }; edmx.Issues.Attach(issue); you are effectively creating a New Issue with the Id of the one you are trying to edit.

Again, I am here looking for ways to make sense of EF myself. Sometimes I miss my SQL statements.

Obi Juan Canoli
Thanks, but no. Like I said, the Issue part of it works fine, its just the child entities that i can't seem to get updated. the "new" you see there is just a way to create a stub entity rather than using EntityKey. See this article: http://blogs.msdn.com/alexj/archive/2009/06/19/tip-26-how-to-avoid-database-queries-using-stub-entities.aspx
Jorin
Ooohhh pretty!! I am going to have to try this stub entity thing, it will cut down on a lot of unnecessary querying. Thank you!
Obi Juan Canoli
+6  A: 

Stubs are generally only effective for 1-* relationships. *-* relationships introduce a different set of challenges.

Namely that when you attach both ends - unlike 1-* - you still have no idea if the relationship already exists or not.

So that means that this code:

if (issue.Scopes.Contains(thisScope))

Is probably going to return false every time.

What I would do is this:

edmx.Issues.Attach(issue);
UpdateModel(issue);
// or ctx.LoadProperty(issue, "Scopes") if it is a POCO class.
issue.Scopes.Load(); // hit the database to load the current state.

Now you need to find out what you need to add & remove from issue.Scopes. You can do this by comparing based on ID.

i.e. if you have a set of Scope IDs you want to have related to the issue (relatedScopes)

Then this code works out what to add and what to remove.

int[] toAdd = relatedScopes.Except(issue.Scopes.Select(s => s.ID)).ToArray();
int[] toRemove = issue.Scopes.Select(s => s.ID).Except(relatedScopes).ToArray();

Now for toAdd you do this:

foreach(int id in toAdd)
{
   var scope = new Scope{Id = id};
   edmx.Scopes.Attach(scope);
   issue.Scopes.Add(scope);
}

And for each scope you need to remove

foreach(int id in toRemove)
{
   issue.Scopes.Remove(issue.Scopes.Single(s => s.ID == id));
}

By now the correct relationships should be formed.

Hope this helps

Alex

Microsoft

Alex James
That's perfect! And looking at SQL profiler, it's only one "extra" call to the DB for the .Load(), but much cleaner way of adding/removing than the way I used to do it by hand with stored procedures. Thanks!
Jorin
@Alex-James nice answer. This is a common problem in many application (effectively add/removing tags). Isn't it time for a system level function to do this without a screen full of code? Something like *issue.Scopes.ReplaceWith(myScopes);*
TFD
@TFD yeah I hear you. Failing that, the EF team has lots on its plate, a simple extension method would do the trick though right?
Alex James
This technique works really well. Thanks for sharing.
JohnnyO
+1 I like this technique.
Ladislav Mrnka