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.
In the Editing, Inserting, and Deleting Data tutorials of my
Working with Data in ASP.NET 2.0 tutorial series, I demonstrate how the GridView, DetailsView, and FormView can all support the built-in editing, deleting, and inserting capabilities
with view state disabled. The reason is because the key data Web control properties (such as the
DataKeys collection), are stored in control state, which is always persisted to view state regardless of the control's
EnableViewState property value.
In my tutorials I don't mention the view state issue, I just silently set the
EnableViewState property to false. And this works wonderfully when testing the page. Deletes and edits work exactly as expected, with or without view state enabled. However, a rather insidious behavior can rear its head when there are multiple users
visiting the same page. This potential trap was pointed out to me by alert reader Jamie Crutchley, whose observed and shared information about this problem on the ASP.NET Forums in the past. I'll explain the problem (and a potential workaround) in my own words,
but you can read Jamie's posts
here and here, if you'd rather.
PROBLEM: Two users - Alice and Bob - visit a page (Products.aspx) that uses a GridView whose to list products; the GridView's view state has been disabled. The GridView uses its
DataKeys collection to store the primary key values of the three products. Imagine that the products listed are Pens, Books, and Paper, and their respective primary key values are 1, 2, and 3.
Alice clicks on the Delete button for the first product in the grid (Pens).
A postback occurs. Because the GridView's view state has been disabled, on postback the data is re-read from the GridView's data source.
This has the side effect of repopulating the DataKeys collection with the newly read data!
Since the first row index was clicked, the GridView grabs the
DataKeys value indexed at 0 and issues a delete based on that primary key value (1).
Bob still sees all three products on the page (since he loaded the page before Alice deleted Pens). Sometime after Alice has made her deletion, Bob, too, decides that Pens must be deleted. He clicks on the Delete button for Pens.
A postback occurs. Because the GridView's view state has been disabled, on postback the data is re-read from the GridView's data source.
This has the side effect of repopulating the DataKeys collection with the newly read data!
Since the first row index was clicked, the GridView grabs the
DataKeys value indexed at 0. However, since the DataKeys collection has been reloaded in Step 5, the first
DataKeys value is the primary key of Books (since Pens has since been deleted). The consequence is that Books is deleted, even though Bob wanted to delete Pens!!
More generally, if Alice deletes any product whose index preceeds the index of the record Bob deletes, Bob's delete will actually delete a different record. Similarly, if Alice deletes a preceeding record of the one Bob is editing, the edits will be applied
to a preceeding row. Eep.
SHORT AND SIMPLE SOLUTION: Unless you are absolutely, 100%, certifiably, unconditionally certain that there will never, ever, not in a million years be two users concurrently editing/deleting records, then be sure to leave the GridView /
DetailsView / FormView's EnableViewState property to True (the default).
MORE INVOLVED SOLUTION: If you really would like to reduce the page size by disabling view state, you can use the following “hack”... When a Delete (or Edit) button is clicked in the GridView the
RowCommand event fires before the
DataKeys collection is internally repopulated. Therefore, you can create an event handler that includes code that “saves” the
DataKeys value(s) for the record being deleted. Then, in the ObjectDataSource's
Deleting event handler you can reassign this value back to the primary key parameter(s).
Here's some code to implement this approach. First, in the
RowCommand event handler the primary key (ProductID) is saved in a page-level variable:
The tutorials and code at Working with Data in ASP.NET 2.0 will be updated so that they no longer disable view state and will include a similar warning as to this blog entry, although due to the breadth of material online, it may take several days or
weeks to get all the changes made and propagated...
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class EditInsertDelete_Basics : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
int y = 0;
}
protected void ObjectDataSource1_Deleting(object sender, ObjectDataSourceMethodEventArgs e)
{
if (recordToDelete > 0)
e.InputParameters["ProductID"] = recordToDelete;
}
protected void ObjectDataSource1_DataBinding(object sender, EventArgs e)
{
int x = 0;
}
protected void ObjectDataSource1_Selecting(object sender, ObjectDataSourceSelectingEventArgs e)
{
int x = 0;
}
int recordToDelete = -1;
protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
recordToDelete = Convert.ToInt32(GridView1.DataKeys[Convert.ToInt32(e.CommandArgument)].Value);
int x = 0;
}
}
Happy Programming!
-- Scott Mitchell
-- mitchell@4guysfromrolla.com
-- http://scottonwriting.net/sowblog/
-- http://www.4GuysFromRolla.com/ScottMitchell.shtml
The workaround works fine if you're using a CommandField, but not if you're using a TemplateField.
With a CommandField, the generated postback script looks like:
__doPostBack('GridView1','Delete$2')
With a TemplateField, the script looks like:
__doPostBack('grdData$ctl04$ctl00','')
Using a TemplateField, the child controls have to be re-created before the event can be raised, which involves re-binding the grid to the data source. As a result, by the time the RowCommand event is raised, the DataKeys collection has been re-populated.
Unfortunately I've lost the code, but I experimented once with creating a derived button control that would emit a "doPostBack" script targetted to it's parent when placed in a templated control. So instead of doing __doPostBack('grdData$ctl04$ctl00','')
it would do __doPostBack('grdData','Delete$2'). That way the event could be raised as if it came from the gridview before instantiating the templated controls. I wish I still had the code so I could show it to you, but essentially I just overrode the Render
method. There's probably a better way to do it, but I was just playing around with alternatives at the time.
Unfortunately I've lost the code, but I experimented once with creating a derived button control that would emit a "doPostBack" script targetted to it's parent when placed in a templated control. So instead of doing __doPostBack('grdData$ctl04$ctl00','')
it would do __doPostBack('grdData','Delete$2'). That way the event could be raised as if it came from the gridview before instantiating the templated controls. I wish I still had the code so I could show it to you, but essentially I just overrode the Render
method. There's probably a better way to do it, but I was just playing around with alternatives at the time.
My student, Matt, wrote in to share the following workaround as well (I've not tested it; might not work in all browsers):
My colleague Borys has proposed a fix. I don't think I can explain it
very well, but we think it has to do with a conflict between post data
and viewstate. I'll let the code speak for itself.
Private Sub DataList1_ItemDataBound(ByVal sender As Object, ByVal e As
System.Web.UI.WebControls.DataListItemEventArgs) Handles
DataList1.ItemDataBound
Dim b As Button = DirectCast(e.Item.Controls(1), Button)
Dim evTarget As String = b.UniqueID.Replace(":", "$")
Dim script As String = "__doPostBack('" + evTarget +
"','');return(false)"
b.Attributes.Add("onclick", script)
End Sub
Hope this helps someone, it was rather fun to examine and unearth the solution.
Which looks like it's updating the __doPostBack call. No idea if this will be of help, but when I saw your post it reminded me of this old blog entry.
HTH
Happy Programming!
-- Scott Mitchell
-- mitchell@4guysfromrolla.com
-- http://scottonwriting.net/sowblog/
-- http://www.4GuysFromRolla.com/ScottMitchell.shtml
jdcrutchley
Member
252 Points
63 Posts
ObjectDataSource + GridView + Disabled ViewState = Major Concurrency Issues (possible solution)
Sep 17, 2006 02:53 AM|LINK
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
Scott Mitche...
Contributor
4114 Points
712 Posts
ASPInsiders
MVP
Re: ObjectDataSource + GridView + Disabled ViewState = Major Concurrency Issues (possible solutio...
Nov 14, 2006 03:07 PM|LINK
I also wanted to share my approach [see http://scottonwriting.net/sowblog/posts/10054.aspx for additional comments]:
-- Scott Mitchell
-- mitchell@4guysfromrolla.com
-- http://scottonwriting.net/sowblog/
-- http://www.4GuysFromRolla.com/ScottMitchell.shtml
RichardD
Contributor
3950 Points
549 Posts
Re: ObjectDataSource + GridView + Disabled ViewState = Major Concurrency Issues (possible solutio...
Dec 05, 2006 06:25 PM|LINK
The only solution I've found is to create a derived control:
OnLoadLoadControlState method and take a copy of the DataKeys;using System; using System.Collections; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; public class ExtendedGridView : GridView { private DataKey[] _originalDataKeys; private DataKey _rowCommandDataKey; public ExtendedGridView() { } protected override void LoadControlState(object savedState) { base.LoadControlState(savedState); if (!EnableViewState) { DataKeyArray keys = this.DataKeys; if (null != keys && 0 != keys.Count) { _originalDataKeys = new DataKey[keys.Count]; keys.CopyTo(_originalDataKeys, 0); } } } protected override bool OnBubbleEvent(object source, EventArgs e) { if (null != _originalDataKeys) { GridViewRow row = source as GridViewRow; GridViewCommandEventArgs args = e as GridViewCommandEventArgs; if (null != row && null != args) { _rowCommandDataKey = _originalDataKeys[row.RowIndex]; } } return base.OnBubbleEvent(source, e); } protected override void OnRowDeleting(GridViewDeleteEventArgs e) { if (null != _rowCommandDataKey) { foreach (DictionaryEntry entry in _rowCommandDataKey.Values) { e.Keys[entry.Key] = entry.Value; } _rowCommandDataKey = null; _originalDataKeys = null; } base.OnRowDeleting(e); } protected override void OnRowUpdating(GridViewUpdateEventArgs e) { if (null != _rowCommandDataKey) { foreach (DictionaryEntry entry in _rowCommandDataKey.Values) { e.Keys[entry.Key] = entry.Value; } _rowCommandDataKey = null; _originalDataKeys = null; } base.OnRowUpdating(e); } }RichardD
Contributor
3950 Points
549 Posts
Re: ObjectDataSource + GridView + Disabled ViewState = Major Concurrency Issues (possible solutio...
Dec 05, 2006 06:47 PM|LINK
I've reported this as a bug:
https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=243580
Scott Mitche...
Contributor
4114 Points
712 Posts
ASPInsiders
MVP
Re: ObjectDataSource + GridView + Disabled ViewState = Major Concurrency Issues (possible solutio...
Dec 06, 2006 03:43 AM|LINK
My workaround works fine in my tests using an ObjectDataSource bound to the GridView. The following works for me:
<%@ Page Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" CodeFile="Basics.aspx.cs" Inherits="EditInsertDelete_Basics" Title="Untitled Page" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContent" Runat="Server"> <h2> The Basics of Editing, Inserting, and Deleting</h2> <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" DeleteMethod="DeleteProduct" InsertMethod="AddProduct" SelectMethod="GetProducts" TypeName="ProductsBLL" UpdateMethod="UpdateProduct" OnDataBinding="ObjectDataSource1_DataBinding" OnDeleting="ObjectDataSource1_Deleting" OnSelecting="ObjectDataSource1_Selecting"> <DeleteParameters> <asp:Parameter Name="productID" Type="Int32" /> </DeleteParameters> <UpdateParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="supplierID" Type="Int32" /> <asp:Parameter Name="categoryID" Type="Int32" /> <asp:Parameter Name="quantityPerUnit" Type="String" /> <asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="unitsInStock" Type="Int16" /> <asp:Parameter Name="unitsOnOrder" Type="Int16" /> <asp:Parameter Name="reorderLevel" Type="Int16" /> <asp:Parameter Name="discontinued" Type="Boolean" /> <asp:Parameter Name="productID" Type="Int32" /> </UpdateParameters> <InsertParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="supplierID" Type="Int32" /> <asp:Parameter Name="categoryID" Type="Int32" /> <asp:Parameter Name="quantityPerUnit" Type="String" /> <asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="unitsInStock" Type="Int16" /> <asp:Parameter Name="unitsOnOrder" Type="Int16" /> <asp:Parameter Name="reorderLevel" Type="Int16" /> <asp:Parameter Name="discontinued" Type="Boolean" /> </InsertParameters> </asp:ObjectDataSource> <h3>Editing, Inserting, and Deleting Data from a FormView</h3> <p> <asp:FormView ID="FormView1" runat="server" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True" EnableViewState="False"> <EditItemTemplate> ProductID: <asp:Label ID="ProductIDLabel1" runat="server" Text='<%# Eval("ProductID") %>'></asp:Label><br /> ProductName: <asp:TextBox ID="ProductNameTextBox" runat="server" Text='<%# Bind("ProductName") %>'> </asp:TextBox><br /> SupplierID: <asp:TextBox ID="SupplierIDTextBox" runat="server" Text='<%# Bind("SupplierID") %>'> </asp:TextBox><br /> CategoryID: <asp:TextBox ID="CategoryIDTextBox" runat="server" Text='<%# Bind("CategoryID") %>'> </asp:TextBox><br /> QuantityPerUnit: <asp:TextBox ID="QuantityPerUnitTextBox" runat="server" Text='<%# Bind("QuantityPerUnit") %>'> </asp:TextBox><br /> UnitPrice: <asp:TextBox ID="UnitPriceTextBox" runat="server" Text='<%# Bind("UnitPrice") %>'> </asp:TextBox><br /> UnitsInStock: <asp:TextBox ID="UnitsInStockTextBox" runat="server" Text='<%# Bind("UnitsInStock") %>'> </asp:TextBox><br /> UnitsOnOrder: <asp:TextBox ID="UnitsOnOrderTextBox" runat="server" Text='<%# Bind("UnitsOnOrder") %>'> </asp:TextBox><br /> ReorderLevel: <asp:TextBox ID="ReorderLevelTextBox" runat="server" Text='<%# Bind("ReorderLevel") %>'> </asp:TextBox><br /> Discontinued: <asp:CheckBox ID="DiscontinuedCheckBox" runat="server" Checked='<%# Bind("Discontinued") %>' /> <br /> <asp:LinkButton ID="UpdateButton" runat="server" CausesValidation="True" CommandName="Update" Text="Update"> </asp:LinkButton> <asp:LinkButton ID="UpdateCancelButton" runat="server" CausesValidation="False" CommandName="Cancel" Text="Cancel"> </asp:LinkButton> </EditItemTemplate> <InsertItemTemplate> ProductName: <asp:TextBox ID="ProductNameTextBox" runat="server" Text='<%# Bind("ProductName") %>'> </asp:TextBox><br /> SupplierID: <asp:TextBox ID="SupplierIDTextBox" runat="server" Text='<%# Bind("SupplierID") %>'> </asp:TextBox><br /> CategoryID: <asp:TextBox ID="CategoryIDTextBox" runat="server" Text='<%# Bind("CategoryID") %>'> </asp:TextBox><br /> QuantityPerUnit: <asp:TextBox ID="QuantityPerUnitTextBox" runat="server" Text='<%# Bind("QuantityPerUnit") %>'> </asp:TextBox><br /> UnitPrice: <asp:TextBox ID="UnitPriceTextBox" runat="server" Text='<%# Bind("UnitPrice") %>'> </asp:TextBox><br /> UnitsInStock: <asp:TextBox ID="UnitsInStockTextBox" runat="server" Text='<%# Bind("UnitsInStock") %>'> </asp:TextBox><br /> UnitsOnOrder: <asp:TextBox ID="UnitsOnOrderTextBox" runat="server" Text='<%# Bind("UnitsOnOrder") %>'> </asp:TextBox><br /> ReorderLevel: <asp:TextBox ID="ReorderLevelTextBox" runat="server" Text='<%# Bind("ReorderLevel") %>'> </asp:TextBox><br /> Discontinued: <asp:CheckBox ID="DiscontinuedCheckBox" runat="server" Checked='<%# Bind("Discontinued") %>' /><br /> <asp:LinkButton ID="InsertButton" runat="server" CausesValidation="True" CommandName="Insert" Text="Insert"> </asp:LinkButton> <asp:LinkButton ID="InsertCancelButton" runat="server" CausesValidation="False" CommandName="Cancel" Text="Cancel"> </asp:LinkButton> </InsertItemTemplate> <ItemTemplate> ProductID: <asp:Label ID="ProductIDLabel" runat="server" Text='<%# Eval("ProductID") %>'></asp:Label><br /> ProductName: <asp:Label ID="ProductNameLabel" runat="server" Text='<%# Bind("ProductName") %>'> </asp:Label><br /> SupplierID: <asp:Label ID="SupplierIDLabel" runat="server" Text='<%# Bind("SupplierID") %>'> </asp:Label><br /> CategoryID: <asp:Label ID="CategoryIDLabel" runat="server" Text='<%# Bind("CategoryID") %>'> </asp:Label><br /> QuantityPerUnit: <asp:Label ID="QuantityPerUnitLabel" runat="server" Text='<%# Bind("QuantityPerUnit") %>'> </asp:Label><br /> UnitPrice: <asp:Label ID="UnitPriceLabel" runat="server" Text='<%# Bind("UnitPrice") %>'></asp:Label><br /> UnitsInStock: <asp:Label ID="UnitsInStockLabel" runat="server" Text='<%# Bind("UnitsInStock") %>'> </asp:Label><br /> UnitsOnOrder: <asp:Label ID="UnitsOnOrderLabel" runat="server" Text='<%# Bind("UnitsOnOrder") %>'> </asp:Label><br /> ReorderLevel: <asp:Label ID="ReorderLevelLabel" runat="server" Text='<%# Bind("ReorderLevel") %>'> </asp:Label><br /> Discontinued: <asp:CheckBox ID="DiscontinuedCheckBox" runat="server" Checked='<%# Bind("Discontinued") %>' Enabled="false" /><br /> CategoryName: <asp:Label ID="CategoryNameLabel" runat="server" Text='<%# Bind("CategoryName") %>'> </asp:Label><br /> SupplierName: <asp:Label ID="SupplierNameLabel" runat="server" Text='<%# Bind("SupplierName") %>'> </asp:Label><br /> <asp:LinkButton ID="EditButton" runat="server" CausesValidation="False" CommandName="Edit" Text="Edit"> </asp:LinkButton> <asp:LinkButton ID="DeleteButton" runat="server" CausesValidation="False" CommandName="Delete" Text="Delete"> </asp:LinkButton> <asp:LinkButton ID="NewButton" runat="server" CausesValidation="False" CommandName="New" Text="New"> </asp:LinkButton> </ItemTemplate> </asp:FormView> </p> <h3> Editing, Inserting, and Deleting Data from a DetailsView</h3> <p> <asp:DetailsView ID="DetailsView1" runat="server" AutoGenerateRows="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True" EnableViewState="False"> <Fields> <asp:BoundField DataField="ProductID" HeaderText="ProductID" InsertVisible="False" ReadOnly="True" SortExpression="ProductID" /> <asp:BoundField DataField="ProductName" HeaderText="ProductName" SortExpression="ProductName" /> <asp:BoundField DataField="SupplierID" HeaderText="SupplierID" SortExpression="SupplierID" /> <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" SortExpression="CategoryID" /> <asp:BoundField DataField="QuantityPerUnit" HeaderText="QuantityPerUnit" SortExpression="QuantityPerUnit" /> <asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice" SortExpression="UnitPrice" /> <asp:BoundField DataField="UnitsInStock" HeaderText="UnitsInStock" SortExpression="UnitsInStock" /> <asp:BoundField DataField="UnitsOnOrder" HeaderText="UnitsOnOrder" SortExpression="UnitsOnOrder" /> <asp:BoundField DataField="ReorderLevel" HeaderText="ReorderLevel" SortExpression="ReorderLevel" /> <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" SortExpression="Discontinued" /> <asp:BoundField DataField="CategoryName" HeaderText="CategoryName" ReadOnly="True" SortExpression="CategoryName" InsertVisible="False" /> <asp:BoundField DataField="SupplierName" HeaderText="SupplierName" ReadOnly="True" SortExpression="SupplierName" InsertVisible="False" /> <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" ShowInsertButton="True" /> </Fields> </asp:DetailsView> </p> <h3> Editing and Deleting Data from a GridView</h3> <p> <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" EnableViewState="False" OnRowCommand="GridView1_RowCommand"> <Columns> <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" /> <asp:BoundField DataField="ProductID" HeaderText="ProductID" InsertVisible="False" ReadOnly="True" SortExpression="ProductID" /> <asp:BoundField DataField="ProductName" HeaderText="ProductName" SortExpression="ProductName" /> <asp:BoundField DataField="SupplierID" HeaderText="SupplierID" SortExpression="SupplierID" /> <asp:BoundField DataField="CategoryID" HeaderText="CategoryID" SortExpression="CategoryID" /> <asp:BoundField DataField="QuantityPerUnit" HeaderText="QuantityPerUnit" SortExpression="QuantityPerUnit" /> <asp:BoundField DataField="UnitPrice" HeaderText="UnitPrice" SortExpression="UnitPrice" /> <asp:BoundField DataField="UnitsInStock" HeaderText="UnitsInStock" SortExpression="UnitsInStock" /> <asp:BoundField DataField="UnitsOnOrder" HeaderText="UnitsOnOrder" SortExpression="UnitsOnOrder" /> <asp:BoundField DataField="ReorderLevel" HeaderText="ReorderLevel" SortExpression="ReorderLevel" /> <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued" SortExpression="Discontinued" /> <asp:BoundField DataField="CategoryName" HeaderText="CategoryName" ReadOnly="True" SortExpression="CategoryName" /> <asp:BoundField DataField="SupplierName" HeaderText="SupplierName" ReadOnly="True" SortExpression="SupplierName" /> </Columns> </asp:GridView> </p> </asp:Content>-- Scott Mitchell
-- mitchell@4guysfromrolla.com
-- http://scottonwriting.net/sowblog/
-- http://www.4GuysFromRolla.com/ScottMitchell.shtml
RichardD
Contributor
3950 Points
549 Posts
Re: ObjectDataSource + GridView + Disabled ViewState = Major Concurrency Issues (possible solutio...
Dec 06, 2006 10:23 AM|LINK
The workaround works fine if you're using a CommandField, but not if you're using a TemplateField.
__doPostBack('GridView1','Delete$2')__doPostBack('grdData$ctl04$ctl00','')Using a TemplateField, the child controls have to be re-created before the event can be raised, which involves re-binding the grid to the data source. As a result, by the time the RowCommand event is raised, the DataKeys collection has been re-populated.
jdcrutchley
Member
252 Points
63 Posts
Re: ObjectDataSource + GridView + Disabled ViewState = Major Concurrency Issues (possible solutio...
Dec 06, 2006 01:27 PM|LINK
Unfortunately I've lost the code, but I experimented once with creating a derived button control that would emit a "doPostBack" script targetted to it's parent when placed in a templated control. So instead of doing __doPostBack('grdData$ctl04$ctl00','') it would do __doPostBack('grdData','Delete$2'). That way the event could be raised as if it came from the gridview before instantiating the templated controls. I wish I still had the code so I could show it to you, but essentially I just overrode the Render method. There's probably a better way to do it, but I was just playing around with alternatives at the time.
RichardD
Contributor
3950 Points
549 Posts
Re: ObjectDataSource + GridView + Disabled ViewState = Major Concurrency Issues (possible solutio...
Dec 06, 2006 02:36 PM|LINK
OK, this version of the code should work for the Select, Edit, Update and Delete commands, whether you're using a CommandField or a TemplateField:
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Security.Permissions; using System.Text; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] [AspNetHostingPermission(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)] public class ExtendedGridView : GridView { private DataKey[] _originalDataKeys; private DataKey _rowCommandDataKey; private int? _selectedIndex; private int? _editIndex; private bool _suppressDataBind; protected override void LoadControlState(object savedState) { base.LoadControlState(savedState); if (!EnableViewState) { DataKeyArray keys = this.DataKeys; if (null != keys && 0 != keys.Count) { _originalDataKeys = new DataKey[keys.Count]; keys.CopyTo(_originalDataKeys, 0); } } } protected override bool OnBubbleEvent(object source, EventArgs e) { if (null != _originalDataKeys && null == _rowCommandDataKey) { GridViewRow row = source as GridViewRow; GridViewCommandEventArgs args = e as GridViewCommandEventArgs; if (null != row && null != args && 0 <= row.RowIndex && row.RowIndex < _originalDataKeys.Length) { _rowCommandDataKey = _originalDataKeys[row.RowIndex]; } } return base.OnBubbleEvent(source, e); } protected override void OnRowCommand(GridViewCommandEventArgs e) { if (null != _originalDataKeys && null == _rowCommandDataKey) { string arg = Convert.ToString(e.CommandArgument, CultureInfo.InvariantCulture); if (!string.IsNullOrEmpty(arg)) { int rowIndex; if (int.TryParse(arg, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out rowIndex)) { if (0 <= rowIndex && rowIndex < _originalDataKeys.Length) { _rowCommandDataKey = _originalDataKeys[rowIndex]; } } } } base.OnRowCommand(e); } protected override void OnRowDeleting(GridViewDeleteEventArgs e) { if (null != _rowCommandDataKey) { foreach (DictionaryEntry entry in _rowCommandDataKey.Values) { e.Keys[entry.Key] = entry.Value; } _rowCommandDataKey = null; _originalDataKeys = null; } base.OnRowDeleting(e); } protected override void OnRowUpdating(GridViewUpdateEventArgs e) { if (null != _rowCommandDataKey) { foreach (DictionaryEntry entry in _rowCommandDataKey.Values) { e.Keys[entry.Key] = entry.Value; } _rowCommandDataKey = null; _originalDataKeys = null; } base.OnRowUpdating(e); } protected override void OnDataSourceViewChanged(object sender, EventArgs e) { _originalDataKeys = null; _rowCommandDataKey = null; base.OnDataSourceViewChanged(sender, e); } protected override void EnsureDataBound() { if (!_suppressDataBind) { base.EnsureDataBound(); } } protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding) { _selectedIndex = null; _editIndex = null; int result = base.CreateChildControls(dataSource, dataBinding); if (base.RequiresDataBinding) { base.SelectedIndex = _selectedIndex ?? -1; base.EditIndex = _editIndex ?? -1; } else { _suppressDataBind = true; try { base.SelectedIndex = _selectedIndex ?? -1; base.EditIndex = _editIndex ?? -1; base.RequiresDataBinding = false; } finally { _suppressDataBind = false; } } return result; } protected override GridViewRow CreateRow(int rowIndex, int dataSourceIndex, DataControlRowType rowType, DataControlRowState rowState) { if (DataControlRowType.DataRow == rowType && null != _originalDataKeys) { int index = base.SelectedIndex; if (-1 != index && index < _originalDataKeys.Length) { if (DataKeyEqualityComparer.Instance.Equals(_originalDataKeys[index], this.DataKeys[rowIndex])) { _selectedIndex = rowIndex; rowState |= DataControlRowState.Selected; } else { rowState &= ~DataControlRowState.Selected; } } index = base.EditIndex; if (-1 != index && index < _originalDataKeys.Length) { if (DataKeyEqualityComparer.Instance.Equals(_originalDataKeys[index], this.DataKeys[rowIndex])) { _editIndex = rowIndex; rowState |= DataControlRowState.Edit; } else if (DataControlRowState.Edit == (DataControlRowState.Edit & rowState)) { rowState &= ~DataControlRowState.Edit; } } } return base.CreateRow(rowIndex, dataSourceIndex, rowType, rowState); } protected static int FindKeyIndex(DataKeyArray keys, DataKey value) { if (null == keys || 0 == keys.Count) return -1; if (null == value) return -1; IEqualityComparer comparer = DataKeyEqualityComparer.Instance; for (int index = 0; index < keys.Count; index++) { if (comparer.Equals(keys[index], value)) return index; } return -1; } } [AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)] public sealed class DataKeyEqualityComparer : IEqualityComparer { private static readonly DataKeyEqualityComparer _instance = new DataKeyEqualityComparer(); private DataKeyEqualityComparer() { } public static DataKeyEqualityComparer Instance { get { return _instance; } } int IEqualityComparer.GetHashCode(DataKey obj) { if (null == obj) return 0; return obj.GetHashCode(); } public bool Equals(DataKey x, DataKey y) { if (object.ReferenceEquals(x, y)) return true; if (null == x || null == y) return false; return Equals(x.Values, y.Values); } private static bool Equals(IDictionary x, IDictionary y) { if (object.ReferenceEquals(x, y)) return true; if (null == x || null == y) return false; if (x.Count != y.Count) return false; foreach (DictionaryEntry entry in x) { if (!y.Contains(entry.Key)) return false; if (!ValueEquals(entry.Value, y[entry.Key])) return false; } return true; } private static bool ValueEquals(object x, object y) { if (object.ReferenceEquals(null, x)) return object.ReferenceEquals(null, y); if (object.ReferenceEquals(null, y)) return false; return x.Equals(y); } }klabran
Member
17 Points
6 Posts
Re: ObjectDataSource + GridView + Disabled ViewState = Major Concurrency Issues (possible solutio...
Dec 06, 2006 05:36 PM|LINK
I confirmed Scott's setup with the objectdatasource...
However, is there a work around for sqldatasources?
I tried adapting your code (Scott) to them and found that I couldn't capture the value before postback occurs to repop the datakeys.
Scott Mitche...
Contributor
4114 Points
712 Posts
ASPInsiders
MVP
Re: ObjectDataSource + GridView + Disabled ViewState = Major Concurrency Issues (possible solutio...
Dec 06, 2006 06:18 PM|LINK
Check out this blog entry of mine: http://scottonwriting.net/sowblog/posts/1268.aspx. Here's the germane part:
Which looks like it's updating the __doPostBack call. No idea if this will be of help, but when I saw your post it reminded me of this old blog entry.
HTH
-- Scott Mitchell
-- mitchell@4guysfromrolla.com
-- http://scottonwriting.net/sowblog/
-- http://www.4GuysFromRolla.com/ScottMitchell.shtml