Creating a Data Control Field – The CounterField

Posted by: Aaron Goldenthal 5/25/2009 9:45 PM

I’ve created a lot of GridViews to display a lot of data.  In many cases they’re simply displaying tabular data and they include one column with a counter cell, typically created with markup like: 1: <asp:TemplateField HeaderText="#"> 2: <ItemTemplate><%# Container.DataItemIndex + 1 %></ItemTemplate> 3: </asp:TemplateField> While this is not overly complicated, rather than create a field like this yet another time, I created a custom data control field to provide that functionality – the CounterField.  Data control fields can be used by both the GridView and the DetailsView (probably why they’re called fields, not columns).  While the CounterField is a fairly simple field, it’s a good introduction to how to create a custom field. The .Net Framework comes with a handful of data control fields, including the BoundField, ButtonField, CheckBoxField, CommandField, HyperlinkField, ImageField, and TemplateField.  All of these are ultimately derived from one base class – DataControlField (ButtonField and CommandField actually derive from ButtonBaseField, which itself derives from DataControlField and exposes several properties used by fields with buttons).  DataControlField includes many of the properties you’re familiar with that are available in all of the various fields – Header/FooterText, HeaderImageUrl, Control/Footer/Header/ItemStyle, and InsertVisible.  As you’ll soon see, this will make developing the CounterField much easier. To start with, we’ll declare the CounterField class, which derives from DataControlField. 1: public class CounterField : DataControlField 2: { 3: } There’s one additional property to add to CounterField on top of what’s already inherited from DataControlField, which we’ll call CountPerPage. 1: public bool CountPerPage 2: { 3: get 4: { 5: object o = ViewState["CountPerPage"]; 6: if (o != null) 7: return (bool)o; 8: else 9: return false; 10: } 11: set 12: { 13: ViewState["CountPerPage"] = value; 14: } 15: } This property will determine whether the count resets with each page, and since we typically won’t want that, the default is false.  As with most control properties, we’ll persist the value to the ViewState. Technically there’s only one method that must be overridden to create a control that derives from DataControlField, and that’s CreateField.  I say technically because DataControlField is an abstract class, and its only abstract method is CreateField.  Doing that alone, however, will create a very boring field (with a header and footer, but no content). 1: protected override DataControlField CreateField() 2: { 3: return new CounterField(); 4: } DataControlField has a method, CloneField, used to clone an existing Field.  When it’s called, it calls CreateField to create a new instance of the appropriate field type, and then calls CopyProperties to copy all of the field’s properties.  Since CounterField has an additional property, we’ll override CopyProperties as well to retain it’s value if the field is cloned. 1: protected override void CopyProperties(DataControlField newField) 2: { 3: base.CopyProperties(newField); 4: ((CounterField)newField).CountPerPage = this.CountPerPage; 5: } Now, to the real meat of the CounterField.  In either a GridView or a DetailsView, a DataControlField ultimately get converted to a TableCell, and the InitializeCell method is called to add content to the cell. 1: public override void InitializeCell(DataControlFieldCell cell, 2: DataControlCellType cellType, DataControlRowState rowState, int rowIndex) 3: { 4: base.InitializeCell(cell, cellType, rowState, rowIndex); 5:  6: if (cellType == DataControlCellType.DataCell) 7: { 8: cell.DataBinding += new EventHandler(OnDataBindField); 9: } 10: } The DataControlField’s InitializeCell method already has the logic we need to populate the header and footer cells, so we’ll call it first to address those.  If the cell is a DataCell, we’ll attach an event handler to the DataBinding event to populate the row index. 1: protected virtual void OnDataBindField(object sender, EventArgs e) 2: { 3: TableCell cell = (TableCell)sender; 4: IDataItemContainer container = (IDataItemContainer)cell.NamingContainer; 5: int rowCount = 6: (CountPerPage ? container.DisplayIndex : container.DataItemIndex) + 1; 7: cell.Text = rowCount.ToString(); 8: } First we need a reference to the cell so we can add the row index.  Since this field can be used in either a GridView or a DetailsView, we’ll utilize the commonality between the two as much as possible.  Both the GridViewRow and the DetailsView implement the IDataItemContainer interface.  You’ve probably made use of this interface in the past and not known it.  This interface is what’s referenced when you use the “Container” keyword within a data control (e.g. Container.DataItem).  The DisplayIndex property is the displayed index on the current page.  The DataItemIndex is the overall index in the data source (including the offset for the page).  So, based on the CountPerPage setting, we pick the appropriate value and set the cell’s Text property. That’s all there is to it.  What’s really nice is that Visual Studio 2008 recognizes this class as a DataControlField, so it appears in Intellisense when adding Columns to a GridView or Fields to a DetailsView. Obviously this is a very simple field, and in most cases the InitializeCell and OnDataBindingField methods would be more complex, but hopefully this was a good introduction. The download includes the complete source code as well as samples of this field used in a GridView and a DetailsView. Download sample website: (224 kb)
Tags: , , ,
Categories: Data Controls
E-mail | Kick it! | DZone it! | Permalink | Comments (2) | Post RSSRSS comment feed

