Problem Statement
Many Windows Mobile applications use an Options or a Settings dialog box to manage application level settings. The dialog box usually consists of a tab control with one or more regular controls on each tab such as labels, check boxes, radio buttons, dropdown lists, updowns, text boxes, etc. The following screenshots show some examples.
When designing such a dialog box, we need to consider carefully what kind of controls to use, how many controls on each tab, and how to layout the controls so that they look good and operatable in different screen resolutions, sizes and orientations (portrait and landscape). The layout issue becomes even trickier if we have to pop up a soft keyboard to collect user input. One way to deal with the layout issue is to design for the lowest common denominator - the lowest resolution and the smallest screen size. But often times, we end up with using more tabs and fewer controls on each tab, a waste of valuable screen real estate if a user happens to have a device with higher resolution and/or bigger screen size. The following screenshots from a 480x800 WVGA device demonstrate the point.
Application settings managed by an Options dialog box need to be persisted to a data store. In full .NET Framework, this is usually done with a Settings file, which corresponds to the <userSettings>
section in the app.config file. At design time, the Settings file is used to generate a Settings
class (derived from System.Configuration.ApplicationSettingsBase
) which provides strongly-typed accessors for each setting and the ability to save the settings to a user-specific file (user.config). Unfortunately, the Settings file mechanism does not exist in .NET Compact Framework. We have to either store the settings in the registry, which is prone to deployment and security issues, or devise our own way to store the settings, as did in AppSettings Implementation for Compact Framework by pbrooks and .NET Compact Framework ConfigurationManager by Shawn Miller. Both employed an XML file similar to the <appSettings>
section of the app.config file. They treated application settings as "flat" key-value pairs with no structural support on how the settings might be organized.
TreeView Based Options Component
In this article, I propose using a TreeView
control coupled with an XML file to manage application settings. The following screenshots show the component in action.
From the UI point of view, the TreeView
based Options component has these advantages:
- No more layout headaches. It works well no matter how many options we have, and what screen resolution, size and orientation we use.
- Options can be organized in groups, subgroups, etc.
- Intuitive and joystick friendly (so a user can operate it with a single hand).
- Move the joystick up and down to move the selection cursor (gray color) up and down.
- Move the joystick to the right to expand a node.
- Move the joystick to the left to collapse a node.
- When current node is a value node, press the joystick down to select it.
- When current node is a collapsed group or option node, press the joystick down to expand it.
- When current node is a expanded group or option node, press the joystick down to collapse it.
- Selected values are highlighted (in yellow).
- Efficient use of screen area. No need to flip through different tabs or screens just to find the right option.
- Consistent multiple-choice style operation for all options. Users don't need to learn different and sometimes confusing UIs.
The only requirement for using the component is that each option or setting must have a value selected from a set of discrete values, or like a multiple-choice. This isn't too restricted. UI controls such as radio buttons, dropdown list, updowns and check boxes are, in fact, multiple-choice type of controls. A text box, which is usually used for a continuous value option such as distance, weight, cache size, etc., should be avoided at any cost because it is such a hassle to input to a text box in a mobile device. So when dealing with a continuous value option, we should think of a way to discretize it instead. Can it be described reasonably well by a set of representative values in the particular context of our mobile application? Is it possible to break it down into several ranges, for example? An added benefit of discretization of all our options: we don't need input validation anymore, as all the values are carefully selected and valid.
The data used to populate the TreeView
is stored in an XML file. The particular XML file has only three node types except the root node: group
, option
and value
. An example of the XML file is shown below:
="1.0"="utf-8"
<options>
<group name="General">
<option name="TimeZone" displayName="Time zone">
<value name="EST" selected="true" />
<value name="CST" selected="false" />
<value name="MST" selected="false" />
<value name="PST" selected="false" />
</option>
<option name="UpdateInterval" displayName="Update interval">
<value name="10" displayName="10 sec" selected="false" />
<value name="30" displayName="30 sec" selected="true" />
<value name="60" displayName="1 min" selected="false" />
<value name="300" displayName="5 min" selected="false" />
<value name="600" displayName="10 min" selected="false" />
</option>
<option name="CacheSize" displayName="Cache size">
<value name="32" displayName="32 MB" selected="true" />
<value name="64" displayName="64 MB" selected="false" />
<value name="128" displayName="128 MB" selected="false" />
</option>
<option name="CheckInterval" displayName="Check for app update">
<value name="0" displayName="Every time app starts" selected="true" />
<value name="1" displayName="Every day" selected="false" />
<value name="7" displayName="Every week" selected="false" />
<value name="30" displayName="Every month" selected="false" />
<value name="365" displayName="Every year" selected="false" />
</option>
</group>
<group name="Appearance">
<option name="Skin">
<value name="Classic" selected="true" />
<value name="IceFusion" displayName="Ice Fusion" selected="false" />
<value name="Monochrome" selected="false" />
</option>
<option name="ShowToolbar" displayName="Show toolbar">
<value name="true" selected="true" />
<value name="false" selected="false" />
</option>
<option name="ShowStatusBar" displayName="Show status bar">
<value name="true" selected="true" />
<value name="false" selected="false" />
</option>
</group>
<group name="SecurityPrivacy" displayName="Security & Privacy">
<option name="EnablePassword" displayName="Enable password protection">
<value name="true" selected="true" />
<value name="false" selected="false" />
</option>
<group name="SharedContents" displayName="Shared contents">
<option name="ContactInfo" displayName="Contact info">
<value name="true" selected="false" />
<value name="false" selected="true" />
</option>
<option name="Photos">
<value name="true" selected="true" />
<value name="false" selected="false" />
</option>
<option name="Posts">
<value name="true" selected="true" />
<value name="false" selected="false" />
</option>
</group>
</group>
</options>
There are some rules about this XML:
- Each node type has a
name
and a displayName
attributes. name
, which is required, is used or referenced in our program. displayName
, which is optional, is used in the TreeView
for presentation. If a displayName
is missing, the corresponding name
is used for presentation. group
can contain zero or more option
nodes and group
nodes (or subgroups). We can have as many levels of embedded group
nodes as we want. option
must only contain one or more value
nodes. - Only one of the multiple
value
nodes of an option
can have a selected
attribute of "true
", indicating that it is the current value
of the option
.
From a programming point of view, using such an XML as data store for our Options component has these advantages:
- Both XML and
TreeView
are based on tree structure. One for storage and one for presentation. This perfect match makes programming easy. - Organizing options in groups and subgroups is inherently supported.
- It makes it easy to manage both the selected values and all the available values. With the traditional Settings file, only selected values are persisted, whileas the available values are either in a separate resource file or hard-coded in the program.
- To a certain extent, we can change the options without having to recompile our program. It is also possible to update the XML remotely.
- It makes localization easy. Just translate all the
displayName
attributes to different languages.
Implementation
For maximum flexibility, I didn't make the Options "component" a user control or a class library. Just a simple class that you can copy and paste to your project.
public class OptionsManager
{
private const string optionValueSeparator = ": ";
private static XmlDocument xdoc = null;
private static string theFile = null;
private static Color selectedValueBackColor = Color.Yellow;
private static bool isChanged = false;
static OptionsManager()
{
theFile = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().
GetName().CodeBase) + @"\Options.xml";
xdoc = new XmlDocument();
if (File.Exists(theFile))
xdoc.Load(theFile);
else
{
LoadDefault();
Save();
}
}
public static void LoadDefault()
{
using (StreamReader sr =new StreamReader(Assembly.GetExecutingAssembly().
GetManifestResourceStream("TreeviewOptions.Options.Options.xml")))
{
xdoc.LoadXml(sr.ReadToEnd());
sr.Close();
isChanged = true;
}
}
public static void Save()
{
if (isChanged)
{
xdoc.Save(theFile);
isChanged = false;
}
}
public static void Cancel()
{
if (isChanged)
{
xdoc.Load(theFile);
isChanged = false;
}
}
public static void LoadToTreeView(TreeView tvw)
{
tvw.Nodes.Clear();
XmlNode root = xdoc.DocumentElement;
DoLoading(tvw, root);
}
private static void DoLoading(object treeviewNode, XmlNode xmlNode)
{
XmlNodeList xmlSubnodes = xmlNode.ChildNodes;
foreach (XmlNode xsn in xmlSubnodes)
{
NodeType nodeType = GetXmlNodeType(xsn);
if (nodeType == NodeType.Group)
{
string groupDisplayName = GetXmlNodeDisplayName(xsn);
TreeNode tn = null;
if (treeviewNode is TreeView)
{
tn = ((TreeView)treeviewNode).Nodes.Add(groupDisplayName);
tn.Tag = string.Format("/group[@name='{0}']",
((XmlElement)xsn).GetAttribute("name"));
}
else
{
tn = ((TreeNode)treeviewNode).Nodes.Add(groupDisplayName);
tn.Tag = tn.Parent.Tag + string.Format("/group[@name='{0}']",
((XmlElement)xsn).GetAttribute("name"));
}
DoLoading(tn, xsn);
}
else if (nodeType == NodeType.Option)
{
string optionDisplayName = GetXmlNodeDisplayName(xsn);
TreeNode tn = null;
if (treeviewNode is TreeView)
{
tn = ((TreeView)treeviewNode).Nodes.Add(optionDisplayName);
tn.Tag = string.Format("/option[@name='{0}']",
((XmlElement)xsn).GetAttribute("name"));
}
else
{
tn = ((TreeNode)treeviewNode).Nodes.Add(optionDisplayName);
tn.Tag = tn.Parent.Tag + string.Format("/option[@name='{0}']",
((XmlElement)xsn).GetAttribute("name"));
}
XmlNodeList values = xsn.ChildNodes;
string selectedValueName = null;
foreach (XmlNode v in values)
{
string valueDisplayName = GetXmlNodeDisplayName(v);
TreeNode vtn = tn.Nodes.Add(valueDisplayName);
vtn.Tag = tn.Tag + string.Format("/value[@name='{0}']",
((XmlElement)v).GetAttribute("name"));
if (((XmlElement)v).GetAttribute("selected") == "true")
{
vtn.BackColor = selectedValueBackColor;
selectedValueName = valueDisplayName;
}
}
tn.Text += optionValueSeparator + selectedValueName;
}
}
}
private static string GetXmlNodeDisplayName(XmlNode node)
{
string dName = ((XmlElement)node).GetAttribute("displayName");
if (string.IsNullOrEmpty(dName))
dName = ((XmlElement)node).GetAttribute("name");
return dName;
}
public static void ChangeValue(TreeNode tn)
{
if (IsValueNode(tn))
{
string parentDisplayName = Regex.Split(tn.Parent.Text, optionValueSeparator)[0];
tn.Parent.Text = parentDisplayName + optionValueSeparator + tn.Text;
tn.BackColor = selectedValueBackColor;
TreeNodeCollection valueNodes = tn.Parent.Nodes;
foreach (TreeNode vn in valueNodes)
{
vn.BackColor = (vn == tn) ? selectedValueBackColor : Color.Empty;
}
XmlNode option = xdoc.DocumentElement.
SelectSingleNode("/options" + tn.Tag).ParentNode;
XmlNodeList values = option.ChildNodes;
foreach (XmlNode v in values)
((XmlElement)v).SetAttribute("selected", "false");
((XmlElement)xdoc.DocumentElement.SelectSingleNode("/options" + tn.Tag)).
SetAttribute("selected", "true");
isChanged = true;
}
}
public static bool IsValueNode(TreeNode tn)
{
string path = (string)tn.Tag;
string[] parts = path.Split("/".ToCharArray());
return (parts[parts.Length - 1].StartsWith("value"));
}
public static string GetOptionValue(string optionXPath)
{
if (!optionXPath.StartsWith("/options"))
optionXPath = "/options" + optionXPath;
XmlNode option = xdoc.DocumentElement.SelectSingleNode(optionXPath);
if (option != null)
{
XmlNodeList values = option.ChildNodes;
foreach (XmlNode v in values)
{
if (((XmlElement)v).GetAttribute("selected") == "true")
return ((XmlElement)v).GetAttribute("name");
}
}
return null;
}
private static NodeType GetXmlNodeType(XmlNode node)
{
switch (node.Name)
{
case "options":
return NodeType.Root;
case "group":
return NodeType.Group;
case "option":
return NodeType.Option;
case "value":
return NodeType.Value;
default:
throw new ApplicationException("Unknow Xml node type.");
}
}
private enum NodeType
{
Root,
Group,
Option,
Value
}
}
Some highlights about the code:
First of all, we need to make the XML file, Options.xml, an embedded resource by setting its Building Action
property to Embedded Resource in Visual Studio. As indicated in the static
constructor of our OptionsManager
class, at the very first time the application starts, the XML file is loaded to memory from the embedded resource via the LoadDefault
method and saved to the location where our application executable resides. In subsequent times, since the XML file already exists in our application root, we simply load it from there.
The LoadToTreeView
method loads the XML document to a specified TreeView
control. Since we can have many levels of embedded group
nodes, we need to do the loading recursively, as indicated in the private
method DoLoading
. For a group
node, we show its displayName
in the TreeView
. For an option
node, we show its displayName
and its currently selected value's displayName
(separated by ": "). For a value
node, we show its displayName
and if it is currently selected, we also set its background color to selectedValueBackColor
. For each tree node, we construct an XPath
to the corresponding XML node and store it to the tree node's Tag
property. Later on, we can easily match a tree node to an XML node with this XPath
. As an example, the following table shows what the XPath
looks like for a particular tree node:
Tree Node | XPath |
General/Time zone option | /group[@name='General']/option[@name='TimeZone'] |
General/Time zone/PST value | /group[@name='General']/option[@name='TimeZone']/value[@name='PST'] |
The ChangeValue
method is called whenever we want to change the selected value of an option. This is usually triggered by the event of pressing down the joystick. We need to do two things in this method. First, in the TreeView
control, we need to update the display of the corresponding option
node and change the highlighter to the value
node that becomes currently selected. Second, in the underlying XML document, we need to update the selected
attribute of the value
nodes of corresponding option
to reflect which value is currently selected.
The GetOptionValue
method returns the selected value (the value's name
attribute to be exact) of an option given the option's XPath
. Since we know the details of our Options.xml file, it's up to us to cast the retrieved string
value to a proper data type.
Using the OptionsManager Class
Here are the steps to use our OptionsManager
class in a Windows Mobile project:
- Create the Options.xml file and make it an embedded resource.
Tips: Use the sample Options.xml file as a template. Make sure the namespace reference to the Options.xml file in the LoadDefault
method is correct. - Add a Windows Form,
OptionsForm
, to the project. This will be our Options dialog box. - Add a
TreeView
control and the following four menu items or buttons to the OptionsForm
:
- Done - Save changes to the options and close the Options dialog box
- Cancel - Cancel changes to the options and close the Options dialog box
- Default - Restore the options to their default values (as specified in the Options.xml stored in the embedded resource)
- Change Value - Change corresponding option's value to the value indicated by the
TreeView
selection cursor
- Wire up the
Load
and Closed
event handlers of the OptionsForm
like so:
private void OptionsForm_Load(object sender, EventArgs e)
{
LoadTreeView();
}
private void OptionsForm_Closed(object sender, EventArgs e)
{
if (this.DialogResult == DialogResult.OK)
OptionsManager.Save();
else
OptionsManager.Cancel();
}
private void LoadTreeView()
{
OptionsManager.LoadToTreeView(treeView1);
TreeNodeCollection nodes = treeView1.Nodes;
foreach (TreeNode n in nodes)
n.Expand();
}
- Wire up the
KeyPress
and AfterSelect
event handlers of the TreeView
control like so:
private void treeView1_KeyPress(object sender, KeyPressEventArgs e)
{
if (OptionsManager.IsValueNode(treeView1.SelectedNode))
{
OptionsManager.ChangeValue(treeView1.SelectedNode);
}
else
{
if (treeView1.SelectedNode.IsExpanded)
treeView1.SelectedNode.Collapse();
else
treeView1.SelectedNode.Expand();
}
}
private void treeView1_AfterSelect(object sender, TreeViewEventArgs e)
{
menuMenuChangeValue.Enabled = OptionsManager.IsValueNode(treeView1.SelectedNode);
}
- Wire up the
Click
event handlers of the four menu items or buttons like so:
private void menuDone_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.OK;
}
private void menuMenuCancel_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
}
private void menuMenuDefault_Click(object sender, EventArgs e)
{
OptionsManager.LoadDefault();
LoadTreeView();
}
private void menuMenuChangeValue_Click(object sender, EventArgs e)
{
OptionsManager.ChangeValue(treeView1.SelectedNode);
}
- Call the
GetOptionValue
method for any option value anywhere in the project. E.g.:
int cacheSize = int.Parse(OptionsManager.GetOptionValue(
"/group[@name='General']/option[@name='CacheSize']"));
bool sharePhotos = bool.Parse(OptionsManager.GetOptionValue(
"/group[@name='SecurityPrivacy']/group[@name='SharedContents']/option[@name='Photos']"));
Further Improvements
The Options
component can be further improved in the following areas:
- Validate the Options.xml against an XSD schema.
- Add icons to tree nodes through an
ImageList
control. - Allow only one of the same level group or option nodes to expand at a time. This mode is desirable for small screen sizes.
Conclusions
A TreeView
based Options component for Windows Mobile is proposed and implemented. It has several advantages over the traditional way of designing an Options dialog box. These includes elimination of layout issues, efficient use of screen area, intuitive and consistent UI, and sound structural support on persisting and organizing application settings.
You can download the source code here.