Team LiB
Previous Section Next Section

The System.Xml Namespace

The System.Xml namespace contains almost 40 classes and a few interfaces to help with your XML needs. It is comprehensive, to say the least. In this section I concentrate on a few of those classes and interfaces.

The first thing I cover is writing to an XML file. One of the things XML files are good for is configuration data. They are fast replacing .ini files for this purpose. Of course, configuration data does not necessarily contain only string data. This may seem like a contradiction because an XML file is a text file. However, an XML file can have a great deal of data that is not string data but is represented by strings. Take the resource file, for instance. In .NET a resource file is often an XML file. I have built resource files with only .jpg files in them. If you were to look at the file, you would see an incredibly huge string for each .jpg file.

One of the nice things about the .NET XML classes is the XmlConvert class, which can convert strongly typed data into strings and back again. You will see this in the next example.

Wide-Open Data

Before I start on this simple example, I want to mention one thing. I hope you are wondering how secure XML files are. After all, they are text files that anyone can open and read using any text editor. In fact, quite a few XML files have descriptive tags that let you glean some pretty useful information.

There are ways you can secure XML files, and I use a method to secure some of the data in an XML file in the next example. I do not go too deep into encryption here, so I suggest that you read up on the cryptography capabilities of .NET. They are quite extensive.

Writing XML Data

Start a new Windows project in either C# or VB. Mine is called "WriteXML." Follow these steps to add a few controls on the default form:

  1. Add a Label that reads Configuration Date.

  2. Add a Label called lblDate below the Configuration Data 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 cmdWrite. Change its text to Write XML.

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

Click To expand
Figure 9-1: The XMLWriter form

This form represents a typical subset of configuration data for an embedded controller of some type.

Now as I inferred earlier, XML files are wide open to hacking by anyone with a text editor. With that in mind, I would be crazy to save a password to an XML file as is. The first thing I need to do code-wise is create a static encryption class that will encrypt and decrypt any string passed to it.

Add a new class to your project called "Classify." Listings 9-1a and 9-1b show the complete code for this class.

Listing 9-1a: C# Code for the Classify Static Class
Start example
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;

namespace WriteXML_c
{
  /// <summary>
  /// Summary description for Classify.
  /// </summary>
  public class Classify
  {

    #region class local variables

    private static string EncryptKey = "CryptoManiac";
    private static MD5CryptoServiceProvider hashmd5;
    private static TripleDESCryptoServiceProvider des;
    private static byte[] pwdhash;

    #endregion

    static Classify()
    {
      hashmd5 = new MD5CryptoServiceProvider();
      pwdhash = hashmd5.ComputeHash(ASCIIEncoding.ASCII.GetBytes(EncryptKey));

      //implement DES3 encryption
      des = new TripleDESCryptoServiceProvider();
      des.Key = pwdhash;
      des.Mode = CipherMode.ECB; //CBC, CFB
    }

    public static string Encrypt(string OriginalString)
    {
      byte[] buff = ASCIIEncoding.ASCII.GetBytes(OriginalString);
      return Convert.ToBase64String(des.CreateEncryptor().
                                        TransformFinalBlock(buff, 0,
                                        buff.Length));
    }

    public static string Decrypt(string EncryptedString)
    {
      byte[] buff = Convert.FromBase64String(EncryptedString);
      return ASCIIEncoding.ASCII.GetString(des.CreateDecryptor().
                                               TransformFinalBlock(buff, 0,
                                               buff.Length));
    }
  }
}
End example
Listing 9-1b: VB Code for the Classify Static Class
Start example
Option Strict On

Imports System.IO
Imports System.Text
Imports System.Security.Cryptography
Public Class Classify

#Region "class local variables"

  Private Shared EncryptKey As String = "CryptoManiac"
  Private Shared hashmd5 As MD5CryptoServiceProvider
  Private Shared des As TripleDESCryptoServiceProvider
  Private Shared pwdhash() As Byte