Manually Databinding a GridView

Posted by: Aaron Goldenthal 4/19/2009 9:36 PM

One of the questions I see frequently on the ASP.NET forums is how to deal with exceptions like The GridView 'GridView1' fired event RowEditing which wasn't handled. The GridView 'GridView1' fired event PageIndexChanging which wasn't handled. The GridView 'GridView1' fired event Sorting which wasn't handled. The GridView 'GridView1' fired event RowDeleting which wasn't handled. when manually databinding a GridView.  When I say manually databinding I mean not using a data source control specified as a DataSourceID, but rather setting the GridView’s DataSource equal to the appropriate data object and calling DataBind.  Developers who were around before ASP.NET 2.0 are familiar with how to deal with this, but since ASP.NET 2.0 most of the examples and tutorials deal with setting the DataSourceID, which buys you a lot of automation that you may not even appreciate unless you’ve done this the old fashioned way.  In this example, we’ll go through a fully featured GridView with editing, deleting, selecting, sorting, and paging functionality that is manually bound and identify the limitations and some of the workarounds. When you use a data source control (e.g. SqlDataSource, ObjectDataSource, LinqDataSource, etc) specified in a DataSourceID, the GridView can automate many functions because, through the data source control, the GridView on its own can perform the following operations: Select data (required any time databinding is required) Update data Delete data Insert data (not necessarily applicable for a GridView, but it is for other data controls) When you manually bind data to a GridView, the GridView itself cannot perform these operations, so they must be implemented in your code.  The GridView does know the events that should be used to implement them, so the exceptions shown above are thrown when you try to perform one of these operations, but you have implemented the appropriate event handler. Working With Other Data Controls The example shown here is for a GridView, but the same types of event handlers must be implemented in roughly the same way for other data controls including the FormView, DetailsView, and ListView.  I’ll try to note any major differences. The general philosophy you’ll see here is: The “-ing” event handlers (e.g. RowEditing, RowUpdating, RowDeleting, Sorting) must be implemented The “-ed” event handlers (e.g. RowUpdated, RowDeleted) do not have to be implemented, and in many cases these events are never raised. To demonstrate the details, I’ve created a very simple table ([UserID] INT, [FirstName] VARCHAR, [LastName] VARCHAR), which we’ll display with the following GridView: 1: <asp:GridView ID="GridView1" runat="server" 2: AllowSorting="true" AllowPaging="true" PageSize="5" 3: AutoGenerateColumns="false" DataKeyNames="UserID" 4: OnPageIndexChanged="GridView1_PageIndexChanged" 5: OnPageIndexChanging="GridView1_PageIndexChanging" 6: OnRowCancelingEdit="GridView1_RowCancelingEdit" 7: OnRowDeleting="GridView1_RowDeleting" 8: OnRowEditing="GridView1_RowEditing" 9: OnRowUpdating="GridView1_RowUpdating" 10: OnSorted="GridView1_Sorted" 11: OnSorting="GridView1_Sorting"> 12: <Columns> 13: <asp:BoundField DataField="UserID" HeaderText="ID" 14: SortExpression="UserID" ReadOnly="true" /> 15: <asp:BoundField DataField="FirstName" HeaderText="First Name" 16: SortExpression="FirstName" /> 17: <asp:TemplateField HeaderText="Last Name" SortExpression="LastName"> 18: <ItemTemplate><%# Eval("LastName")%></ItemTemplate> 19: <EditItemTemplate> 20: <asp:TextBox ID="LastNameTB" runat="server" 21: Text='<%# Bind("LastName") %>'></asp:TextBox> 22: </EditItemTemplate> 23: </asp:TemplateField> 24: <asp:CommandField HeaderText="Actions" ShowDeleteButton="true" 25: ShowEditButton="true" ShowSelectButton="true" /> 26: </Columns> 27: <AlternatingRowStyle CssClass="alt" /> 28: <SelectedRowStyle CssClass="selected" /> 29: <PagerStyle CssClass="pager" /> 30: <PagerSettings Mode="NextPrevious" NextPageText="Next >" 31: PreviousPageText="< Prev" /> 32: </asp:GridView> As you can see this GridView is fairly straightforward: 2 BoundFields 1 TemplateField with a template for display and editing (to show the difference later in retrieving data from a BoundField and a TemplateField) 1 CommandField with the appropriate buttons shown Paging allowed Sorting allowed DataKeyNames set to save the UserID This is the final GridView markup, so you can see the event handlers we’re going to implement.  We’ll start with binding data to the GridView, then build up functionality from there. Databinding the GridView First, we’ll implement a method to databind the GridView, which will be called whenever databinding needs to be performed. 1: protected void BindData() 2: { 3: DataClassesDataContext context = new DataClassesDataContext(); 4: var users = from u in context.Users 5: select u; 6: GridView1.DataSource = users.ToList(); 7: GridView1.DataBind(); 8: } We’re using Linq to SQL to pull data from our database and bind it to the GridView.  We’ll revisit this method when we implement the sorting functionality. We also need force databinding to occur when then page is loaded, which we’ll handle in Page_Load. 1: protected void Page_Load(object sender, EventArgs e) 2: { 3: if (!Page.IsPostBack) 4: { 5: BindData(); 6: } 7: } Paging the GridView Any time the page index is changed, the GridView must be databound, so we need to handle the appropriate event, which in this case is PageIndexChanging. 1: protected void GridView1_PageIndexChanging(object sender, GridViewPageEventArgs e) 2: { 3: GridView1.PageIndex = e.NewPageIndex; 4:  5: GridView1.EditIndex = -1; 6: GridView1.SelectedIndex = -1; 7: } The required actions for paging to work are setting the new page index, setting the DataSource, and calling DataBind (the last two through the BindData method).  In addition to that, we’re resetting the EditIndex and SelectedIndex when the page is changed to avoid cases, for example, where you change pages and are suddenly editing a different item.  BindData could be called in PageIndexChanging, but to show that it the event is raised, we’ve handled that in PageIndexChanged. 1: protected void GridView1_PageIndexChanged(object sender, EventArgs e) 2: { 3: BindData(); 4: } The important point is not which of these two events call BindData, but one of them must or the GridView will not be updated. Sorting the GridView Sorting is a little more complicated than paging.  When bound using a DataSourceID, the GridView internally tracks the current SortExpression and SortDirection, and those are available through properties with the same names.  When you manually bind the GridView, this tracking does not occur, so we must implement some mechanism for tracking those values.  We also want it to be something persistent, so that, for example, if you sort, then page, the sort is maintained.  The way we’ll accomplish this is to create two properties that store those values in ViewState. 1: protected string SortExpression 2: { 3: get 4: { 5: return ViewState["SortExpression"] as string; 6: } 7: set 8: { 9: ViewState["SortExpression"] = value; 10: } 11: } 12:  13: protected SortDirection SortDirection 14: { 15: get 16: { 17: object o = ViewState["SortDirection"]; 18: if (o == null) 19: return SortDirection.Ascending; 20: else 21: return (SortDirection)o; 22: } 23: set 24: { 25: ViewState["SortDirection"] = value; 26: } 27: } We’ll get to how these properties are populated shortly, but first we need to go back to the BindData method.  The original method shown above did not implement any sorting, but now that we have a place to store the SortExpression and SortDirection we need to change that. 1: protected void BindData() 2: { 3: DataClassesDataContext context = new DataClassesDataContext(); 4: var users = from u in context.Users 5: select u; 6:  7: bool sortAscending = 8: this.SortDirection == SortDirection.Ascending ? true : false; 9:  10: switch (this.SortExpression) 11: { 12: case "FirstName": 13: users = sortAscending ? users.OrderBy(u => u.FirstName) : 14: users.OrderByDescending(u => u.FirstName); 15: break; 16: case "LastName": 17: users = sortAscending ? users.OrderBy(u => u.LastName) : 18: users.OrderByDescending(u => u.LastName); 19: break; 20: default: 21: users = sortAscending ? users.OrderBy(u => u.UserID) : 22: users.OrderByDescending(u => u.UserID); 23: break; 24: } 25: GridView1.DataSource = users.ToList(); 26: GridView1.DataBind(); 27: } After specifying the query, we add the appropriate sorting.  We can implement it in this way without a performance impact because the data is not actually retrieved from the database until required, so we can build up a chain of queries up to that point and only the end result will be pulled from the database.  We’re sorting on the appropriate field based on the SortExpression, and setting the direction based on SortDirection by calling either the OrderBy or OrderByDescending methods. Finally we need to populate the SortExpression and SortDirection when they change, and we’ll handle that in the Sorting event. 1: protected void GridView1_Sorting(object sender, GridViewSortEventArgs e) 2: { 3: if (this.SortExpression == e.SortExpression) 4: { 5: this.SortDirection = this.SortDirection == SortDirection.Ascending ? 6: SortDirection.Descending : SortDirection.Ascending; 7: } 8: else 9: { 10: this.SortDirection = SortDirection.Ascending; 11: } 12: this.SortExpression = e.SortExpression; 13:  14: GridView1.EditIndex = -1; 15: GridView1.SelectedIndex = -1; 16: } The new sort expression is available through e.SortExpression based on the user’s selection.  There is a sort direction available through e.SortDirection.  When you bind a GridView through a DataSourceID, the GridView will update this value since it is internally tracking the sort expression, so it knows when to switch between ascending and descending.  When manually databinding the GridView, this will always show ascending, so we need to handle this manually with the following logic: If the SortExpression has not changed, reverse the SortDirection (switching between ascending and descending) If the SortExpression has changed, the SortDirection is ascending After that we save the new SortExpression.  As with paging, we also reset the EditIndex and SelectedIndex when sorting.  Also like paging, so show that it does get called, databinding is handled in the Sorted Event (honestly, we will get to some “-ed” events that are not raised). 1: protected void GridView1_Sorted(object sender, EventArgs e) 2: { 3: BindData(); 4: } Editing Records in the GridView For editing there are events that need to be handled: entering edit mode, cancelling out of edit mode, and updating from edit mode.  First, we’ll handle entering edit mode: 1: protected void GridView1_RowEditing(object sender, GridViewEditEventArgs e) 2: { 3: GridView1.EditIndex = e.NewEditIndex; 4: BindData(); 5: } This is the minimum we need to do to enter edit mode: set the new EditIndex and call BindData (which sets the GridView’s DataSource and calls DataBind). Cancelling out of edit mode is very similar. 1: protected void GridView1_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e) 2: { 3: GridView1.EditIndex = -1; 4: BindData(); 5: } Finally, we’ll handle the RowUpdating method to save the new values.  Most of the automation that’s provided by the GridView when bound using a DataSourceID is lost when manually databinding: The original values are not available through e.OldValues.  The new values are not available through e.NewValues. The DataKeys for the affected row are not available through e.Keys Basically, all the GridView does for you is pass the EditItemIndex through e.RowIndex and dealing with everything else is up to you. 1: protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e) 2: { 3: // e.Keys, e.NewValues, and e.OldValues are only populated if using DataSourceID 4: int id = Int32.Parse(GridView1.DataKeys[e.RowIndex].Value.ToString()); 5:  6: DataClassesDataContext context = new DataClassesDataContext(); 7: var user = (from u in context.Users 8: where u.UserID == id 9: select u).Single(); 10:  11: TextBox firstNameTB = GridView1.Rows[e.RowIndex].Cells[1].Controls[0] as TextBox; 12: user.FirstName = firstNameTB.Text; 13:  14: TextBox LastNameTB = GridView1.Rows[e.RowIndex].FindControl("LastNameTB") as TextBox; 15: user.LastName = LastNameTB.Text; 16:  17: context.SubmitChanges(); 18:  19: GridView1.EditIndex = -1; 20: BindData(); 21: } First, we obtain the UserID of the affected row from the GridView DataKeys collection, which we use to pull the appropriate record from the database.  At that point we need to get the new values so we can update the record, which is done slightly differently for the BoundField and the TemplateField.  For the BoundField, we don’t know the ID of the TextBox since it’s autogenerated by the BoundField, so we obtain a reference to it by getting the first control in the appropriate cell in the edited row.  Since it’s a BoundField, the TextBox is going to be Control[0]. Once we have a reference to the TextBox, we set the new first name. For the TemplateField, we do know the ID of the control since we specified it in the markup, so we obtain a reference to it by calling FindControl on the appropriate row of the GridView.  The GridViewRow is the naming container for the TextBox in this case, so we need to call FindControl on the appropriate row, but we do not need to call it on the specific cell.  Once we have a reference to the TextBox, we set the new last name and save the changes (through the SubmitChanges method). Editing With a BoundField When a BoundField is displaying data (not in edit mode), the appropriate text is added to the Text property of the appropriate cell. When the BoundField is in Edit mode, things are a little different. In that case a TextBox is created and added to the Controls collection for the appropriate cell (and it's the only item added to the Controls collection, so it's Controls[0]). If we were using a TemplateField instead, the compiler translates the markup in the templates to the appropriate types of controls in the same way as the page markup in parsed.  For this example, the ItemTemplate would be transformed into a Literal control, and the EditItemTemplate would be transformed into a Literal control, a TextBox, and a Literal control (the literal controls would include the spacing between the EditItemTemplate tags and the TextBox tags).  In both cases, the controls are added to the controls collection for the cell.  You can see that in the case of a TemplateField, the exact spacing within the control becomes significant, so it’s generally better to get a reference to the control by ID using FindControl than to use the Controls collection. Once the changes are saved, we need to do a little clean up on the GridView.  We reset the EditIndex, and we must again call BindData.  You’ll note we did not accomplish any of this in the RowUpdated event because that event is never fired when manually databinding to a GridView. Deleting Records in the GridView As with updating, most of the automation that’s provided by the GridView when bound using a DataSourceID is lost when manually databinding: The original values are not available through e.Values. The DataKeys for the affected row are not available through e.Keys Basically, all the GridView does for you is pass the RowIndex of the row to delete through e.RowIndex and dealing with everything else is up to you, so it’s very similar to updating. 1: protected void GridView1_RowDeleting(object sender, GridViewDeleteEventArgs e) 2: { 3: // e.Keys and e.Values are only populated if using DataSourceID 4: int id = Int32.Parse(GridView1.DataKeys[e.RowIndex].Value.ToString()); 5: DataClassesDataContext context = new DataClassesDataContext(); 6: var user = (from u in context.Users 7: where u.UserID == id 8: select u).Single(); 9: context.Users.DeleteOnSubmit(user); 10: context.SubmitChanges(); 11:  12: BindData(); 13: } First, we obtain the UserID of the affected row from the GridView DataKeys collection, which we use to pull the appropriate record from the database.  We then delete the record, and call BindData.  As with updating, none of this is handled in the RowDeleted event because that event is never fired when manually databinding to a GridView. Selecting Records in the GridView Selecting is actually the easiest of all of these to implement, all you have to do is – nothing.  Selecting will work without any code.  The reason is that selecting in a GridView does not cause a databind, it simply changes styles related to the selected row.  Note this behavior is not the same for the ListView.  Since the ListView can have a separate template for the selected item, and this may contain different data, the ListView does perform a databind when changing the selected index. Summary This article has detailed the type of code that must be written when manually databinding a GridView with paging, sorting, editing, deleting, and selecting capability. Download sample website: (243 kb) Updated April 26, 2009: Example updated to change one of the BoundFields to a TemplateField to the show the differences in retrieving data between the two types of fields. Updated May 11, 2009: Added VB.NET example.
Tags: ,
Categories: Data Controls
E-mail | Kick it! | DZone it! | Permalink | Comments (37) | Post RSSRSS comment feed