65.9K
CodeProject 正在变化。 阅读更多。
Home

JogLog:一个在 .NET Compact Framework 中使用 XML DOM 的应用程序

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.33/5 (3投票s)

2005年1月21日

CPOL

8分钟阅读

viewsIcon

53767

downloadIcon

128

一个简单的应用程序,使用 XML DOM 来创建和维护一个 XML 日志文件。

Sample Image - JogLog.jpg

引言

本文的目的是向初学者展示如何使用 XML DOM 来操作一个简单的 XML 架构,并解释我如何设计这个应用程序。我试图让它不仅仅是一个 XML DOM 使用示例,而是提供一个可用、相对友好且健壮的应用程序。请告诉我你的想法!

编写此应用程序时,我的目标是学习使用 .NET Compact Framework 对 XML DOM 的实现。我找到的所有示例代码都使用了完整的 .NET Framework 功能。此外,我想在 CE 设备上使用日期选择器控件。

背景阅读

本文 详细介绍了日期时间选择器控件。

应用程序演练

我最初使用日历/计划表纸质笔记本记录我的跑步情况。切换到 PDA 后,我失去的一项重要功能是能够根据自己的时间安排快速输入数据。例如,使用日历/计划表,我可以轻松输入当月第三天的信息,但我的 PDA 上没有等效的机制。

我决定编写一个简单的应用程序来跟踪这些数据,并(如果合理的话)将其扩展到可以轻松完成纸笔难以完成的任务。

应用程序的目标如下:

  1. 记录我某一天跑了多远(以英里或公里为单位)。
  2. 记录我某一天跑步所花费的时间(可选)。
  3. 计算跑步距离的累积总和。用笔和纸计算并不难,但容易出错。
  4. 如果给定距离和时间,可以提供平均速度反馈。同样,这并不难,但很繁琐且容易出错。
  5. 我想让这个应用程序的质量足够高,让我愿意每天使用它。

实现的具体目标如下:

  1. 使用 XML 文件存储数据。
  2. 提供日期时间选择器控件来选择某条记录的日期。
  3. 适用于任何 Windows Mobile 2002 或更高版本的设备。
  4. 提供一个不包含外部依赖项的示例解决方案。

我选择使用 XML 文件,唯一的目的是学习 XML DOM。文本文件会是更简单的解决方案。典型的 JogLog.xml 文件内容如下所示:

<JogLog>
    <RunEntry Distance="5.25" Time="37.20" Date="1/3/2005" />
    <RunEntry Distance="5" Time="36.4" Date="1/5/2005" />
</JogLog>

这遵循 XML 的节点/属性模型。我决定使用属性,因为生成的文件比此文件小。

<JogLog>
      <RunEntry>
         <RunDistance>5.25</RunDistance>
         <RunTime>37.20</RunTime> 
        <RunDate>"1/3/2005"</RunDate>
    </RunEntry>
</JogLog>

1.x .NET CF 的一个常见痛点是缺少日期选择器控件。我研究了三种解决方案:OpenNetCF 组织控件、一个商业开发的控件以及 Microsoft 网站上(如上链接)的一个免费示例控件。
选择控件比我预期的要困难。商业控件具有我想要的所有功能,但价格超出我的承受能力,而且我不想引入外部依赖。我回避 OpenNet CF 解决方案,仅仅是因为我想让我的应用程序对初学者来说易于构建,并且同样不想引入任何外部依赖。

我选择了 Microsoft 控件,因为它易于分发,并且具有我想要的最低限度的功能。

为了解释应用程序,请考虑跑步者 Bob 和以下场景:
Bob 连续两天每次跑步 3.09 英里,每次花费 23 分 10 秒。他想知道他跑了多快以及自安装此应用程序以来总共跑了多远。

在设计应用程序时,我决定将日志文件 (joglog.xml) 保存在与程序文件相同的文件夹中。这时我遇到了第一个障碍:我无法(也一直无法)找到 .NET CF 获取应用程序安装文件夹名称的调用。我不得不像这样硬编码路径:

public string pathtofile = \\Program Files\\joglog\\joglog.xml;

接下来是设计表单。这很简单,除了日期选择器控件。由于 Paul Yao 的《C# .NET Compact Framework 编程》一书中已详细解释的原因,该控件无法添加到 C# 工具箱中。重新阅读 MS 示例代码,我找到了将控件添加到表单的方法。

dtp = new DateTimePicker();
//initialize the date time picker control on the form
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;    

由于 .NET CF 很少退出程序,因此调用 DateTime.Now 将日期选择器设置为今天的日期不如其本来能有的那样有用。

为第一次运行创建文件很简单。

try
{
    XmlTextWriter tw = new XmlTextWriter(pathtofile,System.Text.Encoding.UTF8 );
    tw.WriteStartDocument ();
    tw.WriteStartElement ("JogLog");
    tw.WriteEndElement();
    tw.Close();
}

我在 try 块中这样做了,catch 语句会向用户显示一个警报。

System.Windows.Forms.MessageBox.Show("Could not create the log file. " 
                                      + ee.ToString());

这说明了我使用 try/catch 块进行应用程序设计的思路。我不想用用户无法立即修复的错误来打扰用户。如果出现可以忽略的错误,应用程序应该忽略它并继续。如果可以忽略该错误,则忽略但记录下来。如果以上都不是,那么通过消息框提醒用户,让他们知道发生了什么失败。在这种情况下,日志文件不存在且无法创建,因此会通知用户。

为了预防错误,我添加了一个简单的例程,如果在文本框中未输入有效的距离,则将“保存”按钮变灰。

private void txtDistance_TextChanged(object sender, System.EventArgs e)
{
    TurnOffAvg();        
    this.btnSave.Enabled = false;
    if (txtDistance.Text !="")
    {
        try
        {    //is there a number value in the text field for distance?
            System.Convert.ToDouble(txtDistance.Text);
            this.btnSave.Enabled=true;
         }
         catch(Exception IsNotANumber)
         {this.btnSave.Enabled = false;}//don't care about the exception
     }
}

这同样是一个很好的例子,说明了如何处理我不在乎的异常。在这种情况下,Bob 可能在距离文本框中输入了“Apple”。最明智的做法是默默地禁用“保存”按钮,以便应用程序能够优雅地处理错误。尽管 Convert 系统调用可能会抛出异常,但我看不到做其他事情的必要。

因此,根据 Bob 和他跑步的用户场景,Bob 输入“3.09”。“保存”按钮应该启用,因为时间是可选的。(我跑步时并不总是记录时间)。

但在这种情况下,他输入的时间是“23.10”。或者,他也可以输入“23:10”。无论哪种方式,都会调用时间分隔符的解析器。

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; 

唉,如果 CF 有时间输入控件就好了……

现在,当 Bob 点击“保存”时,会计算平均速度,并显示用于显示其值的文本控件。

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;

这实现了我完成更多工作的目标。最后,如果需要,XML 会被添加到文件中。

bool fileupdatedorsaved=false;
//check to see if an entry is being modified or created new
try
{        //look through the file, again!
    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;
        }
    }
}

首先,我遍历文件并为每个节点构建一个节点列表。节点格式是:
RunEntry Distance="3.09" Time="23.10" Date="01/05/05"

如果 Bob 修改了“1/05/05”的跑步距离,代码将找到该日期的条目,然后将 Distance 和 Time 值更改为新值。

这是节点上一个简单的属性列表。由于我使用日期字段作为“唯一”字段,我检查每个条目以确保用户尝试保存的条目的日期不对应于现有条目。如果是,那么我假设 Bob 正在修改他当天的条目,并应用更改。这限制了我每天只有一个条目。
在我们的场景中,没有匹配项,因此 Bob 正在添加一个新条目。

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 现在可以添加另一个跑步记录。假设他做完了,然后退出了程序。

当他重新启动时,文件会被打开,并从距离字段计算出一个运行总计(哈哈,双关语)。

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();

基本上就是这样。其余代码负责一些 UI 整理(例如,在未计算平均值时将其关闭等)以及处理时间的其他解析情况。

经验教训,或我希望能够改变的地方

按任意顺序列出,我希望有一个更好的日期选择器控件。例如,我希望可以直接在控件的文本字段中输入日期来设置日期,而不是通过日历。如果我要销售一款商业应用程序,我不会使用 MS 控件。

使用 XML DOM 相当容易,一旦我阅读了 MSDN 的完整解释。确实,解释是存在的,但我需要付出的学习精力是巨大的。我找到的大部分文档都与使用 XSD 相关,而 XSD 在 .NET CF 中不受支持。

我学会了拥有丰富控件集的美妙之处。如果我有一个时间输入控件(或者仅仅是一个文本框控件的“仅数字”等效项),我将需要编写的代码会少得多。

我不知道我加载文档并在其超出范围时释放它的实现是否比在 JogLog 类中添加一个 XMLDocument 成员并使其在应用程序运行时保持打开状态要好。文档指出整个文档都保存在内存中,并鼓励您不要一直加载它,但由于这个文件预计会很小,所以也许我可以一直加载它。

事实证明,您必须使用 P/Invoke CE 调用来获取不同语言设备上不同文件夹的特殊路径名。我在文章提交时还没有发现这一点,所以也许我会在未来的更新中研究这一点。

我不喜欢我实现“this.Distance”和“txtDistance.Value”值的方式。我似乎在这两个值之间来回切换,而它们在整个过程中应该相等。

我没有图标技能 :P

历史

提交于 2005 年 1 月 21 日

© . All rights reserved.