#End Region

  Shared Sub New()
    hashmd5 = New MD5CryptoServiceProvider()
    pwdhash = hashmd5.ComputeHash(ASCIIEncoding.ASCII.GetBytes(EncryptKey))

    'implement DES3 encryption
    des = New TripleDESCryptoServiceProvider()
    des.Key = pwdhash
    des.Mode = CipherMode.ECB
  End Sub

  Public Shared Function Encrypt(ByVal OriginalString As String) As String
    Dim buff() As Byte = ASCIIEncoding.ASCII.GetBytes(OriginalString)
    Return Convert.ToBase64String(des.CreateEncryptor(). _
                                      TransformFinalBlock(buff, 0, _
                                      buff.Length))
  End Function

  Public Shared Function Decrypt(ByVal EncryptedString As String) As String
    Dim buff() As Byte = Convert.FromBase64String(EncryptedString)
    Return ASCIIEncoding.ASCII.GetString(des.CreateDecryptor(). _
                                             TransformFinalBlock(buff, 0, _
                                             buff.Length))
  End Function
End Class
End example

Although this is not a book about encryption, I cannot really talk about data without at least mentioning the topic. This class is just about the simplest way to encrypt and decrypt a string in .NET.

As you can see, I use triple Data Encryption Standard (DES) encryption. I have a character-based key that I start out with, which in this case is the word "CryptoManiac." This gets encoded into a hash, which is then given as the key to the DES service provider object.

Using static (shared in VB) methods and a static constructor allow me to set up the DES encryption class just once and use it many times. You know by now my fondness for static classes, and this is a perfect use for one.

All I need to do to encrypt a string is call the Encrypt method. I can also decrypt the string using the same class. Of course, I need the correct key to decrypt the string.

Note 

If you were to release this code, a programmer could easily get the encrypting key by reading the intermediate language (IL) code. If you want to seriously use encryption, you should also use an obfuscator for the IL code. A few obfuscators are available. In addition to using an obfuscator, you should hide the encrypting key by building the string out of a series of bytes that are added via a series of method calls. This way, a programmer cannot use a hex editor and see the word "CryptoManiac" glaring at him or her on the screen.

Now for the main form's code. This is pretty short, so I present all of the code in Listings 9-2a and 9-2b. The Windows-generated code is deleted here for clarity.

Listing 9-2a: C# Code for the Main Form
Start example
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Text;
using System.Xml;
using System.Data;

namespace WriteXML_c
{
  public class Form1 : System.Windows.Forms.Form
  {
    #region vars
    string fname = "Output.xml";
    enum MODE
    {
      None,
      OnLine,
      OffLine,
      Dumb
    }

    #endregion

    private System.Windows.Forms.Button cmdWrite;
    private System.Windows.Forms.Label label1;
    private System.Windows.Forms.Label lblFirst;
    private System.Windows.Forms.Label lblDate;
    private System.Windows.Forms.Label label3;
    private System.Windows.Forms.Label label4;
    private System.Windows.Forms.ComboBox cmbMode;
    private System.Windows.Forms.TextBox txtIP;
    private System.Windows.Forms.TextBox txtOffset;
    private System.Windows.Forms.TextBox txtPass;
    private System.Windows.Forms.Label label5;
    private System.Windows.Forms.TextBox txtRelay;
    private System.Windows.Forms.Label label2;

    private System.ComponentModel.Container components = null;

    public Form1()
    {
      InitializeComponent();

      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";
      cmdWrite.Click += new EventHandler(this.WriteXMLFile);

    }

    protected override void Dispose( bool disposing )
    {
      if( disposing )
      {
        if (components != null)
        {
          components.Dispose();
        }
      }
      base.Dispose( disposing );
    }

    #region Windows Form Designer generated code
...
...
...
    #endregion

    [STAThread]
    static void Main()
    {
      Application.Run(new Form1());
    }

    private void Form1_Load(object sender, System.EventArgs e)
    {
    }

