Databinding in ASP.NET is simply nice. Create a datagrid, assign a DataSet to its DataSource property. Done. If one has got an XML document, just put it in a dataset, done as well.
Unfortunately there are XML structures a dataset can't stand, e.g. recursive structures. Of course one could redesign the structure of that document, but what if that structure already exists and you have no choice? (Or you are just a little bit bullheaded - like me :-) )
First you start searching the internet and you will probably stumble over a bunch of examples for databinding and XML - all doing the boilerplate "put XML into dataset, bind to dataset".
Next you start searching through the .NET Framework documentation. very thoroughly, very deep down. The following is what I found out when I did exactly that - and it's not always been easy.
Databinding of an ASP.NET DataGrid provides several features:
This works just fine with datasets and equally for flat lists with objects, using the properties as column contents. But if you want to bind to more complex object relationships or to none-properties, not to mention XML, things start to get complicated.
Let's start at the beginning...
Take this code snippet taken from an ASP.NET page
grid1.DataSource= XYZ; grid1.DataBind();
XYZ has to implement an interface based on IEnumerable. The .NET Framework documentation also mentions the following interfaces in different places:
The list entries provide not only the data itself, they also provide the information about the columns that should get created automatically:
The datagrid asks for that information more than once during its construction. And it also uses the information to get the actual data values (how it does that is described further down).
Take a collection (Array, CollectionBase, .) with some object references as content. Binding to such a list is the simplest case of databinding.
If you bind to that list you'll get a grid with the object's property's contents in columns, using the property's Name as column header. I guess it's more or less clear how the grid does that work using reflection.
All together, the grid did the following tasks:
An object (i.e. the collection class) that wants to change that behavior (e.g. use different texts as column headers) can do this by implementing ICustomTypeDescriptor.
The result of ICustomTypeDescriptor.GetProperties() is a PropertyDescriptorCollection. The significant information of a PropertyDescriptor is the Name of that "property". Additionally the PropertyDescriptor class has to overwrite GetValue. At some later point in time the grid will call that method providing the current list entry (i.e. a single data line in the grid). GetValue should then provide its actual "property content" which will be put into the current cell.
The adjusted version of the grid's task list in this case looks like this:
![]()
A simple example (actually taken from the .NET Framework documentation).
Binding to a Dataset utilizes the mechanism just described. Therefore it's not the most simple case to look at, on the other hand we can quickly walk through that scenario.
A DataView (respectively a DataTable) implements IList/IListSource. The single list entries are of type DataRowView. Since DataRowView can't provide own properties (at least not with the content just queried from the database) it implements ICustomTypeDescriptor to dynamically provide information on the fields of its dataview/datatable.

A simple example for binding to a DataSet
For XML there is no infrastructure similar to the one found for DataView/DataRowView within the .NET Framework. If one binds to a XmlNodeList (selectNodes) you will get a grid - filled using the described mechanism - containing the content of the properties of each xml node (HasChildNodes, InnerText, etc.) - not necessarily the content one wants to see.
Ergo the work the FCL has done for datasets has to be done for XML documents - maybe adjusted to different needs - by one self.
Let's start with what we want to achieve (always a good strategy if one wants to produce reusable code).
I took an XML document which I used as menu content (test.xml) consisting of groups and entries with Names, links and descriptions. Groups also had Names and could be grouped together, therefore creating a recursive structure. (This may not necessarilly make sense in that context, I just choose an XML document a dataset can't stand). Now it would be nice if the following code snippet worked as expected:
private void Test()
{
XmlDocument xml= new XmlDocument();
xml.Load(Server.MapPath("test.xml"));
XmlDataView dv= new XmlDataView(xml, "//Entry");
// doc, query // column auto creation: available properties:
// dv.AutoAddElements= true; // !!! data.driven, not schema-driven // dv.AutoAddAttribs=
true; // !!! data.driven, not schema-driven // dv.AutoAddSelf= true; //explicitely
adding columns: //Syntax: dv.AddColumn(Name, xpath);
dv.AddColumn("Name");
dv.AddColumn("Group ", "../../@Name");
dv.AddColumn("Group", "../@Name");
dv.AddColumn("Description", "Description");
//dv.AddColumn("ID", "@ID");
grid.DataSource= dv;
}
Of course the snippet doesn't even compile. There is no class Named XmlDataView within the .NET Framework.
Well, that was the goal. Let's look what we have to do to make it a reality.
The classes responsibilities in detail:
The complete source code for this can be found in XmlDataBinding.cs. One can create a view on any XML document that comes along using any XPath that makes sense (well, . ;-) ). Columns can be created using relative xpath statements. Columns can be created automatically (currently based on the data, more on this later).
XmlDataView has two worker methods that get called on demand: Query() and BuildProps().
Query() takes the XML Document and uses SelectNodes and the provided xpath to get the resultset. However the resulting XmlNodeList doesn't get stored as it is. Rather for each XmlNode a XmlDataRowView gets created, takes the XmlNode as reference, and gets stored in an array. These list entries will become the lines in our grid.
protected void Query()
{ if (m_alData!=null) return ; XmlNodeList nl= m_xml.SelectNodes(m_sQuery); // translate XMLNode to XmlDataRowView (holds reference of XmlNode) m_alData= new ArrayList(nl.Count);
for(int i= 0; i<nl.Count; ++i) { XmlNode node= nl.Item(i); m_alData.Add(new XmlDataRowView(this, node)); } }
Since XmlDataRowView manages one line of the grid it has to implement ICustomTypeDescriptor. ICustomTypeDescriptor carries a lot of methods around, but the only one that's interesting is GetProperties(). Instead of building a PropertyDescriptorCollection for each row, GetProperties() calls a similar method in XmlDataView. This method calls BuildProps() to dynamically create the column information.
For this technical proof-of-concept prototype a simple algorithm was choosen to build the column information. The first xml node from the resultset is taken. Each element and each attribute is used to create a column (further controlled with the AutoAddXY properties). A further advanced version would probably analyze the schema information.
BuildProps() takes the first entry from the list of XmlDataRowViews. It iterates over the elements and attributes and creates a column by calling the method AddColumn().
AddColumn() creates a XmlDataViewCol object providing a Name and a relative xpath to get the data - for our automatically created columns simply the Name of the element or '@' plus attribute Name respectively. The Name is later used as column header, the xpath is used to query the content relative to the line node.
By the way, AddColumn() can be called directly providing any Name and xpath.
BuildProps() has a final task to do: GetProperties() has to return a PropertyDescriptorCollection but we only have an array containing objects derived from PropertyDescriptor. So this array simply gets copied to a PropertyDescriptorCollection that gets stored for later use.
protected void BuildProps()
{
if (m_props!=null)
return;
Query();
if (m_alData.Count==0) // we act datadriven !!!
return;
XmlDataRowView row= (XmlDataRowView)m_alData[0];
XmlNode node= row.Node;
if (AutoAddSelf)
AddColumn(node.Name, "");
if (node is XmlElement)
{
if (AutoAddElements)
{
foreach(XmlNode elem in node.SelectNodes("*"))
AddColumn(elem.Name);
}
if (AutoAddAttribs)
{
foreach(XmlNode attr in node.SelectNodes("@*"))
AddColumn(attr.Name, "@"+attr.Name);
}
}
// translate to PropertyDescriptorCollection
PropertyDescriptor[] props= new XmlDataViewCol[m_alProps.Count];
for(int i= 0; i<m_alProps.Count; ++i)
props[i]= (PropertyDescriptor)m_alProps[i];
m_props= new PropertyDescriptorCollection(props);
}
Lastly XmlDataViewCol is derived from PropertyDescriptor. The column Name is already managed by the base class and can be passed on in the c'tor.
The most important method in this class is GetValue(). When the grid is building its content it iterates over the lines. For each line it iterates over the columns and calls GetValue() on the XmlDataViewCol object providing the XmlDataRowView object of the line as parameter. GetValue() just gets the xml node from the XmlDataRowView and queries the content using SingleNode with its xpath on that node.
public override object GetValue(object component)
{
XmlDataRowView row = (XmlDataRowView)component;
XmlNode node = row.Node;
XmlNode result = (m_sQuery.Length==0) ? node : node.SelectSingleNode(m_sQuery);
return (result==null) ? "{null}" : result.InnerXml;
}
Goal accomplished! :-)
![]()
Custom databinding to an XML document.
Nice try but let's do a reality check. Many XML documents could be bound to a grid using the simple "xml to dataset" approach. In this case binding directly to the xml document should not be more complex or take more effort for the developer - otherwise any lazy developer (just like me) would always try to use the dataset approach. If this where the case I would have wasted a lot of time.
I took an ASP.NET page with editable datagrid, implemented using datasets and SQL statements (actually I copied it from the book "Programming Data-Driven Web Application with ASP.NET", SAMS). I stored the dataset as XML and migrated the page to use xml databinding.
As you can see, the binding itself works with exactly the same effort:
void Bind()
{
DataSet ds= new DataSet();
SqlConnection con= new SqlConnection(sCon);
SqlDataAdapter da= new SqlDataAdapter("select ProductID, ProductName,
QuantityPerUnit, UnitPrice from products order by productid DESC", con);
da.Fill(ds, "Products");
grid1.DataSource= ds.Tables["Products"];
grid1.DataBind();
}
void Bind()
{
XmlDocument xml= new XmlDocument();
xml.Load(Server.MapPath("DATA.XML"));
XmlDataView dv= new XmlDataView(xml, "//Products");
// doc, query
dv.AddColumn("ProductID");
dv.AddColumn("ProductName");
dv.AddColumn("QuantityPerUnit");
dv.AddColumn("UnitPrice");
grid1.DataSource= dv;
grid1.DataBind();
}
The major work to do was to change the methods that did the data manipulation using SQL statements to manipulate the xml document instead. The code taken from the book didn't use the dataset features to do the manipulation, instead it built its own insert, update and delete statements. On the other hand this is likely to be the more efficient and therefore the more common case.
It would be nice if one could use any xpath with BoundColumn or DataBinder.Eval() instead of having to provide the columns in advance and therefore redundantly.
Unfortunately the grid doesn't take this xpath to ask the XmlDataView for the result. It asks the view for the property list - in which the xpath can't be in - and looks for the property in that list. The consequence is that this abbreviation can't work.
By the way: using datasets one wouldn't even encounter this problem. Datasets carry all meta information around and it never crossed my mind to enhance that mechanism. This is a little example for how new features cause new expectations.
A solution for his problem could be to derive from datagrid and provide additional support. E.g. the grid could react on ondatabinding, ondatabindcolumn or other events and tell the XmlDataView about the columns it needs. However I didn't spend any time to check this.
Within the .NET Framework documentation a bunch of interfaces and methods are mentioned I didn't even touch for this work.
All these things smell like Databinding with Windows Forms. Every work I did was limited to web forms.
A clean implementation should of course support IDisposable.
Databinding is a powerfull thing. At first glance it might look as if this is done purely and primarily for the dataset, but there are enough hooks to change the standard behaviour. The biggest problem is to find those hooks - the .NET Framework documentation is a good reference, however correlations and interdependencies are usually not properly explained. But at last it took not much work to provide a proper xml based substitute for formerly dataset based data binding. The most work to do was related to things that come with little support from the dataset as well: changing the SQL statements to xml manipulations.
Kontakt: info alexander-jung.net |
top | Letzte Änderung: 27.08.2005 23:31:17 |