devxlogo

Write Your Own Provider For the ASP.NET DataGrid

Write Your Own Provider For the ASP.NET DataGrid

When was the last time you used the ASP.NET DataGrid control with something other than an ADO.NET provider? Chances are, like many developers, you haven’t. The DataGrid control has such a well-publicized relationship with ADO.NET that it’s difficult to imagine it doing anything useful on its own. Yet, by understanding and programming to the interfaces the DataGrid uses to obtain data, you can turn it into an extremely flexible tool that can display any type of information on a web page.

By now, every ASP.NET programmer is familiar with the following two lines of code:

   myGrid.DataSource = mySqlDataReader;   myGrid.DataBind();

The first line tells the grid control to accept the DataReader mySqlDataReader as the data source, and the second line instructs it to go ahead and display whatever is in the reader in an HTML table. The key to getting the data grid to accept a custom provider is to know which interfaces of ADO.NET it uses to obtain the data, and then developing your code to implement those interfaces. Although there are a variety of such constructs you can choose from, the most suitable one for a read-only presentation is the IEnumerable interface, which you’ll find in the System.Collections namespace.

To implement IEnumerable, you need to override only one member function, GetEnumerator(), whose sole purpose is to return an IEnumerator object, which will allow the grid control to “walk through” your data. The slightly more complex IEnumerator interface consists of three member functions, each of which implicitly assumes that you maintain an index pointer to keep track of the current item being enumerated. The three members are:

Reset(): This method moves the index pointer to point to the initial position in the enumerated collection, regardless of its current position. You should invoke this method yourself before handing your provider to the grid, so that the control knows exactly where to start traversing your data.

MoveNext(): This method moves the pointer to the next position. If the new position is final, or if you are already on the final position, the method should return false, signaling to the caller that there is no more data. In all other cases, return true.

Current(): This property should return the object pointed to by the index pointer. The ASP.NET DataGrid invokes this property after every successful call to MoveNext(). To populate the grid correctly, your code should create an object that represents the current row and return it to the grid for rendering.

If the DataGrid calls Current() at the initial or the final positions, your implementation should raise an InvalidOperationException. In practice, this exception suggests a bug in your MoveNext() method, which is probably not returning false when the pointer moves past the end of the list, leading the grid to think there is more data to be retrieved, whereas in fact it has already retrieved all the rows.

There are no special requirements on the object returned by Current(), except that the names of its public properties should correspond to column names, and the property values should correspond to the actual value of the column in question.

Developers familiar with ADO will immediately recognize this structure as very similar to the forward-only/read-only recordset model, as shown in the state transition diagram of Figure 1. To get the information from your data source, the grid uses an algorithm similar to the one represented by the flowchart in Figure 2.

Figure 1. IEnumerator State TransitionsFigure 2. Grid Enumeration Algorithm

Note that, in the flowchart, the grid never calls the Reset() method, meaning that it is your responsibility to call this method before you hand over your provider to it. Failure to do so will result in an uninitialized data source, and you will most likely witness a mysterious loss of rows during runtime.
Putting Theory into Practice
Here’s a practical example. The problem is: How can you display this XML structure in a table on a web page?

                        

Well, you can always use the good old System.Data namespace. Yet, why use a library primarily intended for SQL to deal with XML? Try a whole new approach. Chances are, you will find it simpler and more intuitive.

You can start with the structure that will provide column information to the grid. As mentioned in the preceding section, all you need is a bare-bones class that exposes the columns through its properties. This class will receive the values for all its properties through its constructor, and will store them in private member variables until its public properties are queried by the grid.

   public struct Region {     private string rId;     private string rDesc;        public Region(string regionId,        string regionDescription) {       rId = regionId;       rDesc = regionDescription;     }      public string RegionId {       get {         return rId;       }     }        public string RegionDescription {       get {         return rDesc;       }     }   }

