Integrating XML

This article was originally published in VSJ, which is now part of Developer Fusion.
One of the goals of data-driven programming is to avoid the need to recompile your program every time you confront a changed environment.

A client recently asked me to help him rewrite a Windows program his company uses for testing its own products. The tests are conducted using various gadgets (e.g. gadgets that change the temperature, send signals to their product, and so forth). All of the gadgets are controlled by an engineer’s desktop computer.

Every time my client received a new testing gadget, he had to write a new module for his program; each time a test was changed, a new module was written to reflect the new steps in the sequence. This greatly slowed down the engineers, who had to wait for the code to be written and tested. The system was laborious, inflexible and inefficient.

My goal was to replace his program with a data-driven application, so that each gadget and each test sequence could be described using XML files. The program would know how to read these files and run the test sequences. Eventually, the program would also provide wizards for creating these XML files so that they did not have to be written by hand.

I have simplified the solution I provided, and stripped down the program to create a case study for this article.

Analysis and design

The quickest way to convey both the problems we were trying to solve and the design we came up with is to examine a few use cases that illustrate how the new program will work:
  • Use Case 1 – A New Gadget is received. The chief engineer will describe each gadget in an XML file stored in the Gadgets subdirectory. The XML file will describe how the gadget must be configured, and what commands it can send.
  • Use Case 2 – An Engineer is ready to add a gadget to his desktop set. Each engineer creates a PC-specific XML file for each gadget to be controlled by his PC. The settings required are read from the gadget XML file and stored in an XML file in the PCs subdirectory.
  • Use Case 3 – An Engineer wants to set up a test sequence. The engineer creates an XML file in the Sequences directory that describe the steps to be taken to complete a test.
  • Use Case 4 – The Engineer is ready to run a test. The Engineer picks a test sequence to execute. The program reads through the sequence XML file, and sends commands as described in the gadget’s XML file, using the connection information stored in the current PC XML file.
  • Use Case 5 – Modifications must be made to the Gadget, PC or Sequence file. The Engineer identifies the file to be modified (e.g., picks files from a list and clicks Edit). The current values are read from the XML file into the wizard. The engineer can save the modified file by overwriting the existing name, or by creating a new file.

Development: Incremental approximation

Rather than trying to implement this program directly all at once from a complete design, we took the approach of incremental approximation.
  1. The XML file for a sequence is written by hand. The Sequence file is read and the sequence is simulated.
  2. Gadget XML files and PC XML files are written by hand, and the sequence is rewritten, sending an appropriate command string for each gadget to the logger.
  3. Wizards are written that write the Gadgets, PC and Sequence XML files.
  4. The Wizards are modified to allow editing of existing XML files.
  5. Hook up the program to the actual gadgets and make sure it all works.

The XML files

When you download and unpack the source for this article, you’ll find that you have a master directory VSJXML which has under it two subdirectories: Code and Config. Within Config you’ll find three subdirectories: Gadgets, PCs and Sequences, each with one or more XML files.

Take a look at JLGadget02.xml (in VSJXML/Config/Gadgets) The root element is Gadget. Below the Gadget element are two elements: Configuration and Commands, each of which marks off one of the principal sections of the file. Within the Configurations element there are Configuration (note the singular) sub-elements, each of which has a name and a type attribute. Some Configuration elements (e.g., Connection) also have ConfigurationString sub-elements which are used by the wizard to provide the user with a choice of values (e.g., the user can choose that the Connection value is usb, parallel or serial).

The Commands section has Command sub-elements. Each command element has a name attribute and one or more commandString sub-elements. The value of the CommandString element is the string sent to the gadget for that command.

Thus, we see from this file, that to use a JLGadget02 the engineer must specify the address and the connection type for the controlling computer. These values are specified in an XML file in the PCs subdirectory, such as JessePCS1.xml. In addition, we see that the JLGadget02 gadget can issue two commands: Set Frequency and Set Voltage.

The test sequences are specified in XML files in the Sequences directory, such as VSJSequence01.xml.

The sequence XML file is divided into two sections under the root element Sequence. The Gadgets section names the Gadgets to be used in the sequence, and the Steps section lists the steps to be taken in the test sequence.

In the example sequence (VSJSequence01.xml), the first step is to issue the Initialize command to the JLGadget:

<Step Gadget="JLGadget" Cmd="Initialize"
	Name="Initialize">
</Step>
The second step is a Loop command:
<Step Cmd="Loop" Name="Set Frequency Values">
The Step element for a Loop has within it attributes that identify the Gadget and command to issue:
<Loop Gadget="JLGadgetO2" Cmd="Set Frequency"
	Name="Set Frequency">
…and then, nested within the Loop element are LoopValue elements that specify the various values to supply to that command:
<LoopValue>100
</LoopValue>
<LoopValue>200
</LoopValue>
<LoopValue>300
</LoopValue>
If there are two Loop elements within a single step, the combination of all the values of the two elements are issued. Thus, when running VSJSequence01, you would see commands issued to set the Voltage to 10 and the Frequency to 100, the Voltage to 20 at Frequency 100, and voltage 30 at Frequency 100. You will then see Voltages 10, 20 and 30 issued again at Frequency 200, and the entire set once more at Frequency 300.

Executing the program

Figure 1 shows the test form in action, using the sequence file, gadget file and PC file described above.

Figure 1: The TestForm dialog window
Figure 1: The TestForm dialog window

The best way to see how this all works, is to step through the execution of a sequence.

When the form loads, it searches the PC directory for XML files and fills the first drop down list; it searches the Gadgets directory to fill the second drop down, and it searches the Sequences directory to fill the first list box. The Config file path text box on top is used to set the location of the Config directory (which is the root for the Gadgets, PCs and Sequences subdirectories). The lower list box shows the log recorded as each step in running the sequence was accomplished. (Note that you see the combination of the three frequencies (100,200,300) and the three voltages (10, 20, 30). The value 500 shown on each line is the “address” of the gadget named JLGadget02 on this particular PC (which connects using usb).

Stepping Through

To get started, put a break point on the first line of the event handler for the RunSequence button, as shown in Figure 2.

Figure 2: Setting a breakpoint to follow the logic
Figure 2: Setting a breakpoint to follow the logic

By the time this break point is reached, the two drop down list boxes (that show the XML files in the PC and Gadgets directory) have been filled. To keep our focus on XML, the details of reading these directories is not included here, but can be seen in the complete source code. (See, for example LoadPCFiles().)

The first steps within the btnRunSequence_Click event handler are the creation of a new Logger object, and then registering the logger_OnLoggableEvent method with the event that the Logger object raises.

Logger is a simple helper class I wrote to keep track of the progress of the program. Since I didn’t want to hardwire what is done with these strings (write to a file, write to the db, etc.,) the Logger class has a Write method that simply raises an event (OnLoggableEvent). Any interested classes (in our case, the form) can register with that event, and be notified when there is a message to be recorded. The registering class (e.g., the Form) can then do whatever it wants with the message that is passed in as an event argument. In our case, the form will update a list box.

Once the logger is set, you’ll determine the full path to the PC file and the Sequence file.

string pcFile = TestForm.path +
	"\\PCs\\" +
	this.cbPCs.SelectedItem.ToString();
string sequenceFile = TestForm.path +
	"\\sequences\\" +
	this.lbSequences.SelectedItem.
	ToString();
The path static member variable was stored at form load by reading the txtPath text box.

With these file names, (and an instance of the logger), you are ready to instantiate a SequenceRunner object:

SequenceRunner sequence = new
	SequenceRunner(sequenceFile,
	pcFile, logger);
The constructor for SequenceRunner simply initialises its members to hold the logger, the name of the sequence XML file and the name of the PC XML file. The real work is done within your button click event handler calls Run() on the new SequenceRunner object.
bool success = sequence.Run();
Step into the Run() method to see how the sequence is run.

Running the sequence

Within the Run method, a new XmlReader object is created to read the XML file.
XmlReader reader = new
	XmlTextReader(sequenceFileName);
The Read() method of the XmlReader will return true while there are nodes to read in the XML file.
while ( reader.Read() )
{
Each time you read a node, you check to see if it is an Element (rather than, for example, white space), and if it is, you switch based on the name of the Element:
// only handle elements
if ( reader.NodeType ==
	XmlNodeType.Element )
	{
		switch ( reader.LocalName )
		{
There are four element names you expect in the Sequence XML file: Sequence, Gadgets, Steps, and Step.

Sequence is the root element. There is no action to take when you see this element.

case "Sequence":	// root element
	break;
The Gadget element identifies the gadgets that you’ll need for this sequence. For each gadget named in the file, you’ll want to instantiate an instance of the Gadget class. This work is delegated to the method AddGadget, which takes the reader as an argument so that it can find all the Gadget elements before returning,
case "Gadgets":
	AddGadget(reader);
	break;
The AddGadget method reads through the subsequent nodes. As long as it finds an element or whitespace, it keeps reading until it has finish reading all the sub-nodes of the Gadget element,
private void AddGadget(
	XmlReader reader)
{
	while ( reader.Read() &&
		(reader.NodeType ==
		XmlNodeType.Element ||
		reader.NodeType ==
		XmlNodeType.Whitespace) )
	{
		//...
	}	// end while
Within the while loop you will check the localName of the elements found. If it is GadgetItem you are ready to create a new Gadget. You do so by getting the name attribute from the GadgetItem element and then pass that name, along with the pcFileName and logger (each stored when the SequenceRunner was created) to the constructor of Gadget.
Gadget newGadget = new
	Gadget(gadgetName,
	this.pcFileName, logger);
Once the Gadget object is created, it is added to the SequenceRunner’s gadgets hashtable, with the key set to the Gadget’s name:
this.gadgets.Add(
	gadgetName, newGadget);
The Gadget object itself has four member variables: an instance of a Logger, a string that represents the name of the Gadget, and two hashtables: one for the GadgetCommand objects this Gadget knows about, and a second hashtable for the GadgetConfiguration objects for this Gadget.
string name;
public string Name
	{ get { return name; } }
VSJXML.Logger logger;
private Hashtable commands;
public Hashtable Commands
	{ get { return commands; } }
private Hashtable configurations;
public Hashtable Configurations
	{ get { return configurations; } }
The constructor initialises the name and logger and the two hashtables, and then calls two helper methods, GetGadgetCommands and GetGadgetConfiguration to populate the hashtables.

GetGadgetCommands opens the associated Gadget XML file, looking for elements named Commands. Each time a Commands element is found it calls the AddCommands method, passing in the reader.

AddCommands looks for elements named Command, and instantiates a new GadgetCommand object based on the attributes of the element. The new GadgetCommand object is added to the Gadget’s commands hash table.

Similarly, GetGadgetConfiguration and its helper method AddConfigurations, adds GadgetConfiguration objects to the configurations hash table.

Once all the Gadgets have been created, the sequence XML file is read again, this time looking for Step elements. If the step element is not a loop, then it is a command to be sent to the gadget. The command name and the gadget name are extracted from the XML, and the gadget name is used as an index into the gadgets hash table, retrieving a Gadget object.

case "Step":	// commands or loops
	reader.MoveToAttribute("Cmd");
	string command = reader.Value;
	if ( command.ToUpper() != "LOOP" )
	{
		reader.MoveToAttribute(
			"Gadget");
		// get the gadget name
		string gadgetName =
			reader.Value;
		Gadget gadget = (Gadget)
			gadgets[gadgetName];
		// extract the gadget
The parameters for the command are extracted from the XML, and the Gadget’s SendCommand method is invoked, passing in the command and any parameters:
string param =
	GetCommandValue(reader);
gadget.SendCommand(command, param);
The SendCommand method uses the command name as an index into the Gadget’s hash table of commands, retrieving a reference to the GadgetCommand object. The CreateCommand method is then called on that GadgetCommand object, passing in the parameters. The CreateCommand method is responsible for assembling the correct sequence for the command, handling the parameters as necessary.

Handling loops

If the sequence has a loop, however, each of the permutations must be executed. Thus, in the loop shown in VSJSequence01.xml, you see that there are three values in the first command (Set Frequency) which must be permuted with the three values in the second command (Set Voltage). Thus you want to see these commands:
Set Frequencey 100 Set Voltage 10
Set Frequencey 100 Set Voltage 20
Set Frequencey 100 Set Voltage 30
Set Frequencey 200 Set Voltage 10
Set Frequencey 200 Set Voltage 20
Set Frequencey 200 Set Voltage 30
Set Frequencey 300 Set Voltage 10
Set Frequencey 300 Set Voltage 20
Set Frequencey 300 Set Voltage 30
To accomplish this, you will use recursion.

The first step is to load the loop by reading the XML file. The tricky thing here is you want to key on both the name of the Gadget and also the command (e.g., Set Frequency). You then want to create an array of the associated values (e.g., 100, 200, 300) and store all of that as a single entry in the loopValues hash table.

To do so, you’ll make an instance of a LoopCommandKey, which holds both the name of the gadget and the command as a combined key

LoopCommandKey key = new
	LoopCommandKey(gadgetName, cmd);
You use this as the key for the hash table, passing in as the value to store the ArrayList that was created by calling the helper method GetLoopValues.
loopValues.Add(key,values);
The net result is that you have a hash table where the key is the combination of the gadget name and the command name, and the value is actually an array of values.

When you call ExecuteLooop the first step is to convert the value strings stored in the hash table’s array of values to command strings that can be executed; this is done in the helper method AddCommands:

You loop through the Keys in the loopValues Keys collection (getting each LoopCommandKey in order). The LoopKey gives you the GadgetName and you can use that to extract a reference to the Gadget. You then loop through the Gadget’s configuration’s values collection, building up the configuration string.

Once that is accomplished, you extract the GadgetCommand object (using the CommandName from the loopkey as an index into the Commands collection of the gadget), and you pull out the list of values by using the loopkey itself as an index into the loopvalues hash table. You are now ready to create an array list of commands to issue. Each command will consist (for testing purposes) of the gadget name, followed by two colons, followed by the configuration string, followed by two colons and then ending with the command string. This sequence is, of course arbitrary, but you can easily see how this can be adapted to actually put together a command string that will be meaningful to the particular gadget.

The next, recursive step requires the Keys collection as an arrayList, so you iterate through the Keys, adding them to a new ArrayList called loopValuesArray.

The Combinator method will be invoked with the first key in the list as its first argument, and all the remaining keys (in an array list) as its second argument, and will return an array list of display values. The helper method FirstBut returns the first key in the out parameter, and removes that key from the array passed in.

ArrayList newKeyList = FirstBut(loopValuesArray,
	out firstKey);
Combinator, as you would expect, takes a key and a key list. If the keyList is empty, (the terminating condition) it returns the ArrayList found by using the key passed in as an index into the loop values. Otherwise it extracts the next key by calling FirstBut and then calls AppendToListOfStrings, passing in the value at the newly extracted key and reinvoking Combinator with the newly extracted key and the rest of the keylist).

AppendToListOfStrings is given two array lists, the list of strings you already have, and the new list of values you want to append; it appends the new list to the old and returns the list of lists.

Recursion is always confusing, but stepping through this code, and keeping an eye on the commands that are in the arrayLists will make the process easier to understand.

Creating the XML

In the first iteration of the program, the XML was created by hand. The finished product, of course, has wizards for creating the XML (and for editing it). There are three XML files to create, but they all work more or less the same way. To see a simple example, let’s look at creating (and editing) a PC file. To get started, put a break point on the handler for the CreatePC button.

The first thing to notice is that both the btnCreatePC and btnEditPC share a common event handler; this greatly reduces the amount of duplicated code. In order to tell which was pushed, we cast the sender variable to a Button and test to see if the name of the button is btnEditPC. If it is, we’ll call the overloaded version of CreatePC that recreates the wizard based on the saved XML file. Let’s start, however, with the simpler case in which the name is not btnEditPC (and thus is, presumably, btnCreatePC).

In this case, we instantiate the CreatePC form, passing in only the logger. The CreatePC form has a teeter-totter (Editor’s Note: US term for see-saw) consisting of two list boxes and buttons to move items from the left list box (the list of all equipment) to the right list box (the list of chosen equipment). There is also an area to the right of the list boxes in which panels will appear with details about the equipment as you click on them, as shown in Figure 3.

Figure 3: The user interface
Figure 3: The user interface

To save space we won’t walk through all the details of filling these various list boxes. Each time you click on an entry in the list of selected gadgets, however, a panel must be created (or made visible). This work is done in CreatePC.UpdatePanels. An XMLReader is instantiated to read the selected Gadget’s XML file. The form keeps a hashtable of Penel objects, indexed by the name of the Gadget. If the panel already exists, it is made visible. If the panel does not exist, it is created. As the Gadget’s XML file is read, controls are created and added to the panel.

The name of the element is read. Gadget elements create a label on the panel. Configuration elements create both a label and an input control (either a text box or a combo box depending on whether there are values associated with the Configuration element).

When the user clicks OK on this Wizard, it is time to save the configuration for the PC. You open a standard SaveFileDialog and ask for the name of the file to write to:

private void btnOK_Click(object
	sender, System.EventArgs e)
{
	string fileName = String.Empty;
	SaveFileDialog savePCFileDialog =
		new SaveFileDialog();

	savePCFileDialog.Filter =
		"PC file|*.xml" ;
	savePCFileDialog.RestoreDirectory =
		true;
	savePCFileDialog.InitialDirectory =
		TestForm.path + "\\PCs";

	// Get the file name from the file
	// dialog, if OK pressed, save the
	// file
	if(savePCFileDialog.ShowDialog()
		== DialogResult.OK)
	{
		fileName =
			savePCFileDialog.FileName;
	}
Once you have a file name, it is a simple matter to open an XMLTextWriter on that file, and to set that writer’s formatting property to indented so that the file will be more easily read by humans (computers don’t care about indentation in XML).
XmlTextWriter writer =
	new XmlTextWriter(
	fileName,System.Text.Encoding.UTF8);
writer.Formatting =
	Formatting.Indented;
The first couple lines write the start of document element:
<?xml version="1.0"
	encoding="utf-8"?>)
…and the start element:
<Configurations>
To create these lines the Writer has two special methods:
writer.WriteStartDocument();
writer.WriteStartElement(
	"Configurations");
You will now iterate through the collection of panels created earlier. For each panel you’ll create a Gadget element and use the name of the panel as the value for the name attribute:
writer.WriteStartElement("Gadget");
writer.WriteAttributeString(
	"Name",p.Name);
You find each control in the panel; if the control type is Textbox, you extract the value from the text property. If the control type is ComboBox, you extract the value as the selected item from the combo box:
foreach (Control ctrl in p.Controls )
{
	string controlName = ctrl.Name;
	string controlValue = String.Empty;
	Type controlType = ctrl.GetType();
	switch (controlType.ToString())
	{
		case
		"System.Windows.Forms.TextBox":
			controlValue =
				((TextBox) ctrl).Text;
			break;
		case
		"System.Windows.Forms.ComboBox":
			controlValue =
				((ComboBox)
		ctrl).SelectedItem.ToString();
			break;
In either case, once you have the control name and value, you are ready to write the element to the XML file. The first line:
writer.WriteStartElement(
	"Configuration");
…opens the element Configuration (<Configuration). The second line:
writer.WriteAttributeString(
	"Name",controlName);
…adds an attribute name and value (Name="Address"). The third line:
writer.WriteString(controlValue);
…puts in the value (100) and the fourth line:
writer.WriteEndElement();
…closes the element (</Configuration>)

In the XML file, the result looks like this:

<Configuration Name="Address">100
</Configuration>
When you finish with the panel you close the Gadget element:
writer.WriteEndElement();
…and when you finish with all the panels you close the configuration element, end the document and flush and close the writer:
writer.WriteEndElement();
writer.WriteEndDocument();
writer.Flush();
writer.Close();

Editing the XML

Returning to btnPC_Click, if the button chosen was edit (rather than create) the editing flag will be set true in CreatePC and the method PopulateFromXML will be called.

PopulateFromXML reverses the process described above, reading the XML file and creating the configurations, commands and gadgets reflected in the XML, and then updates the panels and controls accordingly.

Summary

There is a great deal of complexity in even this simple case study, but the key insight is that most of the work is UI manipulation. The actual reading and writing of the XML is made quite simple by the framework classes, and with this XML you can create a very flexible data-driven program.

I recommend you download the complete source code and step through it, looking carefully at how the XML works and how it is read and written.


Jesse Liberty provides customised training, consulting and contract programming, and is the author and editor of numerous books on .NET and software development.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“A computer is a stupid machine with the ability to do incredibly smart things, while computer programmers are smart people with the ability to do incredibly stupid things. They are, in short, a perfect match” - Bill Bryson