    private void WriteXMLFile(object sender, EventArgs e)
    {
      double    RelayDelay;
      DateTime  date;
      string    IP;
      MODE      mode;
      int       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       = Classify.Encrypt(txtPass.Text);
      RelayDelay  = Convert.ToDouble(txtRelay.Text);

      //This is your basic well-formed XML file.
      XmlTextWriter w = new XmlTextWriter(fname, Encoding.UTF8);
      w.Formatting = Formatting.Indented;

      w.WriteStartDocument();
      w.WriteStartElement("Device_Configuration");

      w.WriteElementString("ConfigDate", XmlConvert.ToString(date));
      w.WriteElementString("IP", txtIP.Text);
      w.WriteElementString("Mode", cmbMode.SelectedItem.ToString());
      w.WriteElementString("PassWord", Pword);
      w.WriteElementString("TimeZoneOffset", XmlConvert.ToString(TZ_Offset));
      w.WriteElementString("RelayDelay", XmlConvert.ToString(RelayDelay));

      w.WriteEndElement();
      w.WriteEndDocument();

      w.Flush();
      w.Close();
    }
  }
}
End example
Listing 9-2b: VB Code for the Main Form
Start example
Option Strict On
Imports System
Imports System.Drawing
Imports System.Collections
Imports System.ComponentModel
Imports System.Windows.Forms
Imports System.Text
Imports System.Xml
Imports System.Data
  Public Class Form1
    Inherits System.Windows.Forms.Form

  #Region "vars"

    Private fname As String = "Output.xml"
    Enum MODE
      None
      OnLine
      OffLine
      Dumb
    End Enum

  #End Region

  #Region " Windows Form Designer generated code "

    Public Sub New()
      MyBase.New()

      InitializeComponent()

      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"

      AddHandler cmdWrite.Click, New EventHandler(AddressOf Me.WriteXMLFile)

    End Sub

...
...
...

#End Region
  Private Sub Form1_Load(ByVal sender As System.Object, _
                         ByVal e As System.EventArgs) Handles MyBase.Load

  End Sub

  Private Sub WriteXMLFile(ByVal sender As Object, ByVal e As EventArgs)
    Dim RelayDelay As Double
    Dim dte As DateTime
    Dim IP As String
    Dim mode As MODE
    Dim TZ_Offset As Int32
    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 = Classify.Encrypt(txtPass.Text)
    RelayDelay = Convert.ToDouble(txtRelay.Text)

    'This is your basic well-formed XML file.
    Dim w As XmlTextWriter = New XmlTextWriter(fname, Encoding.UTF8)
    w.Formatting = Formatting.Indented

    w.WriteStartDocument()
    w.WriteStartElement("Device_Configuration")

    w.WriteElementString("ConfigDate", XmlConvert.ToString(dte))
    w.WriteElementString("IP", txtIP.Text)
    w.WriteElementString("Mode", cmbMode.SelectedItem.ToString())
    w.WriteElementString("PassWord", Pword)
    w.WriteElementString("TimeZoneOffset", XmlConvert.ToString(TZ_Offset))
    w.WriteElementString("RelayDelay", XmlConvert.ToString(RelayDelay))

    w.WriteEndElement()
    w.WriteEndDocument()

    w.Flush()
    w.Close()
  End Sub
End Class
End example

Compile and run the program. Your form should look like the one shown in Figure 9-2.

Click To expand
Figure 9-2: The running form

Let's look at the code. First of all, I fill in the text boxes with some values for you. You are free to change these values if you like. I have tried to include a smattering of all the standard data types in .NET. The IP Address is a string. The Mode is an enumeration. The Password is a string, and the Time zone offset is an integer. The Relay Delay is a double.

I did not include any validation on these fields. Changing the values to something other than what is expected will crash the program.

