Introduction
The goal of this article is to show beginners how the XML DOM can be used to manipulate a simple XML schema and to explain how I designed this application. I tried to make this more than a sample of using the XML DOM and provide a usable, somewhat friendly and robust application. Let me know what you think!
Learning about using the .NET compact framework's implementation of the XML DOM was a goal I had when writing this app. All the sample code I could find used the full .NET framework functionality. Additionally, I wanted to use a datepicker control on a CE device.
Background Reading
This article gives details on the datetime picker control.
Application Walkthrough
I had origally used a calendar/planner paper notebook to keep track of my running. After switching to a PDA, the one piece of functionality I lost was the ability to quickly enter this data on whatever schedule I wanted. For instance, with the calendar/planner, I could easily enter data for the third of the month, but there was no equivalent mechanism for my PDA.
I decided to write a simple application to keep track of this data, and extend it (if it made sense) to do things not easily done with paper and pencil.
The goals for the application are this:
- To maintain a record of how far (in miles or kilometers) I ran on a given day.
- To maintain a record of the time spent running on a given day (optional).
- To compute a cumulative total of distances run. This was not terribly hard to do with pencil and paper, but was prone to mistakes.
- If given a distance and time, give feedback on average speed. Again, not terribly hard, but tedious and mistake prone.
- I wanted to make this application of a high enough quality that I'd be happy to use it daily.
The goals for the implementation are these:
- Use an XML file to store the data.
- Provide a datetime picker control to select the date for a given entry.
- To work on any Windows Mobile 2002 or higher device.
- To provide a sample solution which has no external dependencies.
The only reason I chose to use an XML file was to learn about the XML DOM. A text file would have been an easier solution. The contents of a typical sample JogLog.xml file would look like this:
<JogLog>
<RunEntry Distance="5.25" Time="37.20" Date="1/3/2005" />
<RunEntry Distance="5" Time="36.4" Date="1/5/2005" />
</JogLog>
This follows the node/attribute model of XML. I decided to use attributes since the files which resulted were smaller than this type:
<JogLog>
<RunEntry>
<RunDistance>5.25</RunDistance>
<RunTime>37.20</RunTime>
<RunDate>"1/3/2005"</RunDate>
</RunEntry>
</JogLog>
A common point of frustation with the 1.x .NET CF is the lack of a datepicker control. I looked at three solutions: the OpenNetCF org controls, a commercially developed control and a free sample control at Microsoft's web site (link above).
Picking a control was actually a harder proposition than I expected. The commercial controls had all the functionality I wanted, but cost more than I was willing to pay, and I did not want to take an external dependency. I shied away from the OpenNet CF solution simply because I wanted to keep my application easy to build for beginners, and again, did not want to introduce any external dependencies.
I decided on the Microsoft control, since it it easy to distribute and has the bare minimum functionality I want.
To explain the application, consider Bob the runner and this scenario:
Bob runs 3.09 miles twice on consecutive days at 23 minutes 10 seconds each. He wants to know how fast he ran and how far he has run since installing this application.
In desiging the application, I decided I wanted to keep the log file (joglog.xml) in the same folder as the program file. Here's where I hit my first snag: I could not (and have not been able to) find the .NET CF call to get the name of the installed folder for an application. I had to hard code the path like this:
public string pathtofile = \\Program Files\\joglog\\joglog.xml;
Next on the list was to design the form. This was easy, except for the datepicker control. For reasons that are well explained in Paul Yao's ".NET Compact Framework Programming with C#" book, the control can't be added to the C# toolbox. Re-reading the MS sample code showed me how to put the control on the form:
dtp = new DateTimePicker();
dtp.Location = labelPlaceHolder.Location;
dtp.Size = labelPlaceHolder.Size;
labelPlaceHolder.Parent.Controls.Add(dtp);
labelPlaceHolder.Parent.Controls.Remove(labelPlaceHolder);
dtp.Format = DateTimePickerFormat.Short;
dtp.Value = DateTime.Now;
Since the .NET CF seldom exits a program, the call to DateTime.Now to set the Date Picker to today's date is not as useful as it could be.
Creating the file for the first run case is simple:
try
{
XmlTextWriter tw = new XmlTextWriter(pathtofile,System.Text.Encoding.UTF8 );
tw.WriteStartDocument ();
tw.WriteStartElement ("JogLog");
tw.WriteEndElement();
tw.Close();
}
I did this in a try block, and the catch statement displays an alert to the user:
System.Windows.Forms.MessageBox.Show("Could not create the log file. "
+ ee.ToString());
This points out my design with the try/catch blocks throughout my application. I did not want to alert the user with errors which he could not fix immediately. If an error was hit that should be ignored, the app should ignore it and continue. If the error could be ignored, ignore but log it. If neither of these was true, then alert the user via a message box to let him know something failed. In this case, the log file does not exist and cannot be created, so let the user know.
In the spirit of preventing errors, I added a simple routine to gray out the Save button if no valid distance is entered.
private void txtDistance_TextChanged(object sender, System.EventArgs e)
{
TurnOffAvg();
this.btnSave.Enabled = false;
if (txtDistance.Text !="")
{
try
{ System.Convert.ToDouble(txtDistance.Text);
this.btnSave.Enabled=true;
}
catch(Exception IsNotANumber)
{this.btnSave.Enabled = false;} }
}
This also is a good example of hitting an exception I do not care about. In this case, Bob could have typed "Apple" in the text box for distance. The most common sense action to take is to disable the Save button silently so the app handles the error gracefully. I can't see a need to do anything else even though the Convert system call may throw an exception.
So following my user scenario of Bob and his run, Bob types "3.09." The Save button should get enabled since time is optional. (I simply don't always time myself when I run).
But in this case he enters his time of "23.10." Alternately, he could enter "23:10." Either way, the parser for time separation is called:
mins=txtTime.Text.Substring(0,len-(len-sepposit[0]));
secs=txtTime.Text.Substring(sepposit[0]+1,len-(sepposit[0]+1));
runtime =System.Convert.ToDouble(mins)/60+System.Convert.ToDouble(secs)/3600;
Ah, if only the CF had a time field control...
Now, when Bob clicks Save, the average speed is computed and the text control for it's value is shown:
double avg = this.dist / this.time ;
NumberFormatInfo nfi = new CultureInfo( "en-US", false ).NumberFormat;
txtAvg.Text = avg.ToString( "N", nfi );
txtAvg.Visible = true;
lblAvg.Visible = true;
This meets my goal of doing more And, finally, the XML is added to the file if needed:
bool fileupdatedorsaved=false;
try
{ System.Xml.XmlNodeList xlist =
x.DocumentElement.GetElementsByTagName("RunEntry");
foreach (System.Xml.XmlNode i in xlist)
{
if (dtp.Value.ToShortDateString() ==
i.Attributes.GetNamedItem("Date").Value
&& fileupdatedorsaved==false)
{
i.Attributes["Distance"].Value = txtDistance.Text ;
i.Attributes["Time"].Value = txtTime.Text;
fileupdatedorsaved=true;
}
}
}
First, I look through the file and build a node list for each node. The node format is:
RunEntry Distance="3.09" Time="23.10" Date="01/05/05"
If Bob had changed the distance run for "1/05/05" the code would find the entry for the date, then change the Distance and Time values to the new values.
This is a simple attribute list on the node. Since I'm using the date field as "the" field which makes the entry unique, I check each entry to make sure the date of the entry the user is trying to save does not correspond to an existing entry. If it does, then I assume Bob is modifying his entry for the day, and apply the changes. This limits me to having only one entry per day.
In our scenario, there are no matches, so Bob is adding a new entry.
XmlNode newNode = x.CreateElement("RunEntry");
newNode.Attributes.Append(x.CreateAttribute("Distance"));
newNode.Attributes.GetNamedItem("Distance").InnerText=txtDistance.Text;
newNode.Attributes.Append(x.CreateAttribute("Time"));
newNode.Attributes.GetNamedItem("Time").InnerText=txtTime.Text ;
newNode.Attributes.Append(x.CreateAttribute("Date"));
newNode.Attributes.GetNamedItem("Date").InnerText=dtp.Value.ToShortDateString();
x.DocumentElement.AppendChild(newNode);
Bob can add his entry for another run now. Suppose he does and then exits the program.
When he restarts, the file gets opened, and a running total (har, har, pun intended) is computed from the distance fields.
this.total=0;
doc.Load(pathtofile);
try
{
System.Xml.XmlNodeList xlist
= doc.DocumentElement.GetElementsByTagName("RunEntry");
foreach (System.Xml.XmlNode i in xlist)
{
try
{
this.total = this.total
+ System.Xml.XmlConvert.ToDouble(i.Attributes.GetNamedItem("Distance").Value);
}
catch...
txtTotal.Text = this.total.ToString();
That's essentially it. The rest of the code does some UI tidying (turning off the average field if it's not computed, etc..) and handles other parsing cases for time.
Lessons, or What I Wish I Could Change
In no priority order, I wish I had a better Date Picker control. For instance, I wish I could just type in the text field of the control to set the date, rather than going through the calendar. If I wanted to sell a commercial application, I would not use the MS control.
Using the XML DOM was pretty easy, once I had read the ENTIRE MSDN explanation of it. Really, the explanations are there, but the amount of learning I had to do was steep. Much of the documentation I found related to using XSDs, which are not supported in the .NET CF.
I learned the beauty of having a rich set of controls. If I would have had a time field control (or simply a "number only" equivalent of a text box control), I would have had much less code to write.
I have no idea if my implementation of loading the document and releasing it when it goes out of scope is better than adding an XMLDocument member to the JogLog class and keeping it open while the application is running. The documentation points out that the entire document is kept in memory and encourages you not to keep it loaded, but since this file is expected to be pretty small, I maybe could have kept it loaded always.
It turns out that you have to use a P/Invoke CE call to get the special path name of the different folders on different language devices. I did not find this out in time for this article, so maybe I'll research that for a future update.
I don't like the way I implemented the "this.Distance" and "txtDistance.Value" values. I seem to flip back and forth between using the two values, which should be equal throughout.
I have no icon skillz :P
History
Jan 21, 2005 Submitted