Storing Application Data and Settings in Windows Mobile-based Smartphone Applications using .NET Compact Framework

 

Andy Sjostrom
businessanyplace.net

February 2004

Applies to:
   Windows Mobile™-based Smartphones
   Windows Mobile 2003 Second Edition software for Smartphones
   Microsoft® Visual Studio® .NET 2003
   Microsoft .NET Compact Framework version 1.0

Summary: Learn how to develop applications that store application data and application settings in local XML files in non-volatile memory. The sample code provides two reusable classes: the LocalStorage class which can be used to manage locally stored application data and settings, and the EditModeHandler class which can be used to manage different device edit modes. (18 printed pages)

Download Storing Application Data and Settings.msi from the Microsoft Download Center.

Contents

Introduction
ISBN Anyplace Feature Walkthrough
ISBN Anyplace Code Walkthrough
Conclusion

Introduction

The purpose of this article is to show how to work with the volatile Smartphone object store when you need to store application data and settings. The sample code supplied with this article contains a number of enhancements to the "ISBN Anyplace" sample found in the white paper Develop for Windows Mobile-based Smartphones Using the .NET Compact Framework. We recommended you read that white paper before this article.

"ISBN Anyplace" is a sample application that lets the user retrieve the Barnes & Noble price of any book given its unique ISBN number using an external XML Web Service. The application stores data that it receives in the external flash memory file system that all Windows Mobile-based Smartphones have. This file system works in a similar way to a Compact Flash or SD card, albeit it is not removable.

The sample code illustrates the following key areas:

  • How to store both application data and application settings using a common LocalStorage class
  • How to retrieve a correct path to the non-volatile flash memory using the API SHGetSpecialFolderPath along with the constant CSIDL_APPDATA
  • How to manage navigation between forms
  • How to programmatically set the Smartphone user input mechanism to numerical input
  • How to limit the amount of data stored using a user defined application setting
  • How to work with embedded resources, such as XML Schemas

ISBN Anyplace Feature Walkthrough

The ISBN Anyplace sample was created with Microsoft Visual Studio .NET 2003, C#, and .NET Compact Framework.

The sample application is made up of three forms. The main form contains a full screen listview with all books that have been retrieved from the XML Web Service as shown in Figure 1.

Figure 1. Main form with four books

The main form implements a menu bar with a New command, which takes the user to a form which accepts input of a new ISBN number, and a Menu command that allows the user to go Settings and manage the application data of books and prices, as shown in Figure 2.

Figure 2. The main form's menu command

The user uses the following form to enter a new ISBN number as shown in Figure 3.

Figure 3. The user enters the ISBN number.

Finally, the following form is used to manipulate application settings, also stored in non-volatile memory. In this sample, the user can set maximum number of books to store in memory, as shown in Figure 4.

Figure 4. The user can manipulate the application settings.

Let's move on and take a look at the code!

ISBN Anyplace Code Walkthrough

The code walkthrough highlights key programmatic aspects of ISBN Anyplace:

  • A common LocalStorage class
  • Using the API SHGetSpecialFolderPath along with the constant CSIDL_APPDATA
  • Navigation between forms
  • Setting the Smartphone user input mechanism to numerical input
  • Limiting the amount of data stored using a user defined application setting
  • Using embedded resources

Make sure you download the full source code ISBNAnyplace.exe at the beginning of this article because this article does not contain illustrations to all lines of code.

Code Disposition

The project consists of seven main components, which are described in Table 1.

Table 1. The seven main components and their descriptions

Component Description
MainForm.cs Class which implements the main form
NewBook.cs Class which implements the New Book form
Settings.cs Class which implements the Settings form
LocalStorage.cs Class that is used to create, read, update and delete application data and application settings
BarnesNobleWebService.cs Class that wraps around the external XML Web Service
ApplicationData.xsd XML Schema which defines the structure of application data and application settings stored
net.xmethods.www Web Reference to the external XML Web Service
EditModeHandler.cs Class that is used to manage input modes

Figure 5 is a screen shot of the Solution Explorer.

Figure 5. Solution Explorer

Application Data XML Schema

ISBN Anyplace utilizes ADO DataSets to persist data in XML files. The ApplicationData.xsd is the XML Schema Definition that defines how the application data and application settings are stored. The schema defines the data structure to be constructed by a Key/Value pair. Note that the column "Key" is defined as the Primary Key. As in relational databases where tables can have a Primary Key which uniquely identifies a row, so can the XML Schema define one or more combined columns to be the Primary Key. The ADO.NET will then be able to uphold referential integrity and throw an exception in case a duplicate row is about to be inserted. Figure 6 shows an XML Schema for data and settings.

