Introduction
I love Disneyland. I've been there quite a bit, and I still enjoy going back. I've thought many times that it would be handy to have a portable application that would help guide people through "the Disneyland experience", but I never sat down to write it. Now, the .NET Compact Framework competition has given me the incentive I needed. Wanting to learn how to use the Compact Framework didn't hurt, either.
Resort Companion is a mobile data-access application that runs on the PocketPC (it will run on the desktop as well, but that's no fun). It's designed to give the user easy access to a database describing a typical resort (in this case, the Disneyland Resort in Anaheim, California). The database itself is easily editable and replaceable, and the user interface is modular, to allow for changes to the database schema.
As a Palm developer, I designed this application keeping in mind the Palm Computing guidelines for mobile applications. These guidelines emphasize speed of use and compact display layout over just about everything else, which I think makes sense for mobile devices of all kinds. The Palm platform has a variety of controls designed to be as compact as possible, while the PocketPC has controls that are mostly designed to mirror desktop controls, so some compromises in the design were required.
Using the Resort Companion
To use the application, just start it up from the File Explorer on the PocketPC. It will display a splash page while it loads and initializes the database, and when it's done it will activate the "Find a starting place..." button. Pressing this button will open the Finder, allowing you to locate a place in the resort. Use the "Question" tab to form a query, and then select the "Answer" tab to see the results. Select a place and click the "OK" button to continue.
The place you selected will be displayed on the screen. The upper portion of the display shows information about the place, while the lower portion shows other places that are nearby. Selecting one of these places will make it the current place. At the very bottom of the display is a button bar with two buttons on it: the magnifying glass will bring up the Finder form again, while the star will mark the currently selected place as a "favorite". Favorite places are added to the pop-up menu attached to the star. Note that your favorites are only stored in RAM, so resetting your PocketPC (or even stopping the Resort Companion app) will clear the list.
Some caveats: the database is far from finished, and in some cases, not even very accurate (in other words, don't rely on this database exclusively...just yet!). In particular, the listing of nearby places is sometimes...well, "generous" in its estimation of "nearby". With time, I hope to have the database fully fleshed out. I've tested this app on the emulator and an HP Jornada, but I didn't have time to test every part of it, so it still could have a nasty crashing bug somewhere.
The Inside Scoop
I've packaged the entire solution into the .ZIP file linked above. It's ready to open in Visual Studio.NET, so you can examine the source, or add your own databases. The database schema is simple enough that you shouldn't have any trouble making updates. If you do update the database, be sure to run the compress.cmd
script in the Mobile
project. That script uses the DataCompressor
utility to compress the XML and XSD files in the Data
project, and move them to the Mobile
project for deployment. I made a change to my Visual Studio.NET configuration so that double-clicking the .CMD file launches it instead of opening it in the editor (use the "Open with..." menu to set this up yourself). It saves a lot of time.
Behind the scenes, the code is mostly straightforward. The database is loaded into an ADO.NET DataSet from an XML file, using an XSD schema. To save data space on the device, the DataSet
is compressed with ICSharpCode's SharpZipLib, and uncompressed during loading. XML compresses very well, and this technique makes deploying XML databases much less storage-intensive. Since I didn't need the all of SharpZipLib's functionality, I created a "streamlined" version that only includes the stream-handling code. If you wish, you can remove that component from the project and use the full SharpZipLib instead (if it's installed in your Global Assembly Cache, for instance).
(from
LaunchForm
):
InflaterInputStream zDataStream = new InflaterInputStream(
new FileStream(FILENAME_DATA, FileMode.Open));
XmlTextReader dataReader = new XmlTextReader(zDataStream);
this.resort.DataSource.ReadXml(dataReader);
zDataStream.Close();
dataReader.Close();
Once loaded, we decompose the DataSet by wrapping it with a series of custom data objects. Each of these objects exposes the schema of the DataSet while making the data relations explicit. Further, the constructors for each object are memoized, so that a single wrapper object represents each data record. This prevents a data record from getting wrapped multiple times, which would waste memory as well as make object comparisons more complicated.
(from
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];
}
The user interface makes extensive use of custom controls (though there are many places left where they could beneficially be employed further). Since Visual Studio.NET doesn't provide much support for building custom controls for the compact framework, I had to build them by hand, so they're a little rough around the edges. An unexpected side benefit, though, was that I was able to manage sub-control instantiation more precisely than if I'd used the controls designer. That led to faster form load times and a slightly smaller memory footprint.
An interesting bit of code makes use of these custom controls. In the Finder form, I wanted to be able to list the sub-types of the PlaceRecord
class that were defined in the data layer, but I didn't want to tie the "view" classes too closely to the "model" and "controller" classes. The views have to have knowledge of the structure of the sub-types, but hard-coding those subtypes into the Finder itself would have coupled them too closely. So I use a "registry" to manage the relationships.
Each sub-type of PlaceRecord
is associated with a sub-type of a view (either a DetailView
or a FindView
). The base class maintains a static registry of these relationships. Each time a subclass is instantiated, it registers itself as the handler for its associated PlaceRecord
class. A client app can then query the base class registry to fetch the correct view object to handle any particular PlaceRecord
. The PlaceForm
uses this method to choose the correct DetailView
to show the contents of the currently selected record.
(from
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()]));
}
(from
PlaceForm
):
DetailView viewPanel = DetailView.Lookup(this.place);
viewPanel.Place = this.place;
viewPanel.BringToFront();
The Finder uses the Children
property of the FindView
class to get a "one of each" array of its subclasses. This array is used as a DataSource for a ComboBox. When an item is selected, it's simply brought to the front of the display stack.
(from
FinderForm
):
foreach(FindView finder in FindView.Children) {
finder.Parent = this.questionTab;
}
. . .
this.placeType.DataSource = FindView.Children;
. . .
this.viewPanel = (FindView)this.placeType.SelectedItem;
this.viewPanel.BringToFront();
Using the registry technique allows the client app to manipulate the data records and their views "at arm's length", without specifying them directly. This makes it much easier to add new types or make other changes to the record-and-view types without modifying the client app. Eventually I hope to hook up the machinery to support retrieving the UI along with the schema from a web service, to enable dynamic updates to the application to support new databases.
Rough Seas
This was my first Compact Framework application, so I encountered a few rough spots along the way. One of the most glaring was the lack of "floating window" support in the CF. The finder form is faked, in that its title bar is actually just part of the form itself. Floating windows with "real" title bars are not directly accessible through the CF (at least, I couldn't find any…if anyone knows how to make them work, let me know!).
I also discovered what seems to be a bug in (at least) the Compact Framework. I haven't chased it down to determine its scope, but it seems that static members (with initializers) of classes defined in other assemblies don't get automatically initialized before they're used. In my case, the static members of the Data
sub-assembly weren't being initialized when they were called from the Mobile
assembly. I had to create a static constructor and initialize them by hand there. I'll be investigating this further later on.
Moving On
Some future enhancements:
- Fully populate the database!
- Support retrieving the database (and its UI) from a web service
- Add more retrieval criteria to the
FindView
s
- Improve the map drawing and map data handling
What about you? What changes would you make?
History
- 28 May 2004 - updated source code