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

Resort Companion

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.95/5 (14投票s)

2004年5月25日

CPOL

7分钟阅读

viewsIcon

56589

downloadIcon

208

隆重推出度假伴侣,一款用于度假村的数据导航应用程序

引言

我喜欢迪士尼乐园。我去过很多次,现在仍然很喜欢回去。我曾多次想过,如果有一个便携式应用程序能帮助人们体验“迪士尼乐园之旅”会很方便,但我从未真正着手编写。现在,.NET Compact Framework 竞赛给了我所需的动力。想学习如何使用 Compact Framework 也起了作用。

度假伴侣是一款移动数据访问应用程序,可在 PocketPC 上运行(它也可以在桌面运行,但这没意思)。它旨在让用户轻松访问描述典型度假村(本例中是加利福尼亚州阿纳海姆的迪士尼乐园度假区)的数据库。数据库本身易于编辑和替换,并且用户界面是模块化的,以适应数据库架构的更改。

作为一名 Palm 开发者,我设计此应用程序时牢记 Palm Computing 的移动应用程序指南。这些指南强调使用速度和紧凑的显示布局胜过几乎所有其他方面,我认为这对于各种移动设备来说都是有意义的。Palm 平台有各种旨在尽可能紧凑的控件,而 PocketPC 的控件大多旨在模仿桌面控件,因此设计中需要做一些妥协。

使用度假伴侣

要使用此应用程序,只需从 PocketPC 上的文件资源管理器启动它即可。它会在加载和初始化数据库时显示一个启动页,完成后会激活“查找起始地点...”按钮。按下此按钮将打开“查找器”,允许您定位度假村中的一个地点。使用“问题”选项卡形成查询,然后选择“答案”选项卡查看结果。选择一个地点并点击“确定”按钮继续。

您选择的地点将显示在屏幕上。显示屏的上部显示有关该地点的信息,下部显示附近的其他地点。选择其中一个地点将使其成为当前地点。显示屏最底部是一个按钮栏,上面有两个按钮:放大镜将再次调出查找器表单,而星形按钮会将当前选择的地点标记为“收藏”。收藏地点会添加到附加到星形的弹出菜单中。请注意,您的收藏仅存储在 RAM 中,因此重置 PocketPC(甚至停止度假伴侣应用程序)将清除列表。

一些注意事项:数据库远未完成,在某些情况下,甚至不太准确(换句话说,暂时不要完全依赖此数据库!)。特别是,附近地点的列表有时...嗯,对“附近”的估计有些“慷慨”。随着时间的推移,我希望能将数据库完全充实。我已经在模拟器和 HP Jornada 上测试了此应用程序,但我没有时间测试它的每个部分,所以它可能仍然存在某个严重的崩溃错误。

内幕消息

我已将整个解决方案打包到上面链接的 .ZIP 文件中。它已准备好在 Visual Studio.NET 中打开,因此您可以检查源代码,或添加自己的数据库。数据库架构足够简单,您应该不会在进行更新时遇到任何问题。如果您确实更新了数据库,请务必运行 `Mobile` 项目中的 `compress.cmd` 脚本。该脚本使用 `DataCompressor` 实用程序压缩 `Data` 项目中的 XML 和 XSD 文件,并将它们移动到 `Mobile` 项目中进行部署。我对 Visual Studio.NET 配置进行了更改,以便双击 .CMD 文件会启动它而不是在编辑器中打开它(使用“打开方式...”菜单自行设置)。这节省了很多时间。

在幕后,代码大部分都很直接。数据库使用 XSD 架构从 XML 文件加载到 ADO.NET DataSet 中。为了节省设备上的数据空间,`DataSet` 使用 ICSharpCode 的 SharpZipLib 进行压缩,并在加载时解压缩。XML 压缩效果很好,这项技术使部署 XML 数据库的存储密集度大大降低。由于我不需要 SharpZipLib 的所有功能,因此我创建了一个“精简”版本,只包含流处理代码。如果您愿意,可以从项目中删除该组件,转而使用完整的 SharpZipLib(例如,如果它安装在您的全局程序集缓存中)。

(来自 `LaunchForm`)
// load the resort dataset with the data files
// uncompress the stream on the fly
InflaterInputStream zDataStream = new InflaterInputStream(
    new FileStream(FILENAME_DATA, FileMode.Open));
XmlTextReader dataReader = new XmlTextReader(zDataStream);
this.resort.DataSource.ReadXml(dataReader);
zDataStream.Close();
dataReader.Close();

加载后,我们通过一系列自定义数据对象来包装 DataSet,从而对其进行分解。这些对象中的每一个都公开了 DataSet 的架构,同时使数据关系明确化。此外,每个对象的构造函数都经过记忆化处理,以便单个包装对象表示每个数据记录。这可以防止数据记录被多次包装,这会浪费内存并使对象比较变得更加复杂。

(来自 `RegionRecord`)
static public RegionRecord Maker(DataRow row) {
    if(!(RegionRecord.memo.ContainsKey(row))) {
        RegionRecord newRecord = new RegionRecord(row);
        RegionRecord.memo[row] = newRecord;
    } 
    return (RegionRecord)RegionRecord.memo[row];
}

用户界面广泛使用了自定义控件(尽管还有很多地方可以进一步有效地利用它们)。由于 Visual Studio.NET 对为 Compact Framework 构建自定义控件的支持不多,我不得不手动构建它们,所以它们有些粗糙。不过,一个意想不到的附带好处是,我能够比使用控件设计器更精确地管理子控件的实例化。这导致更快的表单加载时间和稍小的内存占用。

一段有趣的代码利用了这些自定义控件。在 Finder 窗体中,我希望能够列出数据层中定义的 `PlaceRecord` 类的子类型,但我不想将“视图”类与“模型”和“控制器”类联系得太紧密。视图必须了解子类型的结构,但将这些子类型硬编码到 Finder 本身会使它们耦合得太紧密。所以我使用了一个“注册表”来管理这些关系。

`PlaceRecord` 的每个子类型都与视图的子类型相关联(可以是 `DetailView` 或 `FindView`)。基类维护这些关系的静态注册表。每次实例化子类时,它都会将自身注册为其关联的 `PlaceRecord` 类的处理程序。然后,客户端应用程序可以查询基类注册表以获取正确的视图对象来处理任何特定的 `PlaceRecord`。`PlaceForm` 使用此方法选择正确的 `DetailView` 来显示当前选定记录的内容。

(来自 `DetailView`)
private static Hashtable mapping;
public static Hashtable Labels {
    get {
        Hashtable result = new Hashtable();
        foreach(Type targetType in mapping.Keys) {
            result[targetType] = 
        ((DetailView)mapping[targetType]).TargetLabel;
        }
        return result;
    }
}
public static void Add(DetailView view) {
    DetailView.mapping[view.TargetType] = view;
}
public static DetailView Lookup(PlaceRecord p) {
    return ((DetailView)(DetailView.mapping[p.GetType()]));
}
(来自 `PlaceForm`)
// request the appropriate view panel based on the type of place
DetailView viewPanel = DetailView.Lookup(this.place);
// set the local data for the view panel and bring it into view
viewPanel.Place = this.place;
viewPanel.BringToFront();

查找器使用 `FindView` 类的 `Children` 属性来获取其子类的一个“每个一个”数组。此数组用作 ComboBox 的 DataSource。当选择一个项目时,它会简单地被带到显示堆栈的最前面。

(来自 `FinderForm`)
// get "one of each" of the specialized find panels for each place type...
foreach(FindView finder in FindView.Children) {
    // ...and attach each one to the "question" tab
    finder.Parent = this.questionTab;
}
. . .
// populate the placeType combo box with the set of finder views
this.placeType.DataSource = FindView.Children;
. . .
// request the appropriate view panel based on the type of place
this.viewPanel = (FindView)this.placeType.SelectedItem;
// set the local data for the view panel and bring it into view
this.viewPanel.BringToFront();

使用注册表技术允许客户端应用程序“保持距离”地操作数据记录及其视图,而无需直接指定它们。这使得在不修改客户端应用程序的情况下添加新类型或对记录和视图类型进行其他更改变得更加容易。最终,我希望将这些机制连接起来,以支持从 Web 服务检索 UI 和架构,从而实现应用程序的动态更新以支持新数据库。

波涛汹涌

这是我的第一个 Compact Framework 应用程序,所以我在开发过程中遇到了一些棘手的问题。其中最明显的一个是 CF 缺乏“浮动窗口”支持。查找器表单是伪造的,它的标题栏实际上只是表单本身的一部分。带有“真实”标题栏的浮动窗口无法通过 CF 直接访问(至少我找不到任何方法...如果有人知道如何实现,请告诉我!)。

我还发现了一个似乎是(至少)Compact Framework 中的一个 bug。我还没有深入追查以确定其范围,但似乎在其他程序集中定义的类的静态成员(带初始化器)在使用之前不会自动初始化。在我的例子中,当从 `Mobile` 程序集调用时,`Data` 子程序集的静态成员没有被初始化。我不得不创建一个静态构造函数并在那里手动初始化它们。我稍后将对此进行进一步调查。

继续

一些未来的增强

  • 完全填充数据库!
  • 支持从网络服务检索数据库(及其UI)
  • 向 `FindView` 添加更多检索条件
  • 改进地图绘制和地图数据处理

你呢?你会做出哪些改变?

历史

  • 2004 年 5 月 28 日 - 更新了源代码
© . All rights reserved.