Pan Fried Tofu with Sugar Peas

by Gong Liu September 26, 2009 06:30

A TreeView Based Options Component for Windows Mobile

by Gong Liu September 12, 2009 18:08

This article is also published on The Code Project. Please vote it there! 

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 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 restrict. 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 any more, 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: 

<?xml version="1.0" encoding="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 &amp; 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 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; //is there any difference between xdoc and theFile

    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();         //create theFile
        }
    }

    //load default options from embeded resource
    public static void LoadDefault()
    {
        using (StreamReader sr =new StreamReader(Assembly.GetExecutingAssembly().
            GetManifestResourceStream("TreeviewOptions.Options.Options.xml")))
        {
            xdoc.LoadXml(sr.ReadToEnd());
            sr.Close();
            isChanged = true;
        }
    }

    //Persist xdoc to theFile
    public static void Save()
    {
        if (isChanged)
        {
            xdoc.Save(theFile);
            isChanged = false;
        }
    }

    //Cancel changes to xdoc (DOM) by reloading it from theFile 
    public static void Cancel()
    {
        if (isChanged)
        {
            xdoc.Load(theFile);
            isChanged = false;
        }
    } 

    //Load xdoc to the specified TreeView
    public static void LoadToTreeView(TreeView tvw)
    {
        tvw.Nodes.Clear();
        XmlNode root = xdoc.DocumentElement;
        DoLoading(tvw, root);
    }

    //treeviewNode can be a TreeView or a TreeNode object
    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;
    }

    //if tn is a value node, update tn.Parent display and corresponding xml value node selection
    public static void ChangeValue(TreeNode tn)
    {
        if (IsValueNode(tn))
        {
            //update tn.Parent display
            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;
            }

            //update xml value node selection
            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;
        }
    }

    //If the XPath stored in tn.Tag ends with a value tag, then tn is a value treenode
    public static bool IsValueNode(TreeNode tn)
    {
        string path = (string)tn.Tag;
        string[] parts = path.Split("/".ToCharArray());
        return (parts[parts.Length - 1].StartsWith("value"));
    }

    //optionXPath is the XPath to the option whose value (name attribute) is to be retrieved.
    //The root /options can be omitted. 
    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:

1. 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.

2. Add a Windows Form, OptionsForm, to the project. This will be our Options dialog box.

3. 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 

4. 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);

    //expand the first level
    TreeNodeCollection nodes = treeView1.Nodes;
    foreach (TreeNode n in nodes)
        n.Expand();

5. 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)
{
    //make the Change Value menu item available only when the current tree node is a value node
    menuMenuChangeValue.Enabled = OptionsManager.IsValueNode(treeView1.SelectedNode);

6. 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);
}

7. 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 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 below:

TreeviewOptions.zip (49.31 kb)

About

A seasoned computer professional. A tofu culture evangelist...
more >>

Tag Cloud

Calendar

<<  April 2017  >>
MoTuWeThFrSaSu
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

View posts in large calendar
Copyright © 2008-2011 Gong Liu. All rights reserved. | credits | contact me
The content on this site represents my own personal opinions, and does not reflect those of my employer in any way.