Figure 6. XML Schema for data and settings

Below you can see the XML Schema as defined in code. The schema id is set to "DataSet" and the entire schema then adheres to the ADO.NET DataSet XML standard so that the XML Schema can be used to read, manage and write ADO.NET DataSets. The Key and Value elements are defined as xs:string and the Key column is defined as DataSetPrimaryKey.

<?xml version="1.0" standalone="yes" ?>
<xs:schema id="DataSet"  xmlns:xs="https://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
   <xs:element name="DataSet" msdata:IsDataSet="true">
      <xs:complexType>
         <xs:choice maxOccurs="unbounded">
            <xs:element name="ApplicationData">
               <xs:complexType>
                  <xs:sequence>
                     <xs:element name="Key" type="xs:string"></xs:element>
                     <xs:element name="Value" type="xs:string" minOccurs="0" />
                  </xs:sequence>
               </xs:complexType>
            </xs:element>
         </xs:choice>
      </xs:complexType>
      <xs:key name="DataSetPrimaryKey" msdata:PrimaryKey="true">
         <xs:selector xpath=".//ApplicationData" />
         <xs:field xpath="Key" />
      </xs:key>
   </xs:element>
</xs:schema>

An XML file, containing application settings, adhering to the above XML Schema can be seen below. Note that the element <ApplicationData> uses the <Key> and <Value> sub-elements and that the <Key> element has two different entries. Since <Key> is Primary Key, it is impossible to have two identical entries.

<?xml version="1.0" standalone="yes"?>
<DataSet>
  <ApplicationData>
    <Key>MaxNumberOfBooks</Key>
    <Value>10</Value>
  </ApplicationData>
  <ApplicationData>
    <Key>DefaultFontSize</Key>
    <Value>10</Value>
  </ApplicationData>
</DataSet>

LocalStorage Class

The LocalStorage class is used in ISBN Anyplace to manage application data and settings. Internally, the class declares two private strings and two private methods:

  • fileName – the name of the file containing the data. For example, ApplicationData.xml or ApplicationSettings.xml
  • path – the path to the file. The path is built in the class constructor using SHGetSpecialFolderPath and the CSIDL_APPDATA constant
  • getDataSet – method to retrieve DataSet from the local XML file. This method is used by methods available externally.
  • getSpecialFolderPath – method which wraps around the SHGetSpecialFolderPath API

Externally, the class publishes the following interfaces:

  • Initialize – initializes local XML files. For example, creating default application settings
  • AddNew – add new row to local XML file
  • GetAll – get all rows from local XML file
  • GetByKey – get one row from local XML file based on Key
  • UpdateByKey – update one row in local XML file based on Key
  • DeleteByKey – delete one row from local XML file based on Key
  • DeleteAll – deletes all rows in local XML file

The following code snippet shows the declarations of the private variables, the CSIDL enumerations, and the DllImport for the SHGetSpecialFolderPath API.

private string fileName;
private string path;
public enum CSIDL : int   
{
   CSIDL_DESKTOP = 0,
   CSIDL_PROGRAMS = 2,
   CSIDL_PERSONAL = 5,
   CSIDL_FAVORITES_GRYPHON = 6,
   CSIDL_STARTUP = 7,
   CSIDL_APPDATA = 26            // \Storage\Application Data
}

[DllImport("coredll.dll")]
private static extern bool SHGetSpecialFolderPath(int hwndOwner, string lpszPath, CSIDL nFolder, bool fCreate);

For more information about SHGetSpecialFolderPath, read File Operation in the Smartphone and the MSDN Library documentation. For all CSIDL constants, see the file shlobj.h installed with eMbedded Visual C++® 4.0. Here's a portion from that file:

#define CSIDL_DESKTOP                   0x0000    // <desktop>
#define CSIDL_INTERNET                  0x0001    // Internet Explorer (icon on desktop)
#define CSIDL_PROGRAMS                  0x0002    // Start Menu\Programs
#define CSIDL_CONTROLS                  0x0003    // My Computer\Control Panel
#define CSIDL_PRINTERS                  0x0004    // My Computer\Printers
#define CSIDL_PERSONAL                  0x0005    // My Documents
#define CSIDL_FAVORITES                 0x0006    // <user name>\Favorites
#define CSIDL_STARTUP                   0x0007    // Start Menu\Programs\Startup
#define CSIDL_RECENT                    0x0008    // <user name>\Recent
#define CSIDL_SENDTO                    0x0009    // <user name>\SendTo
#define CSIDL_BITBUCKET                 0x000a    // <desktop>\Recycle Bin
#define CSIDL_STARTMENU                 0x000b    // <user name>\Start Menu
#define CSIDL_MYDOCUMENTS               0x000c    // logical "My Documents" desktop icon
#define CSIDL_MYMUSIC                   0x000d    // "My Music" folder
#define CSIDL_MYVIDEO                   0x000e    // "My Videos" folder
#define CSIDL_DESKTOPDIRECTORY          0x0010    // <user name>\Desktop
#define CSIDL_DRIVES                    0x0011    // My Computer
#define CSIDL_NETWORK                   0x0012    // Network Neighborhood
#define CSIDL_NETHOOD                   0x0013    // <user name>\nethood
#define CSIDL_FONTS                     0x0014    // windows\fonts
#define CSIDL_TEMPLATES                 0x0015
#define CSIDL_COMMON_STARTMENU          0x0016    // All Users\Start Menu
#define CSIDL_COMMON_PROGRAMS           0X0017     // All Users\Programs
#define CSIDL_COMMON_STARTUP            0x0018    // All Users\Startup
#define CSIDL_COMMON_DESKTOPDIRECTORY   0x0019    // All Users\Desktop
#define CSIDL_APPDATA                   0x001a    // <user name>\Application Data
#define CSIDL_PRINTHOOD                 0x001b    // <user name>\PrintHood
#define CSIDL_LOCAL_APPDATA             0x001c    // <user name>\Local Settings\Applicaiton Data (non roaming)
#define CSIDL_ALTSTARTUP                0x001d    // non localized startup
#define CSIDL_COMMON_ALTSTARTUP         0x001e    // non localized common startup
#define CSIDL_COMMON_FAVORITES          0x001f
#define CSIDL_INTERNET_CACHE            0x0020
#define CSIDL_COOKIES                   0x0021
#define CSIDL_HISTORY                   0x0022
#define CSIDL_COMMON_APPDATA            0x0023    // All Users\Application Data
#define CSIDL_WINDOWS                   0x0024    // GetWindowsDirectory()
#define CSIDL_SYSTEM                    0x0025    // GetSystemDirectory()
#define CSIDL_PROGRAM_FILES             0x0026    // C:\Program Files
#define CSIDL_MYPICTURES                0x0027    // C:\Program Files\My Pictures
#define CSIDL_PROFILE                   0x0028    // USERPROFILE
#define CSIDL_SYSTEMX86                 0x0029    // x86 system directory on RISC
#define CSIDL_PROGRAM_FILESX86          0x002a    // x86 C:\Program Files on RISC
#define CSIDL_PROGRAM_FILES_COMMON      0x002b    // C:\Program Files\Common 
#define CSIDL_PROGRAM_FILES_COMMONX86   0x002c    // x86 Program Files\Common on RISC
#define CSIDL_COMMON_TEMPLATES          0x002d    // All Users\Templates
#define CSIDL_COMMON_DOCUMENTS          0x002e    // All Users\Documents
#define CSIDL_COMMON_ADMINTOOLS         0x002f    // All Users\Start Menu\Programs\Administrative Tools
#define CSIDL_ADMINTOOLS                0x0030    // <user name>\Start Menu\Programs\Administrative Tools
#define CSIDL_CONNECTIONS               0x0031    // Network and Dial-up Connections

The LocalStorage constructor requires a file name and application name as input. This way, the class can be used generically without hard coded file and application names. Based on the application name and file name, a full path including filename is constructed:

public LocalStorage(string FileName, string ApplicationName)
{
   // Get folder path to local storage
   path = getSpecialFolderPath(CSIDL.CSIDL_APPDATA);

   // Add application specific folder to path
   path = path + @"\" + ApplicationName;

   // Add the folder path to the filename
   fileName = path + @"\" + FileName;
}

Note in the code below that this folder is created if it does not exist. Every application should have its own folder beneath the \Application Data\ folder. Make sure you pick a name unique to your application. The application name parameter passed to the LocalStorage class is used to create and access an application specific folder. You can even consider using a Global Unique Identifier for this purpose to ensure uniqueness.

Since the LocalStorage class is used to manage application settings, it is useful to have support for initialization of default settings. The Initialize-method can be used to pass a Hashtable of Key/Value pairs and create the local XML file.