The following piece of C# code is responsible for writing the file:

      XmlTextWriter w = new XmlTextWriter(fname, Encoding.UTF8);
      w.Formatting = Formatting.Indented;

      w.WriteStartDocument();
      w.WriteStartElement("Device_Configuration");

      w.WriteElementString("ConfigDate", XmlConvert.ToString(date));
      w.WriteElementString("IP", txtIP.Text);
      w.WriteElementString("Mode", cmbMode.SelectedItem.ToString());
      w.WriteElementString("PassWord", Pword);
      w.WriteElementString("TimeZoneOffset", XmlConvert.ToString(TZ_Offset));
      w.WriteElementString("RelayDelay", XmlConvert.ToString(RelayDelay));

      w.WriteEndElement();
      w.WriteEndDocument();

Notice that I use the XmlConvert routines to convert the strongly typed data to a string that is suitable for saving in an XML file. Now the string that you get out of this conversion is not always the same as what you would get out of using the plain Convert functions. This is important to remember if you are wondering why I use this specialized class. Most notably, the date is converted to a format that is not what you would expect from a date-to-string conversion.

Notice also that I save the encrypted version of the password to the XML file. It would not do to have the raw password here.

So what happens when you click the Write XML button? You get the following result:

<?xml version="1.0" encoding="utf-8"?>
<Device_Configuration>
  <ConfigDate>2003-02-01T00:00:00.0000000-05:00</ConfigDate>
  <IP>123.456.789.13</IP>
  <Mode>Dumb</Mode>
  <PassWord>aXIHM6wwE6HXiEmkPOz42g==</PassWord>
  <TimeZoneOffset>-34</TimeZoneOffset>
  <RelayDelay>21.8</RelayDelay>
</Device_Configuration>

Each piece of data is tagged correctly and the password is encrypted. The date is also in a format that XML parsers can understand.

By the way, you will find this file in the Debug directory of your C# project and in the BIN directory of your VB project.

Using the XmlTextWrite like I have here to write an XML file is much like writing a bunch of If-EndIf statements in VB. Let me show you the pertinent code again, only this time with some indentation so you can see what I mean:

      XmlTextWriter w = new XmlTextWriter(fname, Encoding.UTF8);
        w.Formatting = Formatting.Indented;
        w.WriteStartDocument();
          w.WriteStartElement("Device_Configuration");

            w.WriteElementString("ConfigDate", XmlConvert.ToString(date));
            w.WriteElementString("IP", txtIP.Text);
            w.WriteElementString("Mode", cmbMode.SelectedItem.ToString());
            w.WriteElementString("PassWord", Pword);
           w.WriteElementString("TimeZoneOffset", XmlConvert.ToString(TZ_Offset));
            w.WriteElementString("RelayDelay", XmlConvert.ToString(RelayDelay));
          w.WriteEndElement();
        w.WriteEndDocument();
      w.Flush();
      w.Close();

I bracket the whole file with a start document command and an end document command. This document command has a nested element command, which in turn has some nested strings. If you keep this kind of format, you will be fairly safe.

So, what is a quick way to tell if your resulting document is well-formed? Try opening it with an Internet browser. If the document is missing necessary elements, then the browser will choke.

Those of you who write in HTML or XML quite often are used to writing documents that are well-formed. You can, however, write an XML document that contains all the information you are looking for and it is not well-formed at all. If you do this, you can still read it using an XmlTextReader. So what's the point of the well-formed document? The point is that you also want other programs to read your XML file. If you can make it standard, then no one has to call you up to get the format. This is a big plus.

Reading and Validating What You Wrote

OK, now you know how to write a simple XML configuration file. How do you read it back? By using the XmlTextReader, of course.

The XmlTextReader class has about eight hundred million members that let you do all kinds of stuff. I use only a few here to show you a simple reader and validation procedure. The XmlTextReader is a forward-only, nonvalidating reader. It is intended to be used for high-speed access to XML documents whose contents are known by you to be well-formed. This class is also handy because it does not require a schema or a document type definition (DTD).

On to the next part of this example. Add a new Button to your form called cmdRead. Its text should be Read XML. Figure 9-3 shows this addition.

Click To expand
Figure 9-3: The new Read XML button

You need to go back into this form and add some new code. Because you will use some regular expressions for validation, you need to include the System.Text.RegularExpressions namespace at the top of your form.

Add the following enumeration to your form's local variables region.

C#

    //Added for read
    enum CONFIG_STATE
    {
      C_UNKNOWN,
      C_DATE,
      C_IP,
      C_MODE,
      C_PASS,
      C_OFFSET,
      C_RELAY
    }

VB

  'Added for read
  Enum CONFIG_STATE
    C_UNKNOWN
    C_DATE
    C_IP
    C_MODE
    C_PASS
    C_OFFSET
      C_RELAY
  End Enum

You use this enumeration to determine what state the code is in while reading the XML file.

You need to add some code to your constructor. Here it is with the new code in bold.

C#

    public Form1()
    {
      InitializeComponent();

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

      cmdWrite.Click += new EventHandler(this.WriteXMLFile);

      //=========== New read code =============
      cmdRead.Enabled = false;
      cmdRead.Click += new EventHandler(this.ReadXMLFile);
    }

VB

  Public Sub New()
    MyBase.New()

    InitializeComponent()

    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"

    AddHandler cmdWrite.Click, New EventHandler(AddressOf Me.WriteXMLFile)

    '=========== New read code =============
    cmdRead.Enabled = False
    AddHandler cmdRead.Click, New EventHandler(AddressOf Me.ReadXMLFile)
  End Sub

This disables the Read XML button until you have written the file to disk. It also wires up the ReadXMLFile delegate to the click event handler of this button.

Now add the following piece of code to the bottom of your existing WriteXMLFile method.

C#

      //enable read code
      cmbMode.SelectedIndex = 3;
      cmdRead.Enabled   = true;
      cmdWrite.Enabled  = false;
      txtIP.Text        = "";
      txtPass.Text      = "";
      txtOffset.Text    = "";
      txtRelay.Text     = "";
      lblDate.Text      = "";

VB

    'enable read code
    cmbMode.SelectedIndex = 3
    cmdRead.Enabled = True
    cmdWrite.Enabled = False
    txtIP.Text = ""
    txtPass.Text = ""
    txtOffset.Text = ""
    txtRelay.Text = ""
    lblDate.Text = ""

This code clears the screen, disables the Write XML button, and enables the Read XML button. The last bit of code to add, of course, is the ReadXMLFile method.

C#

    private void ReadXMLFile(object sender, EventArgs e)
    {
      double          RelayDelay  = 0.0;;
      DateTime        date        = DateTime.Today.AddYears(-28);
      string          IP          = "INVALID IP";
      MODE            mode        = MODE.None;
      int             TZ_Offset   = 0;
      string          Pword       = string.Empty;
      bool            Config      = false;
      CONFIG_STATE    cs          = CONFIG_STATE.C_UNKNOWN;

      //I use a state machine based upon the CONFIG_STATE value.
      //This is but one way to do this.
      XmlTextReader r = new XmlTextReader(fname);
      //Ignore all white space
      r.WhitespaceHandling = WhitespaceHandling.None;
      while (r.Read())
      {
        switch (r.NodeType)
        {
          case XmlNodeType.Element:
            if(r.Name == "Device_Configuration")
              Config = true;
            else
            {
              if(Config)
              {
                switch(r.Name)
                {
                  case "ConfigDate":
                    cs = CONFIG_STATE.C_DATE;
                    break;
                  case "IP":
                    cs = CONFIG_STATE.C_IP;
                    break;
                  case "Mode":
                    cs = CONFIG_STATE.C_MODE;
                    break;
                  case "PassWord":
                    cs = CONFIG_STATE.C_PASS;
                    break;
                  case "TimeZoneOffset":
                    cs = CONFIG_STATE.C_OFFSET;
                    break;
                  case "RelayDelay":
                    cs = CONFIG_STATE.C_RELAY;
                    break;
                }
              }
            }
            break;
          case XmlNodeType.Text:
            if(Config)
            {
              switch(cs)
              {
                case CONFIG_STATE.C_DATE:
                  date = XmlConvert.ToDateTime(r.Value);
                  break;
                case CONFIG_STATE.C_IP:
                  if(Regex.IsMatch(r.Value,
                                   "^\\d{1,3}.\\d{1,3}.\\d{1,3}.\\d{1,3}$"))
                    IP = r.Value;
                  break;
                case CONFIG_STATE.C_MODE:
                  switch(r.Value)
                  {
                    case "Dumb":
                      mode = MODE.Dumb;
                      break;
                    case "OnLine":
                      mode = MODE.OnLine;
                      break;
                    case "OffLine":
                      mode = MODE.OffLine;
                      break;
                    case "None":
                    default:
                      mode = MODE.None;
                      break;
                  }
                  break;
                case CONFIG_STATE.C_PASS:
                  Pword = Classify.Decrypt(r.Value);
                  break;
                case CONFIG_STATE.C_OFFSET:
                  //Do some validation
                  if(Regex.IsMatch(r.Value, "^[0-9+-]+$"))
                    TZ_Offset = XmlConvert.ToInt32(r.Value);
                  break;
                case CONFIG_STATE.C_RELAY:
                  //Do some validation
                  if(Regex.IsMatch(r.Value, "^[0-9+-.]+$"))
                    RelayDelay = XmlConvert.ToDouble(r.Value);
                  break;
              }
            }
            break;
        }
      }
      r.Close();
      txtIP.Text         = IP;
      txtPass.Text       = Pword;
      txtOffset.Text     = TZ_Offset.ToString();
      txtRelay.Text      = RelayDelay.ToString();
      lblDate.Text       = date.ToLongDateString();
      for(int k=0; k<cmbMode.Items.Count; k++)
      {
        if((MODE)cmbMode.Items[k] == mode)
        {
          cmbMode.SelectedIndex = k;
          break;
        }
      }
      cmdWrite.Enabled = true;
      cmdRead.Enabled  = false;
    }

VB

  Private Sub ReadXMLFile(ByVal sender As Object, ByVal e As EventArgs)
    Dim RelayDelay As Double = 0.0
    Dim dte As DateTime = DateTime.Today.AddYears(-28)
    Dim IP As String = "INVALID IP"
    Dim mode As MODE = mode.None
    Dim TZ_Offset As Int32 = 0
    Dim Pword As String = String.Empty
    Dim Config As Boolean = False
    Dim cs As CONFIG_STATE = CONFIG_STATE.C_UNKNOWN
    'I use a state machine based upon the CONFIG_STATE value.
    'This is but one way to do this.
    Dim r As XmlTextReader = New XmlTextReader(fname)
    'Ignore all white space
    r.WhitespaceHandling = WhitespaceHandling.None
    While r.Read()
      Select Case r.NodeType
        Case XmlNodeType.Element
          If r.Name = "Device_Configuration" Then
            Config = True
          Else
            If Config Then
              Select Case r.Name
                Case "ConfigDate"
                  cs = CONFIG_STATE.C_DATE
                Case "IP"
                  cs = CONFIG_STATE.C_IP
                Case "Mode"
                  cs = CONFIG_STATE.C_MODE
                Case "PassWord"
                  cs = CONFIG_STATE.C_PASS
                Case "TimeZoneOffset"
                  cs = CONFIG_STATE.C_OFFSET
                Case "RelayDelay"
                  cs = CONFIG_STATE.C_RELAY
              End Select
            End If
            End If
          case XmlNodeType.Text:
          If Config Then
            Select Case cs
              Case CONFIG_STATE.C_DATE
                dte = XmlConvert.ToDateTime(r.Value)
              Case CONFIG_STATE.C_IP
                If Regex.IsMatch(r.Value, _
                                 "^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$") Then
                  IP = r.Value
                End If
              Case CONFIG_STATE.C_MODE
                Select Case r.Value
                  Case "Dumb"
                    mode = mode.Dumb
                  Case "OnLine"
                    mode = mode.OnLine
                  Case "OffLine"
                    mode = mode.OffLine
                  Case "None"
                    mode = mode.None
                End Select
              Case CONFIG_STATE.C_PASS
                Pword = Classify.Decrypt(r.Value)
              Case CONFIG_STATE.C_OFFSET
                'Do some validation
                If Regex.IsMatch(r.Value, "^[0-9+-]+$") Then
                  TZ_Offset = XmlConvert.ToInt32(r.Value)
                End If
                case CONFIG_STATE.C_RELAY:
                  'Do some validation
                  If Regex.IsMatch(r.Value, "^[0-9+-.]+$") Then
                    RelayDelay = XmlConvert.ToDouble(r.Value)
                End If
            End Select
          End If
      End Select
    End While
    r.Close()
    txtIP.Text = IP
    txtPass.Text = Pword
    txtOffset.Text = TZ_Offset.ToString()
    txtRelay.Text = RelayDelay.ToString()
    lblDate.Text = dte.ToLongDateString()
    Dim k As Int32
    For k = 0 To cmbMode.Items.Count - 1
      If CType(cmbMode.Items(k), MODE) = mode Then
        cmbMode.SelectedIndex = k
        Exit For
      End If
    Next
    cmdWrite.Enabled = True
    cmdRead.Enabled = False
  End Sub

