The XmlValidatingReader sits on top of the XmlTextReader and validates each node according to a schema. Now you can validate against a DTD as well, or even make a validating reader that validates against nothing.
I am going to show you one way to validate XML data as it is being read in. I will validate against an external XML schema file. If at the end of this section you find validation of XML interesting, I suggest you spend some quality time with the online help. Although it is important, what I am about to show you is just the tip of the XML and Extensible Schema Definition (XSD) iceberg.
So, what is XML validation? Here is what you can do with validation:
Validate the data type.
Validate that the right tags are in the correct owning blocks.
Validate the minimum number of times an element appears.
Validate the maximum amount of times an element appears.
What you cannot do is validate your data against regular expressions. You will still need to do that manually. XML validation automates a big part of reading an XML file, however.
The best way to explain how an XmlValidatingReader works is to show you by example. In this section I expand on the previous example by adding schema support for the method that reads the XML file.
Start with a brand-new C# or VB project. Mine is called "ValidateXML." The form is almost the same as that in the previous example, with the addition of one more major control. I copied the form layout of the previous project to the Clipboard and pasted it on the new form for this project. Here are the manual instructions if you did not complete the last example:
Add a Label that reads Configuration Date.
Add a Label called lblDate below the Configuration Date Label. Make the BorderStyle FixedSingle.
Add a Label whose text reads IP Address.
Add a TextBox called txtIP below the IP Address Label.
Add a Label whose text reads Mode.
Add a ComboBox called cmbMode below the Mode Label.
Add a Label whose text reads Password.
Add a TextBox called txtPass below the Password Label.
Add a Label whose text reads Time zone offset.
Add a TextBox called txtOffset below the Time zone offset Label.
Add a Label whose text reads Relay Delay.
Add a TextBox called txtRelay below the Relay Delay Label.
Add a Button called cmbWrite. Change the text to Write XML.
Add a Label whose text reads Read Results. Center the text in the control.
Add a RichTextBox to the form and call it rcResults.
Your new form should look like the one shown in Figure 9-4.
I use the RichTextBox to display the (good and bad) results of reading the file. Double-click the two buttons to get the delegates created for you.
Now, a validating reader needs a schema to work with. You could make one via code, but the easier way is to use the XML designer provided by .NET. Although your XML data file may change, the schema should remain static once you have your validation rules in place.
Make sure that you have the following code in your constructor.
C#
lblDate.Text = DateTime.Now.ToLongDateString(); cmbMode.Items.Add(MODE.Dumb); cmbMode.Items.Add(MODE.OffLine); cmbMode.Items.Add(MODE.OnLine); cmbMode.Items.Add(MODE.None); cmbMode.SelectedIndex = 0; txtIP.Text = "123.456.789.13"; txtPass.Text = "Abc56def"; txtOffset.Text = "-34"; txtRelay.Text = "21.8";
lblDate.Text = DateTime.Now.ToLongDateString() cmbMode.Items.Add(MODE.Dumb) cmbMode.Items.Add(MODE.OffLine) cmbMode.Items.Add(MODE.OnLine) cmbMode.Items.Add(MODE.None) cmbMode.SelectedIndex = 0 txtIP.Text = "123.456.789.13" txtPass.Text = "Abc56def" txtOffset.Text = "-34" txtRelay.Text = "21.8"
Go to the Solution Explorer window and add a new XML Schema to your project. You will see a blank page with two tabs at the bottom left of the page. One tab is for viewing the controls for the page and the other is for viewing the actual XML code generated by said controls. Name this new XML Schema ConfigDevice.xsd. Figure 9-5 shows my Solution Explorer window after I added the new item.
Now for the fun part. Drop a complex type control onto your schema form. Name it ConfigDevice. Click the first box under the letters CT and you will see a drop-down control. Click this control and choose an element. Call it ConfigDate and choose a type of DateTime in the cell next to it.
Table 9-1 shows the elements that you need to add to this control.
Element |
Type |
---|---|
ConfigDate |
DateTime |
IP |
string |
Mode |
string |
PassWord |
string |
TimeZoneOffset |
integer |
RelayDelay |
double |
Once you have finished adding the elements, drag over an element from the Toolbox and drop it on your XML form. The element name should be "Device_Configuration". The type of this element should be ConfigDevice. You choose the type from the drop-down box in the cell to the right of the name. Once you do this, the rest of this element box should fill itself in for you automatically.
Figure 9-6 shows these two controls as they are filled in on the XML form.
You made a user-defined type called "ConfigDevice." The members of this new type are the elements and associated types that you typed in. Once this was complete, you dropped a new element on the form. You gave it a name and assigned its type to the new user-defined type.
You can think of this as defining a structure with certain public variables. You then define a new variable whose type is this new structure. You have done it hundreds of times in code; now you are doing it using XML blocks.
There is one more thing you need to do to this schema. You will want to make sure that the XML file you are checking actually has the values you are looking for. You will also want to see those values only once in the XML file.
Click the element ConfigDate in the complex type control. Go over to the Properties screen and enter 1 as the minOccurs value. Enter 1 also for the maxOccurs value. Do this for all the elements in the complex type.
Click the XML tab at the bottom of your XML form. Your code should look like Figure 9-7.
I like working with schema files because the syntax is the same as an XML file. A DTD uses a different syntax.
If you want to, you can embed this XML Schema at the top of an XML data file. This will give you a single XML file for transport that includes data and validation requirements. This is called an inline schema.
You need to go to Windows Explorer and copy this XSD file to the BIN directory if you are working in VB or to the BIN\Debug directory if you are working in C#.
You need to add the following namespace references to your code.
C#
using System.Xml; using System.IO; using System.Xml.Schema;
VB
Imports System.Xml Imports System.IO Imports System.Xml.Schema
I include the IO namespace because I will use a stream to read the file. Add the following variables block.
C#
#region vars string XMLfname = "ConfigDevice.xml"; string XSDfname = "ConfigDevice.xsd"; string NameSpace = "http://tempuri.org/ConfigDevice.xsd"; enum MODE { None, OnLine, OffLine, Dumb } #endregion
#Region "vars" Private XMLfname As String = "ConfigDevice.xml" Private XSDfname As String = "ConfigDevice.xsd" Private nmSpace As String = "http://tempuri.org/ConfigDevice.xsd" Private Enum MODE None OnLine OffLine Dumb End Enum #End Region
I include a namespace in both the XSD and XML files so they can reference the correct schema file.
The next thing to add is the code to write the XML file.
C#
private void WriteXMLFile() { double RelayDelay; DateTime date; string IP; MODE mode; Decimal TZ_Offset; string Pword; //I am going to be really bad here and assume that all user-filled-in //fields are going to convert properly date = Convert.ToDateTime(lblDate.Text); IP = txtIP.Text; mode = (MODE)cmbMode.SelectedItem; TZ_Offset = int.Parse(txtOffset.Text); Pword = txtPass.Text; RelayDelay = Convert.ToDouble(txtRelay.Text); //This is your basic well-formed XML file. XmlTextWriter w = new XmlTextWriter(XMLfname, null); w.Formatting = Formatting.Indented; w.WriteStartDocument(); w.WriteStartElement("Device_Configuration", NameSpace); //Uncomment the short date line and comment the one above to cause //an XML validation error in the read routine w.WriteStartElement("ConfigDate"); w.WriteString(XmlConvert.ToString(date)); // w.WriteString(date.ToShortDateString()); w.WriteEndElement(); w.WriteStartElement("IP"); w.WriteString(txtIP.Text); w.WriteEndElement(); w.WriteStartElement("Mode"); w.WriteString(cmbMode.SelectedItem.ToString()); w.WriteEndElement(); w.WriteStartElement("PassWord"); w.WriteString(Pword); w.WriteEndElement(); w.WriteStartElement("TimeZoneOffset"); w.WriteString(XmlConvert.ToString(TZ_Offset)); w.WriteEndElement(); w.WriteStartElement("RelayDelay"); w.WriteString(XmlConvert.ToString(RelayDelay)); w.WriteEndElement(); w.WriteEndElement(); w.WriteEndDocument(); w.Flush(); w.Close(); //enable read code cmbMode.SelectedIndex = 3; cmdRead.Enabled = true; cmdWrite.Enabled = false; }
Private Sub WriteXMLFile() Dim RelayDelay As Double Dim dte As DateTime Dim IP As String Dim mode As MODE Dim TZ_Offset As Decimal Dim Pword As String 'I am going to be really bad here and assume that all user-filled-in 'fields are going to convert properly dte = Convert.ToDateTime(lblDate.Text) IP = txtIP.Text mode = CType(cmbMode.SelectedItem, MODE) TZ_Offset = Int32.Parse(txtOffset.Text) Pword = txtPass.Text RelayDelay = Convert.ToDouble(txtRelay.Text) 'This is your basic well-formed XML file. Dim w As XmlTextWriter = New XmlTextWriter(XMLfname, Nothing) w.Formatting = Formatting.Indented w.WriteStartDocument() w.WriteStartElement("Device_Configuration", nmSpace) 'Uncomment the short date line and comment the one above to cause 'an XML validation error in the read routine w.WriteStartElement("ConfigDate") w.WriteString(XmlConvert.ToString(dte)) 'w.WriteString(dte.ToShortDateString()) w.WriteEndElement() w.WriteStartElement("IP") w.WriteString(txtIP.Text) w.WriteEndElement() w.WriteStartElement("Mode") w.WriteString(cmbMode.SelectedItem.ToString()) w.WriteEndElement() w.WriteStartElement("PassWord") w.WriteString(Pword) w.WriteEndElement() w.WriteStartElement("TimeZoneOffset") w.WriteString(XmlConvert.ToString(TZ_Offset)) w.WriteEndElement() w.WriteStartElement("RelayDelay") w.WriteString(XmlConvert.ToString(RelayDelay)) w.WriteEndElement() w.WriteEndElement() w.WriteEndDocument() w.Flush() w.Close() 'enable read code cmbMode.SelectedIndex = 3 cmdRead.Enabled = True cmdWrite.Enabled = False End Sub
I am writing to the XML file a little differently from the previous example. In that example I used the WriteElementString method. Here I explicitly write the start element tag, write its value, and then write the end element tag.
The only real difference here from the last example is that I am not encrypting the password. You have that class and you can use it if you like.
Next is the XML reader. I first show you the code then explain what is going on. This code region includes two methods.
C#
#region Read XML file private void ReadXMLFile () { double RelayDelay; DateTime date; string IP; MODE mode; decimal TZ_Offset; string Pword; try { //open up a stream and feed it to the XmlTextReader StreamReader sRdr = new StreamReader(XMLfname); XmlTextReader tRdr = new XmlTextReader(sRdr); //Instantiate a new schemas collection //Add this one schema to the collection //A collection means that you can validate this XML file against any //number of schemas. You would do this if you were reading the file //piecemeal XmlSchemaCollection Schemas = new XmlSchemaCollection(); Schemas.Add(null, XSDfname); //Instantiate a new validating reader. This validates for data type //and presence. //Add the schemas collection to the validating reader and funnel the //XmlTextReader through it. //wire up an ad-hoc validation delegate to catch any validation errors XmlValidatingReader vRdr = new XmlValidatingReader(tRdr); vRdr.ValidationType = ValidationType.Schema; vRdr.ValidationEventHandler += new ValidationEventHandler(ValXML); vRdr.Schemas.Add(Schemas); //Read the XML file through the validator object node; while (vRdr.Read()) { node = null; if (vRdr.LocalName.Equals("ConfigDate")) { node = vRdr.ReadTypedValue(); if(node != null) date = (DateTime)node; } if (vRdr.LocalName.Equals("RelayDelay")) { node = vRdr.ReadTypedValue(); if(node != null) RelayDelay = (double)node; } if (vRdr.LocalName.Equals("TimeZoneOffset")) { node = vRdr.ReadTypedValue(); if(node != null) TZ_Offset = (decimal)node; } if (vRdr.LocalName.Equals("PassWord")) { node = vRdr.ReadTypedValue(); if(node != null) Pword = (string)node; } if (vRdr.LocalName.Equals("Mode")) { node = vRdr.ReadTypedValue(); // mode = (string)node; } if (vRdr.LocalName.Equals("IP")) { node = vRdr.ReadTypedValue(); if(node != null) IP = (string)node; } if(node != null) { rcResults.AppendText(vRdr.LocalName + "\n"); rcResults.AppendText(node.GetType().ToString() + "\n"); rcResults.AppendText(node.ToString() + "\n\n"); } } vRdr.Close(); tRdr.Close(); sRdr.Close(); } catch (Exception e) { //The handler will catch malformed XML docs. //It is not intended to catch bad data. That is the delegate's job MessageBox.Show("Exception analyzing Config file: " + e.Message); } } private void ValXML(Object sender, ValidationEventArgs e) { //This delegate will ONLY catch bad data. It will not catch //a malformed XML document!! rcResults.AppendText(e.Message + "\n\n"); } #endregion
VB
#Region "Read XML file" Private Sub ReadXMLFile() Dim RelayDelay As Double Dim dte As DateTime Dim IP As String Dim mode As MODE Dim TZ_Offset As Decimal Dim Pword As String Try 'open up a stream and feed it to the XmlTextReader Dim sRdr As StreamReader = New StreamReader(XMLfname) Dim tRdr As XmlTextReader = New XmlTextReader(sRdr) 'Instantiate a new schemas collection 'Add this one schema to the collection 'A collection means that you can validate this XML file against any 'number of schemas. You would do this if you were reading the file 'piecemeal Dim Schemas As XmlSchemaCollection = New XmlSchemaCollection() Schemas.Add(Nothing, XSDfname) 'Instantiate a new validating reader. This validates for data type 'and presence. 'Add the schemas collection to the validating reader and funnel the 'XmlTextReader through it. 'wire up an ad-hoc validation delegate to catch any validation errors Dim vRdr As XmlValidatingReader = New XmlValidatingReader(tRdr) vRdr.ValidationType = ValidationType.Schema AddHandler vRdr.ValidationEventHandler, _ New ValidationEventHandler(AddressOf ValXML) vRdr.Schemas.Add(Schemas) 'Read the XML file through the validator Dim node As Object While vRdr.Read() node = Nothing If vRdr.LocalName.Equals("ConfigDate") Then node = vRdr.ReadTypedValue() If Not node Is Nothing Then dte = CType(node, DateTime) End If End If If vRdr.LocalName.Equals("RelayDelay") Then node = vRdr.ReadTypedValue() If Not node Is Nothing Then RelayDelay = CType(node, Double) End If End If If vRdr.LocalName.Equals("TimeZoneOffset") Then node = vRdr.ReadTypedValue() If Not node Is Nothing Then TZ_Offset = CType(node, Decimal) End If End If If vRdr.LocalName.Equals("PassWord") Then node = vRdr.ReadTypedValue() If Not node Is Nothing Then Pword = CType(node, String) End If End If If (vRdr.LocalName.Equals("Mode")) Then node = vRdr.ReadTypedValue() ' mode = (string)node; End If If vRdr.LocalName.Equals("IP") Then node = vRdr.ReadTypedValue() If Not node Is Nothing Then IP = CType(node, String) End If End If If Not node Is Nothing Then rcResults.AppendText(vRdr.LocalName + vbCrLf) rcResults.AppendText(node.GetType().ToString() + vbCrLf) rcResults.AppendText(node.ToString() + vbCrLf + vbCrLf) End If End While vRdr.Close() tRdr.Close() sRdr.Close() Catch e As Exception 'The handler will catch malformed XML docs. 'It is not intended to catch bad data. That is the delegate's job MessageBox.Show("Exception analyzing Config file: " + e.Message) End Try End Sub Private Sub ValXML(ByVal sender As Object, ByVal e As ValidationEventArgs) 'This delegate will ONLY catch bad data. It will not catch 'a malformed XML document!! rcResults.AppendText(e.Message + vbCrLf + vbCrLf) End Sub #End Region
This code creates a new XmlTextReader that is fed by the StreamReader. I then create a schema collection and add the schema I just created to it.
The next thing I do is create an XmlValidatingReader, tell it how to validate the XML file, and wire up a validation error handler. I pass the schemas collection to this validating reader.
Because I am passing a collection of schemas to the reader, it is safe to infer that this reader can validate the XML file against any number of schemas. In fact, that is true. If you have a huge XML data file, you may want to validate chunks of it against different schemas.
As I read the XML file, I look for a particular tag and then cast the string directly to the variable I need. Here is the RelayDelay repeated.
C#
if (vRdr.LocalName.Equals("RelayDelay")) { node = vRdr.ReadTypedValue(); if(node != null) RelayDelay = (double)node; }
In the previous example I needed to check this value with the regular expression parser to make sure it was the correct data type. Here I can safely assume it is because the ValidationEventHandler delegate would have caught any data type errors.
Once I know the nodes are valid, I write some statistics to the TextBox screen. Here is the code for that.
VB
If Not node Is Nothing Then rcResults.AppendText(vRdr.LocalName + vbCrLf) rcResults.AppendText(node.GetType().ToString() + vbCrLf) rcResults.AppendText(node.ToString() + vbCrLf + vbCrLf) End If
The LocalName is the element name. Besides this, I show the data type that the schema expects and the actual value of the node. If there are any problems with data type or missing data, I handle the problem in the delegate.
Enter the following code, which wires up the buttons.
C#
#region events private void cmdWrite_Click(object sender, System.EventArgs e) { WriteXMLFile(); } private void cmdRead_Click(object sender, System.EventArgs e) { ReadXMLFile(); } #endregion
VB
#Region "events" Private Sub cmdWrite_Click_1(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles cmdWrite.Click WriteXMLFile() End Sub Private Sub cmdRead_Click_1(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles cmdRead.Click ReadXMLFile() End Sub #End Region
Compile and run the program. Click the Write XML button then the Read XML button. Figure 9-8 shows the form after reading the XML file.
All's well here. Now make a slight change to the code to write to the XML file. This is where you write the ConfigDate string to the file.
C#
w.WriteStartElement("ConfigDate"); // w.WriteString(XmlConvert.ToString(date)); w.WriteString(date.ToShortDateString()); w.WriteEndElement();
The VB code is the same minus the semicolon.
What I am doing here is saving a raw date string to the XML file. Run the program and you should see the screen shown in Figure 9-9.
You can see that the validation handler caught the problem and the error displays on the screen.
Note |
The XmlValidatingReader catches only validation errors. If you have a malformed XML file, the XmlValidatingReader will not catch that. It will instead throw an exception. You may want to read in the XML file first and check for well-formedness before letting the XmlValidatingReader take a look at it. |
I suggest playing around a little with the code to see what errors you can catch.