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
fixedyou 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.