Introduction to Maintaining User-Specific Data Using Isolated Storage and C#

Environment: Visual Studio .NET (version 7.0.9466), .NET Framework (Version 1.0.3705), Windows XP

Introduction

The .NET framework provides a very useful feature that allows an application to store and retrieve data on a per-user basis. This new Isolated Storage mechanism replaces the previous methods of storing such data in Windows .ini files and the system Registry. Isolated Storage allows applications to save user-specific data into a unique location determined via the user and assembly identities. This effectively eliminates storage conflicts and provides the developer with a completely transparent and reliable storage location.

Although the .NET Framework Developer's Guide suggests many situations where Isolated Storage is useful, an obvious and highly effective use for it is to maintain simple user-specific preferences and configurations for an application. In today's applications, it is becoming more and more common to maintain an application state and to provide users with the ability to customize features and appearance. Users now expect that an application will maintain their settings when they run it.

This article primarily provides an overview of how you can use Isolated Storage within your C# application to read and write data easily and safely to/from a managed transparent location, determined by the .NET framework.

The second part of this tutorial is a step-by-step guide to develop a simple example application which demonstrates user-customizability via Isolated Storage. Because many people are new to C#, I've made the guide detailed enough for a beginner to work through from start to finish. Hopefully, if you're new to .NET and C#, and you work through the entire example from start to finish, you'll learn a few other useful things on the way. The example guide assumes that the developer is familiar with the very basics of C# development using MicroSoft Visual Studio .NET.

Using Isolated Storage

Isolated Storage is very easy to use within your C# application. As previously explained, this feature allows an application to save user-specific data into a unique location (data compartment) that is associated with both the user and assembly identities. It is possible to actually limit and control the size of an application's Isolated Storage, as well as configuring the security around it. For the scope of this tutorial, we are primarily interested in simply using the feature to read and write data reliably and easily.

Isolated Storage is implemented as a file stream. To store data, you first must create an instance of an IsolatedStorageFileStream. To write to this stream, we also will require the creation of a StreamWriter object. To create these objects, we need to include the appropriate namespaces:

using System.IO;
using System.IO.IsolatedStorage;

The IsolatedStorageFileStream object can be initialized with a filename and file mode. With Isolated Storage, the developer does not know what the storage location will be in advance (and so cannot create a pre-installed default settings file), and so it is essential that the application assumes that the file could not exist and must therefore be created if required. This is done using the FileMode.Create file mode.

After the IsolatedStorageFileStream is created, it can be used as the stream parameter in the initialization of a StreamWriter object. This StreamWriter object then can be used to output any form of string-based data that you want. The opportunities are endless here—you could quite easily store XML data in there. Here is an example WriteUserData function that performs the actions we have described, storing five simple string values from the containing class:

private void WriteUserData()
{
  // create an isolated storage stream...
  IsolatedStorageFileStream userDataFile = 
    new IsolatedStorageFileStream("UserData.dat", FileMode.Create);

  // create a writer to the stream...
  StreamWriter writeStream = new StreamWriter(userDataFile);

  // write strings to the Isolated Storage file...
  writeStream.WriteLine(string1);
  writeStream.WriteLine(string2);
  writeStream.WriteLine(string3);
  writeStream.WriteLine(string4);
  writeStream.WriteLine(string5);

  // Tidy up by flushing the stream buffer and then closing
  // the streams...
  writeStream.Flush();
  writeStream.Close();
  userDataFile.Close();
}

Yes, it is as simple as that! All we need to supply is the filename. The actual location never concerns us; the .NET framework takes care of it. Similarly, the following ReadUserData example function shows how the file can be read again to retrieve the five string values:

private void ReadUserData()
{
  // create an isolated storage stream...
  IsolatedStorageFileStream userDataFile =
    new IsolatedStorageFileStream("UserData.dat", FileMode.Open);

  // create a reader to the stream...
  StreamReader readStream = new StreamReader(userDataFile);

  // write strings to the Isolated Storage file...
  string1 = readStream.ReadLine();
  string2 = readStream.ReadLine();
  string3 = readStream.ReadLine();
  string4 = readStream.ReadLine();
  string5 = readStream.ReadLine();

  // Tidy up by closing the streams...
  readStream.Close();
  userDataFile.Close();
}

When the IsolatedStorageFileStream object is initialized here, we pass the FileMode.Open file mode because we want to open an existing file. The code does not handle the exception that would occur if the file did not exist, which must be considered when used in a real application. The simple methods described above also store and retrieve the strings in a known order for clarity. It would be safer to store data as name-value pairs and parse the values when reading (to remove order-dependency).

Isolated Storage Example—A Step-by-StepGuide

This step-by-step guide can be followed to build the example solution included with this tutorial. In fact, if you follow it carefully, you won't need to download anything at all! The example is a simple customizable application which enables the user to select what a message on the application displays, if the application has minimize and/or maximize buttons available in the title bar, if the application appears in the Windows task bar, and finally the background color of the application.

Start by creating a new C# Windows application called IsolatedStorageExample. First, we need to do some tidying up. In the Solution Explorer, right-click Form1.cs and rename the file to MainForm.cs. Now, right-click MainForm.cs and select View Code. With the code visible, press Control+H to invoke the Find and Replace dialog. We want to give the form class a suitable name, so Replace all instances of Form1 with MainForm (there should have been four). We are now ready to begin creating our application.

In the Design view of MainForm.cs, select the MainForm object and change the Text property to "Isolated Storage Example" (ignore the quotations). This gives our application a sensible title. Now set the form's Height and Width properties to be 200 and 300 respectively (found under the Size property). Your form should now look something like this:

Now add two new labels to the form. Stretch the width of each label so that they fill the width of the form. Position the labels one above the other on the form. Set the names of these labels to lblMessage and lblLastCustomized. Set the Text property of lblMessage to "Message: No Message" and the Text property of lblLastCustomized to "Last Customized: Not Yet Customized". Now set the BackColor property of both lblMessage and lblLastCustomized to White. The form now should look something like this:

Add a button to the form and call it bnCustomize. Position the button at the bottom of the form somewhere, clear of the labels. Set the Text property of bnCustomize to "Customize". Now set the BackColor property of bnCustomize to Yellow. At this point, our MainForm's appearance is complete. It represents our customizable application and should look something like this:

Okay, so we have our customizable application but how to we actually allow the user to customize it? We need to create a new form. To do this, select Add Windows Form from the Project menu. Select the Windows Form template and name the file CustomizeForm.cs. Add a textbox to the new form and name it txtMessage. Change the Text property of txtMessage to be empty and widen the object so that it nearly fills the form width. Add three checkboxes to the form and name them cbMinimize, cbMaximize, and cbTaskBar. Set the Text property of cbMinimize to "Minimize Box". Set the Text property of cbMaximize to "Maximize Box". Set the Text property of cbTaskBar to "Show In Task Bar". You can widen any of these checkboxes if the label does not fit on one line. If you want, you now can add some static labels to your form to give lblMessage a heading of "Message" and the checkboxes a label of "Settings". I have done this using two static groupbox objects (you will need to place existing object on top of these groupboxes), and the form look like this:

There is one last customizable setting to add to this form—the background color. We will add a button now to enable the user to select a color. Call the new button bnColor and set the Text property of it to "Set Background Color". Resize the button so that the label fits correctly and position it along with the checkboxes (under the Settings heading). Because we will need to read settings from the controls on this dialog, it is very important that we now set the Modifiers properties of lblMessage, cbMinimize, cbMaximize, cbTaskBar, and bnColor to Public. Finally, we need to enable the user to accept or reject the settings, so we add two new buttons and call them bnApply and bnCancel. These are positioned at the bottom of the dialog. For bnApply, set the Text property to "Apply" and the DialogResult property to "OK". For bnCancel, set both the Text property and the DialogResult property to "Cancel". Finally, add a ColorDialog object to the form and name it colorDialog (notice that this object is placed under the form in Visual Studio .NET). Your form should look something like this:

Okay, the visual aspects of our forms have been created now, and up until this point you have probably noticed that we have not yet written any code at all! We shall do that now....

In MainForm.cs, the first thing that we are going to do is create some useful constants at the beginning of the MainForm class declaration. Add the following code within the start of the MainForm class:

const string MessageLabel    = "Message: ";
const string CustomizedLabel = "Last Customized: ";
const string UserConfigFile  = "StorageExample.dat";

Both MessageLabel and CustomizedLabel represent strings that we always want to precede corresponding output on our application, whereas UserConfigFile represents a filename which is specific to this application and will be used as our Isolated Storage data file.

In CustomizeForm.cs, you will notice that a default constructor has been created which takes no parameters. Because our Customize form needs to be set up with the current user settings on creation, we will change this constructor so that it takes five settings as parameters. Change the constructor to the following:

public CustomizeForm(string message, bool minBox, bool maxBox,
                                     bool taskBar, Color formColor)
{
  InitializeComponent();

  // Set up the form controls...
  txtMessage.Text    = message;
  cbMinimize.Checked = minBox;
  cbMaximize.Checked = maxBox;
  cbTaskBar.Checked  = taskBar;
  bnColor.BackColor  = formColor;
}

It can be seen here that when the form is created with the five passed parameter settings, we then set up our form to reflect these settings. The txtMessage text box is set to display the string message, the checkboxes cbMinimize, cbMaximize, and cbTaskBar are checked/unchecked appropriately, and the color select button color is set to match the Color parameter passed.

Now that we can construct and set up our Customize form, we can put some code in place to actually do this. In the Design view of MainForm.cs, double-click the bnCustomize button to automatically generate an event handler for it, called bnCustomize_Click. This function shall contain any code we wish to execute if the user presses this button. We want this action to invoke our Customize form as a dialog. To do this, add code in the bnCustomize_Click function as follows:

private void bnCustomize_Click(object sender, System.EventArgs e)
{
  CustomizeForm customizeDialog 
            = new CustomizeForm(lblMessage.Text.Substring
                               (MessageLabel.Length),
                                this.MinimizeBox, this.MaximizeBox,
                                this.ShowInTaskbar, this.BackColor);

  if (customizeDialog.ShowDialog(this) == DialogResult.OK)
  {
    // Read the new custom settings from the customize dialog...
    lblMessage.Text        = MessageLabel
                           + customizeDialog.txtMessage.Text;
    lblLastCustomized.Text = CustomizedLabel
                           + DateTime.Now.ToString();
    this.MinimizeBox       = customizeDialog.cbMinimize.Checked;
    this.MaximizeBox       = customizeDialog.cbMaximize.Checked;
    this.ShowInTaskbar     = customizeDialog.cbTaskBar.Checked;
    this.BackColor         = customizeDialog.bnColor.BackColor;
  }

  // Close the Customize dialog...
  customizeDialog.Dispose();
}

You can see here that when the bnCustomize button is pressed, the first thing that happens is that we instantiate our CustomizeForm object. We supply the constructor with our application's current state for all five of the customizable parameters. In the case of the message string, we pick out only the actual message part of it using the Substring function to remove the preceding label (the MessageLabel constant).

Next, we have a conditional statement that looks to see if the result of the dialog is "OK". You may remember earlier that we set the DialogResult value for the Apply button (on the CustomizeForm object) to be "OK". If the user presses the Apply button, we will enter this conditional statement; otherwise, we will skip it (for example, when the dialog is closed or the Cancel button is pressed). If the user has pressed Apply, we copy over the new custom settings from the CustomizeForm object. This is pretty straightforward in that we copy over directly the boolean state of the MinimizeBox, MaximizeBox, and ShowInTaskbar options, along with the BackColor value. It also can be seen that when we copy over the message string, we add our MessageLabel constant to the beginning. Finally, it can be seen that there is another property present that is not actually copied over from the CustomizeForm object. This is the current date and time, which we add a preceding label to (the CustomizedLabel) and then display on our MainForm. This extra string displays the date and time that the application was last customized (as in, the last time the user pressed the Apply button on the CustomizeDialog form).

The next thing we need to do is to deal with any remaining functionality on our CustomizeForm. We have already seen that when we instantiate it, we set the values of all customizable properties to the controls on the form. All of these controls then can be manipulated, ready for the values to be read back from them when we click Apply—apart from the bnColor object. The BackColor property of bnColor cannot yet be changed, and this is where our ColorDialog object becomes useful. The colorDialog object provides us with a standard dialog object for selecting a new color. We need to invoke this dialog when the user presses the bnColor button. Double-click the bnColor button to automatically generate an event handler for it, called bnColor_Click. Add the following code into that function:

private void bnColor_Click(object sender, System.EventArgs e)
{
  colorDialog.Color = bnColor.BackColor;
  if (colorDialog.ShowDialog(this) == DialogResult.OK)
  {
    bnColor.BackColor = colorDialog.Color;
  }
}

It can be seen here that when the bnColor button is pressed, we simply set the current color of the colorDialog object to match our current BackColor, before actually invoking the dialog. When the user has finished selecting a new color from the colorDialog object and clicked "OK", we simply copy the new color over and set our bnColor.BackColor property to it. If the user closed the colorDialog object by any other method, we do nothing.

At this point, we are have nearly completed the functionality of our CustomizeForm object. Double-click the bnCancel button and set the event handler function to the following:

private void bnCancel_Click(object sender, System.EventArgs e)
{
  this.Close();
}

This will simply close CustomizeForm if the Cancel button is pressed. Finally, it would be nice to add a little user-specific feature to the CustomizeForm object. Every time the form is created, we can set the title of the form to display "Customize for username", where username is the actual Windows username of the current user. To do this, we double-click the title bar of the form to create an event handler for when the form is loaded, called CustomizeForm_Load. Add the following code to that function to set the form title as we require:

private void CustomizeForm_Load(object sender, System.EventArgs e)
{
  this.Text = "Customize for " + SystemInformation.UserName;
}

The code for the CustomizeForm object is now complete.

Now we move on to the whole point of this little tutorial—Isolated Storage. The first thing we need to do is set up the MainForm class to use this feature by ensuring the following namespaces are used in MainForm.cs:

using System.IO;
using System.IO.IsolatedStorage;

Next, we will create a new function called WriteCustomUserSettings to our MainForm class. This function will write all of our user-customizable settings to a user-specific file via Isolated Storage. It can be seen that it is very similar to the example given at the start of this tutorial. Add the function in the end of the MainForm class as follows:

private void WriteCustomUserSettings()
{
  IsolatedStorageFileStream userConfigFile =
    new IsolatedStorageFileStream(UserConfigFile, FileMode.Create);

  // create a writer to the stream...
  StreamWriter writeStream = new StreamWriter(userConfigFile);

  // write the form configuration to the file...
  writeStream.WriteLine(lblMessage.Text.Substring
                       (MessageLabel.Length));
  writeStream.WriteLine(lblLastCustomized.Text.Substring
                       (CustomizedLabel.Length));
  writeStream.WriteLine(this.MinimizeBox);
  writeStream.WriteLine(this.MaximizeBox);
  writeStream.WriteLine(this.ShowInTaskbar);
  writeStream.WriteLine(this.BackColor.ToArgb());

  // clean up...
  writeStream.Flush();
  writeStream.Close();
  userConfigFile.Close();
}

In this function, the first thing we do is to instantiate a new IsolatedStorageFileStream object. We call it userConfigFile and construct it using our preset filename (UserConfigFile) and a FileMode.Create parameter. Obviously, the filename parameter is the name of the file in which we will store our data and the FileMode.Create parameter signifies that the file should be created if it does not yet exist. It can be seen that we write the value of each of the customizable settings in a specific order (this is for simplicity, and so this order must be adhered to when reading). Any non-string value is automatically converted. Notice that we store the color as a string representation of the ARGB value. This will allow us to store any color, and successfully retrieve that color. If the color had been stored using the ToName() function, this would cause problems if the colour was anonymous as it would not be easy to read back in (try it!).

The WriteCustomUserSettings function will be called only if the user clicks Apply via the CustomizeForm dialog. This is handled within the bnCustomize_click function, when the DialogResult is "OK". Add the WriteCustomUserSettings call to the conditional statement so that the bnCustomize_click function now looks like this:

private void bnCustomize_Click(object sender, System.EventArgs e)
{
  CustomizeForm customizeDialog 
            = new CustomizeForm(lblMessage.Text.Substring
                               (MessageLabel.Length),
                                this.MinimizeBox,
                                this.MaximizeBox,
                                this.ShowInTaskbar,
                                this.BackColor);

  if (customizeDialog.ShowDialog(this) == DialogResult.OK)
  {
    // Read the new custom settings from the customize dialog...
    lblMessage.Text        = MessageLabel
                           + customizeDialog.txtMessage.Text;
    lblLastCustomized.Text = CustomizedLabel
                           + DateTime.Now.ToString();
    this.MinimizeBox       = customizeDialog.cbMinimize.Checked;
    this.MaximizeBox       = customizeDialog.cbMaximize.Checked;
    this.ShowInTaskbar     = customizeDialog.cbTaskBar.Checked;
    this.BackColor         = customizeDialog.bnColor.BackColor;

    // Write using Isolated Storage...
    WriteCustomUserSettings();
  }

  // Close the Customize dialog...
  customizeDialog.Dispose();
}

We now can write our user settings via Isolated Storage, but this is useless unless we can read them again. To do this, we create another function, called ReadCustomUserSettings. This function uses the Isolated Storage procedure already described to read our settings back into the appropriate places. We add preceding labels (our string constants) to the two strings for lblMessage.Text and lblLastCustomized.Text. We then also use some simple built-in functions to convert the stored Boolean string representations back to usable values. Finally, we convert the string representation of the color back to a usable Color object using the FromArgb() function. The full ReadCustomUserSettings should be added in the end of the MainForm class as follows:

private void ReadCustomUserSettings()
{
  try
  {
    IsolatedStorageFileStream userConfigFile = 
      new IsolatedStorageFileStream(UserConfigFile, FileMode.Open);

    // create a reader to the stream...
    StreamReader readStream = new StreamReader(userConfigFile);

    // read each setting from the file...
    lblMessage.Text        = MessageLabel + readStream.ReadLine();
    lblLastCustomized.Text = CustomizedLabel + readStream.ReadLine();
    this.MinimizeBox       = readStream.ReadLine().Equals
                                       (Boolean.TrueString);
    this.MaximizeBox       = readStream.ReadLine().Equals
                                       (Boolean.TrueString);
    this.ShowInTaskbar     = readStream.ReadLine().Equals
                                       (Boolean.TrueString);
    this.BackColor         = Color.FromArgb(System.Int32.Parse
                                           (readStream.ReadLine()));

    // clean up...
    readStream.Close();
    userConfigFile.Close();
  }
  catch (System.IO.FileNotFoundException)
  {
    // Warn the user that no user configuration file has been found
    // and the default settings will therefore be used...
    MessageBox.Show("This application has not yet been
                      user-customized.\n\n" +
          "Default settings will be applied.\n\n",
          "Isolated Storage Example");
  }
}

Notice here that we have added an exception handler which catches System.IO.FileNotFoundException. This needs to be present to cover the situation where a file does not exist. If the application is being run for the first time, this will be the case. If the exception occurs, we assume that this is why and output an appropriate message to the user.

The last thing that we need to do is make a call to our ReadCustomUserSettings function. We want this to occur every time our application is started. Double-click the title bar of the MainForm form to generate an event handler called MainForm_Load, which handles any functionality required when loading the form. Add a call to the ReadCustomUserSettings within it. It should look like this:

private void MainForm_Load(object sender, System.EventArgs e)
{
  ReadCustomUserSettings();
}

The example is now complete and can be built and run. Experiment with it by opening the application, customizing it, and then closing it. Re-launch it again to see how your settings are retained. Notice that the message box appears the first time you run the application, proving that the expected exception has been thrown when the file does not yet exist. Try logging into Windows as a different user. Run the application again and you will see that the retained settings really are user-specific. If you want to investigate where the application is writing to, search for StorageExample.dat on your hard drive (you may need to search hidden and system files).

Downloads

Download demo project - 13 Kb
Download source - 4 Kb


Comments

  • Dont rate this so harshly

    Posted by darwen on 12/13/2004 05:49pm

    Look, the .NET framework is like this. It's one of the things I don't like about .NET remoting : the fact that backward compatibility is completely impossible. Now, some would say that changes in settings between versions would require you to start anew. To prevent bugs/exceptions being made. In actual fact if you use this article sensibly then there's no problems. It shouldn't be rated one star. It's well written, comprehensive (except for the omission of versioning problems) and pretty good.

    Reply
  • Unfortunately, if you copy or move your app ...

    Posted by Legacy on 11/13/2003 12:00am

    Originally posted by: Rob

    Unfortunately, if you copy or move your app to another location it will create another *.dat file. Using registry keys makes it location-independent which is quite more functional and useful.

    Reply
  • What about upgrades?

    Posted by Legacy on 12/26/2002 12:00am

    Originally posted by: Michael Potter

    Very nice intro article.

    What happens when an application is upgraded. The Assembly changes and would no longer have access to the user settings. How can the upgrade access/import the previous versions settings?

    Thanks,
    Mike

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • On-demand Event Event Date: December 18, 2014 The Internet of Things (IoT) incorporates physical devices into business processes using predictive analytics. While it relies heavily on existing Internet technologies, it differs by including physical devices, specialized protocols, physical analytics, and a unique partner network. To capture the real business value of IoT, the industry must move beyond customized projects to general patterns and platforms. Check out this webcast and join industry experts as …

  • Learn How A Global Entertainment Company Saw a 448% ROI Every business today uses software to manage systems, deliver products, and empower employees to do their jobs. But software inevitably breaks, and when it does, businesses lose money -- in the form of dissatisfied customers, missed SLAs or lost productivity. PagerDuty, an operations performance platform, solves this problem by helping operations engineers and developers more effectively manage and resolve incidents across a company's global operations. …

Most Popular Programming Stories

More for Developers

RSS Feeds