Next, define the provider class. You need a member variable to store the XML root node, and a current pointer to keep track of your position during enumeration.

   public struct RegionProvider : IEnumerable,       IEnumerator {      private int current;      private XmlNode root;         public RegionProvider(XmlDocument document) {      current = -1; // Reset()      root = document.DocumentElement;   }   

Because this class implements both IEnumerable and IEnumerator, the GetEnumerator() method just returns the object itself. Under more complex circumstances, you may want to put the enumeration logic in a separate class, but for the time being, this approach will suffice.

   // IEnumerable interface   public IEnumerator GetEnumerator() {      return this;   }

IEnumerator is where you perform the real act. Note how you return false after the final element in MoveNext(), signaling the grid to stop enumeration after that point. Also, any exception occurring within Current() is almost certainly due to a buggy client that has disregarded the return value from MoveNext(), so you can safely raise the InvalidOperationException.

   // IEnumerator interface   public void Reset() {      current = -1;   }      public bool MoveNext() {      current++;      return current < root.ChildNodes.Count;   }      public object Current {     get {       try {         XmlNode node = root.ChildNodes[current];         return new Region(node.Attributes[0].Value,             node.Attributes[1].Value);       } catch(Exception) {       }       throw new InvalidOperationException();     }   }

The final step is to feed the XML document to the provider, and let the ASP.NET DataGrid manipulate it.

   protected void Page_Load(Object Src, EventArgs E) {     if (!IsPostBack) {       // Load string       string str = "";       str += "";       str += "";       str += "";       XmlDocument doc = new XmlDocument();       doc.LoadXml(str);       grid.DataSource = new Provider(doc);       grid.DataBind();     }   }
Figure 3. Your RegionProvider in Action

Voila! As the screenshot shows, when you make the DataGrid happy, it returns the favor by shielding you from the complexities of HTML tables, and helps you keep presentation logic separate from data access logic.

Even this relatively simple example shows obvious benefits compared to using the DataView object from the System.Data namespace. Your custom provider entirely encapsulates the data you want to present, so the grid has no idea where the data is coming from, resulting in flexibility. You can change the physical source of your XML without having to touch the presentation logic. You need only modify your provider to fit the source, and the page will work seamlessly.
Determining Properties Dynamically
No solution has ever been perfect in the world of software engineering, and the previous example is no exception. In fact, it has a very significant shortcoming: Now that the DataGrid isn't discovering the column structure, it means you have to know the structure of your columns before you can deploy the solution. That means that if the XML schema changes, you would have to recompile your code. Worse, you would have to create a new class for every possible XML schema, which is outright impossible. In addition, your column names are limited to valid identifiers of the Common Language Runtime (CLR) languages, so even a column name as common as First Name would be illegal because it contains a space.

It would be great if the grid obtained column information from one of the built-in lookup structures of the .NET collection framework, for example, a Dictionary or HashTable, but unfortunately, the framework doesn't work that way. The grid control always uses the properties of the provider class to get the column names?unless your class implements the ICustomTypeDescriptor interface, in which case, the DataGrid queries that interface first. That behavior gives you the opportunity to dynamically define your column information at runtime.

ICustomTypeDescriptor, by itself, is a very detailed interface, providing type information for every conceivable aspect of a .NET component. However, it turns out that the DataGrid only needs the GetProperties() member function; therefore you can safely provide a minimal implementation for the remaining members.

Your GetProperties() method must build a list of the column names that you want the grid to display, and must return this information as a PropertyDescriptorCollection object, which is essentially a structured collection of PropertyDescriptor objects. The PropertyDescriptor class has a few abstract methods, so you must derive your own class from it, and override the abstract methods appropriately.

With this information at hand, start by creating a ColumnStructure class. Note that you must call the base class constructor with the name parameter, which names the column. Also note that the name you provide when creating the new ColumnStructure is fixed?you cannot rename it, because there's no public Name property for your code to set later.

     public class ColumnStructure : PropertyDescriptor {       private string name;          public ColumnStructure(string name) :          base(name, null) {            this.name = name;       }

The ColumnStructure class will not be serializable, and is read-only, so you can provide empty implementations for the methods the grid will not call.

       public override bool IsReadOnly {         get {           return true;         }       }          public override void SetValue(object component, object value) {       }          public override bool CanResetValue(         object component) {         return false;       }          public override void ResetValue(         object component) {       }          public override bool ShouldSerializeValue         (object component) {            return false;       }

The ComponentType method should return a Type object for the component that hosts the property. In this example, the RowStructure class, described in detail later in this article, is the container.

       public override Type ComponentType {         get {           return typeof(RowStructure);         }       }

On the other hand, the PropertyType method returns the type of the property itself. Here, both regionId and regionDescription are strings.

       public override Type PropertyType {         get {           return typeof(string);         }       }

When the grid needs to obtain the value of a column, it calls the GetValue() method, and passes in the row object. This means that your property must know how to retrieve its value by examining the component passed to it.

      public override object GetValue(         object component) {         return ((RowStructure)component).Node.Attributes           [name].Value;       }     }

Next comes the RowStructure class. As you can guess, a RowStructure instance stores the data for the entire row, and acts as the binding element between your provider and ColumnStructure. Note how the code defines the public property Node solely to enable a ColumnStructure to examine the node in the GetValue() method.

     public struct RowStructure : ICustomTypeDescriptor {       private XmlNode node;          public RowStructure(XmlNode node) {         this.node = node;       }          public XmlNode Node {         get {           return node;         }       }

The GetProperties() method of the ICustomTypeDescriptor interface is how you can return a customized property set. This method is where you analyze your row, and return its column information.

       // ICustomTypeDescriptor interface       // ....       public PropertyDescriptorCollection          GetProperties() {            PropertyDescriptor[] pd = new                PropertyDescriptor[node.Attributes.Count];         for (int i = 0; i < node.Attributes.Count; i++)            pd[i] = new                ColumnStructure(node.Attributes[i].Name);            return new PropertyDescriptorCollection(pd);       }     }

The two classes you have seen so far provide the bulk of the implementation. The RegionProvider class from the previous scenario essentially stays the same, since the way the XML file is traversed remains unchanged. The only difference is, each time the DataGrid invokes the Current() property, your code returns a RowStructure object to the grid, rather than a Region.

   public object Current {      get {      try {         XmlNode node = root.ChildNodes[current];         return new RowStructure(node);       } catch(Exception) {       }       throw new InvalidOperationException();     }

The infrastructure to dynamically determine property names can be confusing. Yet, the basics are quite straightforward, as illustrated in Figure 4.

Figure 4. Dynamic Property Determination

After these improvements, the provider is a lot more flexible than it was. You can now change the attributes of your rows at will without going through the trouble of modifying your Region class each time. As you have seen, creating a custom data provider frees you from reliance on ADO.NET and lets you populate the DataGrid with fewer intermediaries, enabling a more structured relationship between the DataGrid and the real source of your data.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist