Introduction
Looking pretty sharp kiddo, but your DataSet
isn�t sharp enough? Don�t you worry netnicks1
. Here is another easy way of sharpening your untyped DataSet
into a strongly typed one and making Intellisense out of it.
I�m not going to dwell on the pros and cons of typed vs. untyped DataSet
s here2. However if your boss or design yells for a strongly typed DataSet
and you don�t happen to have a database to conveniently generate it from, perhaps this will ease the pain.
One of the reasons for the hype behind DataSet
s is their support for XML. Developers can quickly transpose XML from and into a DataSet
by using the ReadXml()
and WriteXml()
methods of the DataSet
. However, converting the same XML into a strongly typed DataSet
requires some effort. One obvious question is, why can�t we populate the typed DataSet
with values from an XML by using the ReadXml
method of the auto-generated class foobar
, as in: foo.ReadXml(Server.MapPath("roster.xml"))
, where foo
is subclass of foobar
. Unfortunately this ReadXml
method often causes an Exception
: System.IndexOutOfRangeException
: There is no row at position 0. Very informative. Not sure if it isn�t implemented or some of the internal indices aren't populated, but the ReadXml
fails, and waiting for workarounds from others is always painful [like waiting for the economy or democracy to flourish on an empty stomach]. So for now, we create our own workaround. Besides, there is always the added benefit of learning, right?
Let's consider a common situation in today�s development environment: a web service or another process provides an XML document with dozens or hundreds of elements and a developer facing a task of creating a strongly typed DataSet
from that XML. We don�t always have the luxury of using an implicit database connection object automatically to write something like adapter.Fill(theRoster, "ClassRoster")
and produce the desired strongly typed DataSet
easily. Sorry, no drag and drop convenience of using a table in the Visual Studio XSD designer to help us. So we proceed to implement our own Fill
method, by first creating a schema from an XML document.
XmlDocument doc = new XmlDocument();
doc.Load(@"C:\temp\roster.xml");
DataSet ds = new DataSet();byte [] buf =
System.Text.ASCIIEncoding.ASCII.GetBytes(doc.OuterXml);
System.IO.MemoryStream ms = new System.IO.MemoryStream(buf);
ds.ReadXml(ms,XmlReadMode.InferSchema);ms.Close();
ds.WriteXmlSchema(@"C:\temp\roster.xsd");
Here is a mini version of a sample input XML document:
<roster>
<type>demo</type>
<class>12345</class>
<instructor>Waldo Erickson</instructor>
<title>Introduction to .NET</title>
<student>
<grade>P</grade>
<name>Jane Doe</name>
<phone>555-1212</phone>
</student>
<student>
<grade>F</grade>
<name>John Hush</name>
<phone>779-1490</phone>
</student>
</roster>
and what the resulting roster.xsd schema looks like:
="1.0"
<xs:schema id="NewDataSet" targetNamespace="http://tempuri.org/roster.xsd"
xmlns:mstns="http://tempuri.org/roster.xsd"
xmlns="http://tempuri.org/roster.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"
attributeFormDefault="qualified"
elementFormDefault="qualified">
<xs:element name="roster">
<xs:complexType>
<xs:sequence>
<xs:element name="type" type="xs:string" minOccurs="0" />
<xs:element name="class" type="xs:string" minOccurs="0" />
<xs:element name="instructor" type="xs:string" minOccurs="0" />
<xs:element name="title" type="xs:string" minOccurs="0" />
<xs:element name="student" minOccurs="0" maxOccurs="unbounded">
<xs:complexType>
<xs:sequence>
<xs:element name="grade" type="xs:string" minOccurs="0" />
<xs:element name="name" type="xs:string" minOccurs="0" />
<xs:element name="phone" type="xs:string" minOccurs="0" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="NewDataSet"
msdata:IsDataSet="true" msdata:EnforceConstraints="False">
<xs:complexType>
<xs:choice maxOccurs="unbounded">
<xs:element ref="roster" />
</xs:choice>
</xs:complexType>
</xs:element>
</xs:schema>
Done? Not yet. We created a schema inherited from the XML document, but not a strongly typed DataSet
. The schema provides only a �signature� of a typed DataSet
- its �body� is another class that represents the schema.
Next, let�s use the roster.xsd file within a Visual Studio project and generate a strongly typed DataSet
from it. Start with viewing the roster.xsd file with the Schema tab selected, then select Generate DataSet from the Schema menu. The typed DataSet
represented by the newly generated class (roster.cs- not shown here) is empty at this point.
Having generated the XML schema and the typed DataSet
from it manually, the next task is to populate it. You mean each row and item one at a time? With data from the same XML document that created it in the first place � what sort of automation is this? Besides, it�s very likely, that data returned from the web service may expand or change and other web services may provide even more complex XML documents in the future. Data always change � one thing you can count on. Can you see the face of a maintenance beast at the end of the tunnel?
Let�s make life easy. What we really want after all this work is a Fill
method for the strongly typed DataSet
that will work with an input XML document, so we can simply write:
TypedDataSet tds = new TypedDataSet();tds.Fill(xmlDoc);
OR
tds.ReadXml(@�c:\temp\roster.xml�);
One solution is to modify the roster.cs class directly and add the Fill
method to it. That will pose a problem when we have to regenerate the typed DataSet
. We have to add the Fill
method again and the practice of modifying a generated class is generally discouraged. Another solution is to inherit from the typed DataSet
, and add the Fill
method to the new subclass. Thus, when we regenerate the typed DataSet
, we won�t loose our newly added Fill
method. The class below implements the second solution:
using System;
using System.Data;
using System.Xml;
namespace Rosters{
public class TypedDataSet: roster
{
private TypedDataSet tds;
public void Fill(XmlDocument doc)
{
DataSet ds = new DataSet();
byte [] buf = System.Text.ASCIIEncoding.ASCII.GetBytes(doc.OuterXml);
System.IO.MemoryStream ms = new System.IO.MemoryStream(buf);
ds.ReadXml(ms);
ms.Close();
Fill(ds);
}
public new void ReadXml(string xmlFile)
{
XmlDocument xmlDoc = new XmlDocument();
try
{
xmlDoc.Load(xmlFile);
this.Fill(xmlDoc);
}
catch (XmlException)
{
}
public void Fill(DataSet ds)
{
tds = this;
for ( int i = 0; i < ds.Tables.Count; i++ )
{
foreach (DataRow iDr in ds.Tables[i].Rows)
{
DataRow oDr = tds.Tables[i].NewRow();
for ( int j = 0; j < ds.Tables[i].Columns.Count; j++ )
try
{
oDr[j] = iDr[j].ToString();
}
catch (NoNullAllowedException)
{
}
tds.Tables[i].Rows.Add(oDr);
}
}
tds.AcceptChanges();
}
}
}
Points of Interest
This example shows one workaround for the occasional ReadXml
method failure of a typed DataSet
. The generated roster
class in the sample code, inherits from the DataSet
class, but gives developers a somewhat more straightforward syntax to get at fields and tables, than it would be with an untyped DataSet
2. The public void Fill
method above uses a ToString()
conversion, because of the inherent nature of XML. Other types could easily be converted, if one chooses to manually assign types other than string
, to the elements in the typed DataSet
. The outlined conversion approach can be refined and expanded to cover other development tasks. For instance, HttpRequest
name-value pairs can be represented in an XML document and subsequently by a typed DataSet
, for business object use in another layer.
- [netnicks: Members of the .NET generation; true .NET conformists in behavior and attitude].
- [For more on the philosophy of typed vs. untyped, see Grady Booch's "Object Oriented Analysis and Design with Applications� and other fine publications.]