Team LiB
Previous Section Next Section

The XmlValidatingReader

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:

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:

  1. Add a Label that reads Configuration Date.

  2. Add a Label called lblDate below the Configuration Date Label. Make the BorderStyle FixedSingle.

  3. Add a Label whose text reads IP Address.

  4. Add a TextBox called txtIP below the IP Address Label.

  5. Add a Label whose text reads Mode.

  6. Add a ComboBox called cmbMode below the Mode Label.

  7. Add a Label whose text reads Password.

  8. Add a TextBox called txtPass below the Password Label.

  9. Add a Label whose text reads Time zone offset.

  10. Add a TextBox called txtOffset below the Time zone offset Label.

  11. Add a Label whose text reads Relay Delay.

  12. Add a TextBox called txtRelay below the Relay Delay Label.

  13. Add a Button called cmbWrite. Change the text to Write XML.

  14. Add a Label whose text reads Read Results. Center the text in the control.

  15. Add a RichTextBox to the form and call it rcResults.

Your new form should look like the one shown in Figure 9-4.

Click To expand
Figure 9-4: The new form for the validating reader

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";

VB

    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"

Making the Schema

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.

Click To expand
Figure 9-5: The new XML Schema file

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.

Table 9-1: Elements for the ConfigDevice Complex Type 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.

Click To expand
Figure 9-6: XML controls

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.

Click To expand
Figure 9-7: XML output for the XSD file

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

The Validation Form's Code

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

VB

#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;
    }

VB

  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.

Click To expand
Figure 9-8: The validated XML form

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.

Click To expand
Figure 9-9: An error in XML validation

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.


Team LiB
Previous Section Next Section