public void Initialize(Hashtable KeyValues)
{
   // If directory does not exist, then create!
   if(!Directory.Exists(path))
   {
      Directory.CreateDirectory(path);
   }

   // If file does not exist, then create!
   if(!File.Exists(fileName))
   {
      // Initiate dataset
      DataSet ds = new DataSet();
      
      // Read schema
      ds.ReadXmlSchema(new XmlTextReader(Assembly.GetExecutingAssembly().GetManifestResourceStream ("ISBNAnyplace.ApplicationData.xsd")));

      // Populate dataset with KeyValue-pairs
      foreach (DictionaryEntry KeyValue in KeyValues)
      {
         // New row
         DataRow dr = ds.Tables["ApplicationData"].NewRow();

         // Set row properties
         dr["Key"] =  KeyValue.Key.ToString();
         dr["Value"] =  KeyValue.Value.ToString();
   
         // Add row to dataset
         ds.Tables["ApplicationData"].Rows.Add(dr);
      }

      // Save data
      ds.AcceptChanges();
      ds.WriteXml(fileName);
   }
}

Both the directory and file is created if they do not exist. If the file does not exist and the passed Hashtable contains data, then a new file is created and populated with the Key/Value pairs. Note that the XML Schema file is read from the executing assembly using the GetManifestResourceStream method.

The AddNew-method contains exception handling to manage the situation that occurs when duplicate data is about to be inserted. The catch-statement ensures that no data is lost by updating the already present row instead using the UpdateByKey-method.

public void AddNew(string Key, string Value)
{
   DataSet ds = getDataSet();

   // New row
   DataRow dr = ds.Tables["ApplicationData"].NewRow();

   // Set row properties
   dr["Key"] =  Key;
   dr["Value"] =  Value;
   
   // Add row to dataset.
   // Exception will be thrown if primary key violation occurs. Instead of adding, then update row.
   try
   {
      ds.Tables["ApplicationData"].Rows.Add(dr);

      // Save data
      ds.AcceptChanges();
      ds.WriteXml(fileName);
   }
   catch(Exception ex)
   {
      UpdateByKey(Key, Value);
   }
   finally
   {}
}

Download this article's full source code, ISBNAnyplace.exe, at the beginning of this article for access to all of the LocalStorage class.

The MainForm Class

The MainForm implements the start up and main form. This form's code illustrates how the LocalStorage class is used and how form navigation within an application can be handled. The class publishes three public string variables:

public string ApplicationDataFile = "ApplicationData.xml";
public string ApplicationSettingsFile = "ApplicationSettings.xml";
public string ApplicationName = "ISBNAnyplace";

These variables are used when instantiating the LocalStorage class. The MainForm class constructor makes sure the two files exist using the LocalStorage class:

public MainForm()
{
   InitializeComponent();

   // Initialize Application Data and Settings files
   // Initialize Application Data file with no initial data
   LocalStorage ApplicationData = new LocalStorage(ApplicationDataFile,  ApplicationName);
   ApplicationData.Initialize(new Hashtable());

   // Initialize Application Settings file with initial settings
   LocalStorage ApplicationSettings = new LocalStorage(ApplicationSettingsFile, ApplicationName);
   Hashtable InitialSettings = new Hashtable();
   InitialSettings.Add("MaxNumberOfBooks", "10");
   ApplicationSettings.Initialize(InitialSettings);
}

The MainForm Load event calls the private method populateListview which instantiates the LocalStorage class and calls the GetAll-method to retrieve all books stored in ApplicationData.xml.

private void populateListview()
{
   LocalStorage ApplicationData = new LocalStorage(ApplicationDataFile,  ApplicationName);
   DataSet ds = ApplicationData.GetAll();

   // Clear listview
   this.lvwBook.Items.Clear();

   // Loop through rows backwards. The last value appears first.
   for(int i = ds.Tables["ApplicationData"].Rows.Count-1; i >= 0; i--)
   {
      DataRow dr = ds.Tables["ApplicationData"].Rows[i];
      
      // New item
      ListViewItem book = new ListViewItem(dr["Key"].ToString());

      // Assign ISBN and Price
      book.SubItems.Add(dr["value"].ToString());

      // Add the item to the listview
      this.lvwBook.Items.Add(book);
   }

   // Select first item in list
   if(this.lvwBook.Items.Count>0)
      this.lvwBook.Items[0].Selected=true;
   
}

ISBN Anyplace implements a setting that controls maximum number of books stored in the application data file. When the user wants to add a new book to the list, a check is performed comparing the number of books already stored and maximum number of books. This check is executed in the New_Click event and uses the LocalStorage class GetByKey method:

private void mitNew_Click(object sender, System.EventArgs e)
{
   Cursor.Current = Cursors.WaitCursor;

   try
   {
      // First check if MaxNumberOfBooks has been reached
   
      // Use LocalStorage class for Application Data
      LocalStorage ApplicationData = new LocalStorage(ApplicationDataFile,  ApplicationName);
      
      // Get current number of books
      int NumberOfBooks = ApplicationData.GetAll().Tables["ApplicationData"].Rows.Count;

      // Use LocalStorage class for Application Settings
      LocalStorage ApplicationSettings = new LocalStorage(ApplicationSettingsFile);
   
      // If MaxNumberOfBooks has been reached, then throw exception
      if(NumberOfBooks > System.Convert.ToInt32(ApplicationSettings.GetByKey("MaxNumberOfBooks")))
      {
         throw new Exception("Maximum number of books has been reached.  Delete one or more books.");
      }

      // Open new book form
      NewBook newbook = new NewBook(this);
      newbook.ShowDialog();

      // Populate listview
      populateListview();

      // Focus on listview
      this.lvwBook.Focus();
   }

   catch(Exception ex)
   {
      Cursor.Current = Cursors.Default;
      MessageBox.Show(ex.Message, "Error", MessageBoxButtons.OK,  MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button1);
   }
   finally
   {

   }
}

If the check verifies that more books can be added, then the MainForm itself is passed to the Settings-form. There are number of reasons for doing this. The form navigation process implemented in ISBN Anyplace is dependent on this procedure and it is explained in detail in the white paper Develop for Windows Mobile-based Smartphones Using the .NET Compact Framework. Furthermore, the Settings-class can access the variables ApplicationDataFile and ApplicationSettingsFile held by the MainForm class because of this procedure.

The NewBook Class

The NewBook class implements the form used to get new prices from the Barnes & Noble XML Web Service. The class constructor first takes care of the passed MainForm and sets a private variable to keep it available:

public NewBook(MainForm ParentForm)
{
   InitializeComponent();

   this.parentForm = ParentForm;
}

The Get_Click event instantiates the BarnesNobleWebService class, which wraps around the XML Web Service reference, retrieves the price and adds it to the application data file using the AddNew-method.

private void mitGet_Click(object sender, System.EventArgs e)
{
   Cursor.Current = Cursors.WaitCursor;
   
   BarnesNobleWebService bnws = new BarnesNobleWebService();

   string ISBN = this.txtBookISBN.Text;

   // Get price
   float price = bnws.GetPrice(ISBN);

   // Instantiate LocalStorage for application data
   LocalStorage ApplicationData = new LocalStorage(parentForm.ApplicationDataFile, parentForm.ApplicationName);
      
   // Store ISBN and price to XML file
   ApplicationData.AddNew(ISBN, price.ToString());

   // Reset textbox
   txtBookISBN.Text = "";
   
   Cursor.Current = Cursors.Default;
   
   // Close form
   this.Close();
}

The form's Closing event takes care of showing the parent form again, the MainForm.

private void NewBook_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
   parentForm.Show();
}

The Settings Class

The Settings class implements the form used to manipulate the application setting "MaxNumberOfBooks". There are no differences between the Settings class and the NewBook class in terms of form navigation code. The main code in the Settings class updates the application setting file:

private void mitDone_Click(object sender, System.EventArgs e)
{
   // Save value and close form
   LocalStorage ApplicationSettings = new LocalStorage(this.parentForm.ApplicationSettingsFile,  this.parentForm.ApplicationName);
   ApplicationSettings.UpdateByKey("MaxNumberOfBooks", this.txtMaxNumberOfBooks.Text);
   this.Close();
}

The EditModeHandler Class

ISBN Anyplace uses the EditModeHandler class to change textboxes to accept only numerical input. The EditModeHandler class is explained in more detail in the white paper Develop for Windows Mobile-based Smartphones Using the .NET Compact Framework.

More information on the subject of Smartphone input mode can also be found in the article Crafting Smartphone User Interfaces Using .NET Compact Framework: "The Compact Framework does not provide a class to change the input mode, so we need to look at how native applications set this. The Smartphone API provides the message EM_SETINPUTMODE which can be sent to a windows handle to set the input mode, so we need to use native interop to achieve this."

The article also contains an important warning related to the use of P/Invoke and the use of HWNDs from within the .NET Compact Framework:

**Warning   **The use of HWNDs from within the .NET Compact Framework is unsupported and should be thoroughly tested within your application. Due to how the runtime internally manages these, use of HWNDs may not work with future releases or could have breaking consequences if you do a lot of manipulation with them.

Conclusion

This article provided information and sample code to be used when addressing needs of application data and settings storage in Smartphone development. The class LocalStorage implemented a private method wrapping around the SHGetSpecialFolderPath API in order to get a correct path to the non-volatile flash memory. The sample code also illustrated that combining the use of local XML files, XML Web Services and ADO.NET DataSets provide an efficient platform for working with structured information and managing local storage.