Sunday, November 21, 2004

At the heart of our production application is a paradigm of a work order. As one can surmise, the real-life work order has quite a complex data structure. In our case it is a chunk of XML couple of thousands of bytes in size. When you need to edit this XML in your app, you have to keep writing long and unpleasant queries like this:

txtWONumber.Text = objWO[”WorkOrder”].GetAttribute[”Number”];
txtWOTask.Text = objWO[”WorkOrder”][”Asset”][”Task”].InnerText;

Of course in part this is caused by lack of XQuery support in CF 1.0, but still, later you have to write the update code and make sure you haven't forgotten to call it. And if you need to populate a listbox or a datagrid from a list of XML nodes, you are quite out of luck and in for some manual work.

And yet, the databinding underneath is highly automated and quite extensible, so let's see what we can do to make it understand XML data.

First of all, we will decide on what is our data unit (component). It will be an XML node. Component properties could be either attributes or subnodes, or even any valid XQuery expression - anything that returns data. A logical choice of data source is an XML node list (not an array). Such a list is easily obtained from an XML document via ChildNodes property of its document element.

I am not going to go into details of how the databinding works for there are many good sources that cover this process1. Instead I will simply say that at the root of custom databinding is an object derived from a class called PropertyDescriptor. We are going to derive our own class from it and call it XmlPropertyDescriptor.

Let's set some gorund rules. We want to be able to take an xml document like this:

xml version="1.0" encoding="utf-8" ?>
<
bookstore xmlns:bk="urn:samples">

<book genre="novel" publicationdate="1997" bk:ISBN="1-861001-57-6">

<title>Pride And Prejudicetitle>

<price>24.95price>

book>

<book genre="novel" publicationdate="1992" bk:ISBN="1-861001-45-3">

<title>The New Dawntitle>

<price>29.95price>

book>

<book genre="novel" publicationdate="1991" bk:ISBN="1-861001-57-8">

<title>Blue Smoketitle>

<price>19.95price>

book>

bookstore>

and bind to a list of “book” items using display expression like “title” or “@bk:isbn" . If we had more complex book item structure, the binding name could be a more complex path - “author/Name/@First”. What we do not want to do (since we don't really have XQuery/XPath support in CF) is to allow relative paths, functions and queries. Moreover, we will say, that in the path, every element must be a node name, except of the last one, that can be an attribute name (starting with @ ).

When you derive from the PropertyDescriptor class there are few things to keep in mind:

  1. Override IsReadonly property. This will tell the framework whether your property supports updates
  2. Override PropertyType property to return typeof(string) - this is our property type; we don't do any data conversions here. If we want data conversion/validation, we can implement it in Format/Parse events of the binding. We could to some data validation and type conversion via reading the schema (if available), but it would be outside of the scope of this article.
  3. Override ComponentType property to return typeof(XmlNode). This is the only component type we deal with.
  4. Override GetValue method. This is the most important method. It actually goes ahead and retrieves the property value given the component. In our case it gets the XmlNode (and already has the “XPath” by way of constructor parameter). Our task is to perform the query. Very simple on the desktop. A bit more complex in CF.
  5. Override SetValue method. It receives the component and the new value and sets it using the known “Xpath”.

Now, wouldn't it be nice to make the binding use our XmlPropertyDescriptor class? Simple - we just need to create our own Data Source that would report XmlPropertyDescriptors instead of whatever is used by CurrencyManager. Whatever is usually a SimplePropertyDescriptor class, ReflectPropertyDescriptor or DataColumnPropertyDescriptor.

This brings us to building our own data source object. There is no abstract class to override. When creating it, we need to make sure we implement several interfaces:

  • ITypedList
  • IEnumerable
  • IList
  • IBindingList

ITypedList::GetItemProperties creates a bit of a problem. It is supposed to return Property descriptor collection populated with all properties the data unit exposes. Unfortunately in our case properties are valid xpath expressions, which could be quite a few, expecially on a complex XML document

The reason we need to implement ITypedList::GetItemProperties is that we need for example a data grid to be able to get populated automatically, without us specifying XPath expression for each column. The problem here is that we don't want to build a full list of all possible xpath expressions valid on our XML item, so we need to decide on some way to simplify it. Let's limit the autogenerated property list to all attributes or all 1-st level child elements. If the XML is more complex than that, well, you need to specify the grid columns explicitly.

The IEnumerable and IList are simply delegated to the underlying XmlNodeList. IBindingList implementation is somewhat simplistic as we don't really want to figure out how to track the external changes in the XmlNodeList.

We end up with a class that allows us to take the XML document above and write things like:


doc.Load("data.xml");

XmlDataSource src = new XmlDataSource(doc.DocumentElement.ChildNodes, XmlDataSourceMode.DataSourceModeAuto);

txtGenre.DataBindings.Add("Text", src, "@genre");

txtISBN.DataBindings.Add("Text", src, "@bk:ISBN");

txtPrice.DataBindings.Add("Text", src, "price");

txtPubDate.DataBindings.Add("Text", src, "@publicationdate");

txtTitle.DataBindings.Add("Text", src, "title");

 

listBox1.DisplayMember = "title";

listBox1.ValueMember = "@bk:ISBN";

listBox1.DataSource = src;

dataGrid1.DataSource = new XmlDataSource(doc.DocumentElement.ChildNodes,

new string[] { "@genre", "title/#text", "price/#text" });

This produces a screen like this one:

 

Selecting a list box item moves the current position along the node list. Data edits are also supported.

The source code for this article can be found here.

11/21/2004 8:41:11 PM (Pacific Standard Time, UTC-08:00)  #    Comments [0]  |