Data-Aware Controls
Not all controls work well with data binding. For example, the popular TreeView and ListView controls need to be filled manually. In other circumstances, you may have controls that support data binding, but you want to take control of the entire process. Maybe you want to create a control that can't be filled all at once, but uses partial data reads or just-in-time queries to allow a user to browse through a large amount of data..NET provides many opportunities for data integration without data binding. One handy technique is using the Tag property. Every control provides the Tag property, but the .NET framework doesn't use it. Instead, you can use the Tag property to store any information or object you need. For example, you could use this property to store the relevant business object with each node in a TreeView, or a DataRow object with each row in a ListView.The next example shows a TreeView that embeds the data it needs to use the Tag property of each node. Here's the code needed to fill the TreeView (which could be placed in the Form.Load event handler):
DataSet dsStore = new DataSet();
dsStore.ReadXmlSchema(Application.StartupPath + "\\store.xsd");
dsStore.ReadXml(Application.StartupPath + "\\store.xml");
// Define the relation.
DataRelation relCategoryProduct = new DataRelation("Products in this category",
dsStore.Tables["Categories"].Columns["CategoryID"],
dsStore.Tables["Products"].Columns["CategoryID"]);
dsStore.Relations.Add(relCategoryProduct);
TreeNode nodeParent, nodeChild;
foreach (DataRow rowParent in dsStore.Tables["Categories"].Rows)
{
// Add the category node.
nodeParent = treeDB.Nodes.Add(rowParent["CategoryName"]);
// Store the disconnected category information.
nodeParent.Tag = rowParent;
foreach (DataRow rowChild in rowParent.GetChildRows(relCategoryProduct))
{
// Add the product order node.
nodeChild = nodeParent.Nodes.Add(rowChild["ModelName"]);
// Store the disconnected product information.
nodeChild.Tag = rowChild;
}
}
When a node is selected, a generic code routine reads the accompanying DataRow and displays all the information it contains in a label. (This code reacts to the TreeView.AfterSelect event.)
private void treeDB_AfterSelect(object sender,
System.Windows.Forms.TreeViewEventArgs e)
{
lblInfo.Text = ";
DataRow row = (DataRow)e.Node.Tag;
foreach (object field in row.ItemArray)
{
lblInfo.Text += field.ToString() + "\n";
}
}
The result, shown in Figure 9-22, is a TreeView that has easy access to the information for each node.

Figure 9-22: A TreeView with embedded data
A Decoupled TreeView with Just-in-Time Nodes
The preceding TreeView example requires very little information about the data source. Instead, it loops through the available fields to display a list of information. However, in doing so the control also gives up the ability to format the data in a more acceptable format. For example, fields that aren't important are always displayed, and the field order is fixed.There is an elegant way to solve this problem. The next example shows a TreeView that still embeds data, but relies on a ProductDatabase class to transform the DataRow fields into display information. An instance of the ProductDatabase class is created as form-level variable:
private ProductDatabase DataClass = new ProductDatabase();
Thanks to the ProductDatabase class, the TreeView doesn't need to handle the table hierarchy. Instead, it begins by filling the tree with a list of categories and adds dummy nodes under every level.
private void TreeViewForm_Load(object sender, System.EventArgs e)
{
TreeNode nodeParent;
foreach (DataRow row in DataClass.GetCategories().Rows)
{
// Add the category node.
nodeParent =
treeDB.Nodes.Add(row[ProductDatabase.CategoryField.Name].ToString());
nodeParent.ImageIndex = 0;
// Store the disconnected category information.
nodeParent.Tag = row;
// Add a "dummy" node.
nodeParent.Nodes.Add("*");
}
}
When a node is expanded and the TreeView.BeforeExpand event fires, our code calls the ProductDatabase with the expanded node, and requests more information. The ProductDatabase class then returns the information needed to add the appropriate child nodes.
private void treeDB_BeforeExpand(object sender,
System.Windows.Forms.TreeViewCancelEventArgs e)
{
TreeNode nodeSelected, nodeChild;
nodeSelected = e.Node;
if (nodeSelected.Nodes[0].Text == "*")
{
// This is a dummy node.
nodeSelected.Nodes.Clear();
foreach (DataRow row in
DataClass.GetProductsInCategory((DataRow)nodeSelected.Tag))
{
string field = row[ProductDatabase.ProductField.Name].ToString());
nodeChild = nodeSelected.Nodes.Add(field);
// Store the disconnected product information.
nodeChild.Tag = row;
nodeChild.ImageIndex = 1;
nodeChild.SelectedImageIndex = 1;
}
}
}
When an item is selected, the code again relies on the ProductDatabase class to "translate" the embedded DataRow. In this case, the code responds to the TreeView.AfterSelect event:
private void treeDB_AfterSelect(object sender,
System.Windows.Forms.TreeViewEventArgs e)
{
lblInfo.Text = DataClass.GetDisplayText((DataRow)e.Node.Tag);
}

