Aaron Goldenthal

Sometimes ASP.NET is Rocket Science

Creating a Data Control Field – The CounterField

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: CounterField.zip (224 kb)