DISCLAIMER: I'm no expert on the GridView lifecycle, but empirical evidence seems to suggest this stuff is correct... I'm sure if I've gotten anything wrong here an expert will jump in and correct me.
Issue:
When you hook up a GridView to an ObjectDataSource when the ViewState is disabled, there is a major concurrency issue. The issue is that if Bob and Joe are both looking at the same list, and Joe deletes Row #3, then Bob goes to delete Row #5, Bob is really going to delete Row #6 (of the original list they were looking at) because all of the rows have shifted upward after Joe's delete. This could happen even with the ObjectDataSource's concurrency stuff running because things like Delete are just looking at row number. Note that this might also happen with ViewState enabled - I'm not really sure because I never enable ViewState for big honking GridViews...
Root Cause:
When the GridView fires off a Delete event, it sets the form's __EVENTTARGET variable to the ID of the GridView (like ctl00$ContentPlaceHolder1$GridView1) and the __EVENTARGUMENT to "Delete$x" where the "x" part represents the row of the deleted item. So, Joe fires off his delete of Row #3 and __EVENTARGUMENT is "Delete$3", and this tells the ObjectDataSource "Delete the thing at Row #3", so it grabs the DataKey and does a delete. Now Bob fires off his delete before refreshing the display and __EVENTARGUMENT gets set to "Delete$5". Trouble is, now what was Row #5 is really Row #4 because of Joe's previous delete, so the ObjectDataSource ends up deleting the wrong line. Another side effect of this is that hitting the browser's refresh button immediately after doing a delete of Row #3 will cause whatever happens to now be on Row #3 to be deleted without error. Again, this might not be the same if you have concurrency set on the ObjectDataSource and ViewState enabled on your GridView - but who wants all of the data in a huge GridView getting sent to the client anyway?
A Possible Solution:
Well, the only way I've found to get around this is to just chuck the ObjectDataSource and go back to manually binding my data. (That's probably a tiny bit faster anyway since the calls can be early-bound.) Once that's done, you can hook up to the GridView's OnRowCreated event and do something like this:
if (e.Row.RowType == DataControlRowType.DataRow)
e.Row.ID = "ctl" + ((MyNamespace.MyClass)e.Row.DataItem).ID;
Doing this will make the ID of the control represent the unique ID of your object instead of the row number. That way command event gets fired if that unique item still exists in the list, but if it doesn't, the command event is never fired. Also, if you want to let the user know that their action didn't happen because of a concurrency issue, you can check in OnLoad to see if you're in a postback and if FindControl can't find a control with the ID in __EVENTTARGET then the item they were going to perform the action on is gone and you can give them a notification.
This method doesn't work at all with the DataSource controls because the event that gets fired for them always uses the GridView as the sender (as opposed to the LinkButton within it) and so there's no way to use this hackery to change from row number to object ID. I tried just changing the CommandArgument in the OnDeleting event, but it's readonly and that would confuse the DataSource anyway because it's expecting a row number...
Drawbacks:
Well, one obvious drawback is you can't use the ObjectDataSource to automagically update your data. To me that's a very small issue though since writing a "GV1.DataSource=something; GV1.DataBind();" isn't the hardest thing in the world and I don't like alot of automagical stuff anyway... There are probably other ones that will pop up as I use this more, but for now it's working great for me.
As always, there's more than one way to beat a mule, so you could also just use HyperLinks for your actions and add your object ID to the querystring if you don't mind adding junk to your querystring. That'd work too, but again, you couldn't use the ObjectDataSource to automagically delete your data.
Bottom Line:
ObjectDataSource is good for quick prototyping or single-user applications, but not so good for real-world applications that will have multiple users updating the same data.
jdc