Figure 9-23: A decoupled TreeView
This pattern allows the ProductDatabase to handle its own data access strategy—it can fetch the information as needed with miniqueries every time a node is expanded, or it can retain it in memory as a private member variable (as it does in this example). Even better, the ProductDatabase code is extremely simple because it doesn't need to convert ADO.NET objects into "business" objects. The TreeView can use and embed the ADO.NET objects natively, without needing to know anything about their internal field structures.
public class ProductDatabase
{
public class Tables
{
public const string Product = "Products";
public const string Category = "Categories";
}
public class ProductField
{
public const string Name = "ModelName";
public const string Description = "Description";
}
public class CategoryField
{
public const string Name = "CategoryName";
}
private DataSet dsStore;
DataRelation relCategoryProduct;
public ProductDatabase()
{
dsStore = new DataSet();
dsStore.ReadXmlSchema(Application.StartupPath + "\\store.xsd");
dsStore.ReadXml(Application.StartupPath + "\\store.xml");
// Define the relation.
relCategoryProduct = new DataRelation("Prod_Cat",
dsStore.Tables["Categories"].Columns["CategoryID"],
dsStore.Tables["Products"].Columns["CategoryID"]);
dsStore.Relations.Add(relCategoryProduct);
}
public DataTable GetCategories()
{
return dsStore.Tables["Categories"];
}
public DataRow[] GetProductsInCategory(DataRow rowParent)
{
return rowParent.GetChildRows(relCategoryProduct);
}
public string GetDisplayText(DataRow row)
{
string text = ";
switch (row.Table.TableName)
{
case Tables.Product:
text = "ID: " + row[0] + "\n";
text += "Name: " + row[ProductField.Name] + "\n\n";
text += row[ProductField.Description];
break;
}
return text;
}
}
The ProductDatabase methods could easily be used with other controls. None of them are specific to the TreeView.
Can There Be a Data-Bound ListView Control?
It seems like dealing with data is always a compromise. You can have a fullfeatured control that supports flexible data binding and lacks user interface niceties, like the DataGrid, or a more attractive ListView or TreeView that doesn't have any intrinsic support to display information from a data source. Wouldn't an ideal solution combine both of these worlds and create a ListView or TreeView that can bind to any data source?The short answer is no. Programmers have developed ListView controls that can automatically display DataTable information, and TreeView controls that can accept DataSets and show a master-details list by inspecting the table relations. But these custom controls are rarely flexible enough to be used in a real application. Their intelligence is remarkable, but once you start to work with them, you repeatedly stumble across basic limitations. For example, in a data-bound ListView there would be no easy way to set column widths and ordering. This type of information can't be stored in a DataSet or DataTable object, and even if it could, it might vary with the display font or the current user's preferences. Similarly, a data-bound TreeView would have no support for multiple groupings. A just-in-time node solution (like you saw in the previous example) can't be implemented because the data-bound TreeView requires a completely configured DataSet.These limitations are not trivial. The DataGrid solves them partially by providing another layer of indirection with DataGridColumnStyle classes. These styles allow you to configure the display appearance of the data separately from the data itself. Even still, the DataGrid lacks many formatting and display niceties. To work with a richer control like the TreeView would require the development of a similar framework. As an undertaking, it would be far more difficult than creating a customized TreeView that's tailored for your type of data.In short, the best approach is to design your control to suit your data strategy. No single control can support every type of data, and no data-binding framework can accommodate every possible way data binding can be used. If you are creating a control that needs to support several different ways of interacting with data, follow the design explained with the decoupled TreeView. This control allows you a maximum of programming convenience, with the flexibility to change your controls or your data access strategy later.