This is a lot of code just to read a few lines of XML data. Let's examine it.

For those of you familiar with the Simple API for XML (SAX) reader method, the XmlTextReader is much the same. The big difference, though, is that while the SAX reader "pushed" nodes in the form of events, this reader pulls nodes from the XML file.

Quite a few types of nodes are available that I can trap, but because this is a very simple XML file, I trap only the elements and their values.

The first thing I want to know is if this is a device configuration file. If not, I fall through all the code as the reader traverses the file. Once I know that this is a device configuration file, I set a flag and crank up the state machine.

I look at each element name to see if it jibes with what I expect out of this file. If so, I set the machine state and wait for the next iteration of the read cycle. If this next iteration is a value (it should always be some kind of value in my XML file), I convert the value to the proper data type according to the state machine value.

Notice that before I convert the numerical values, I use the regular expression parser to validate that the data is in the correct form. I do this for the IP address as well.

Tip 

Regular expressions are a big part of data validation. See Chapter 8 for more involved uses of regular expressions.

I do this conversion process until the XML file has been read completely. The only thing that I cannot convert explicitly is the enumerated value that I save to the file.

Enumerated Values in XML Files

You have two ways to save the enumerated value in the XML file. The first way is to convert the enumeration to an integer and save the integer to the file. This method makes for much less code in converting the value back. There is one big problem with doing this, though.

Suppose I had saved the enumeration value in the file, and then 6 months later I inserted another enumerated value in the middle of the Enum definition in my program. The next time I read in the integer from the XML file and convert to an enumeration, I could be one off. This can be an elusive and possibly destructive bug to have.

The second way—the way I chose to implement—is to save the text form of the enumeration to the XML file. Although this takes a little bit of code to restore from the file, it actually saves two bugs. The first bug it eliminates is the addition/subtraction of an enumerated value in the definition. The second bug it eliminates is the deletion of the enumerated value itself. If the code does not find the enumeration match, it defaults to whatever I initialized the value at.

Validating Using the XmlTextReader

It may seem like overkill, but you can see from this reader code that I converted the string value from the XML file into a strong data type then converted it back so I could display it in the TextBox. I did this to prove a point.

Most of the time when you save data to an XML file, it will be nonstring data. You will need to do the conversion process back and forth as you write and read the data.

As I noted earlier, the XmlTextReader is not a validating reader. Everything it pulls in is text. You must do the validation yourself. You can see from the example that this validation is not hard, but it can be time consuming. Suppose I had quite a bit of data in all different kinds of formats that I wanted to read from an XML file. The better way to go would be to have some kind of easy validation while the file was being read. Well, it just so happens that .NET provides you with one: the XmlValidatingReader.


Team LiB
Previous Section Next Section