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.