Chapter 39. Persisting Application Settings to Isolated Storage

 

It’s very good for an idea to be commonplace. The important thing is that a new idea should develop out of what is already there so that it soon becomes an old acquaintance. Old acquaintances aren’t by any means always welcome, but at least one can’t be mistaken as to who or what they are.

 
 --Penelope Fitzgerald

Almost every application requires the ability to store session and state information. This is especially true when dealing with disconnected applications or applications that cache data when possible to reduce load times. Traditionally, settings were persisted to INI files when working with 16-bit Windows, but this approach had fairly substantial limitations. Maximum file size was a factor when using the APIs provided by Microsoft, and the settings were stored in flat file format, so hierarchical relationships could not be represented. Another problem was determining where INI files should be located. Typically, these files were deployed alongside the executables or in the Windows directory because it was the only directory guaranteed to be available on all computers. Additionally, multiple users were not supported by this mechanism. This deployment strategy created a configuration nightmare, and was very hard to support and maintain.

Microsoft tried to introduce a new approach to solve these problems by creating the registry on 32-bit Windows. This solution could represent hierarchical data, so complex data could be nested to make configuration much cleaner. There was also no size limit on the amount of data that could be stored. One of the biggest problems with INI files is deciding where to place the physical files, which is not an issue with the registry because it is globally accessible. The registry solved a number of problems with INI configuration, but other problems were introduced in doing so. With all configuration data for the system stored in a single location, the entire system slows down at an exponential rate as more entries are added. Having large amounts of configuration data in the same place also makes finding a particular entry challenging. Additionally, the registry is only accessible via an API, so backing up configuration info is extremely difficult. Security is also a big concern, because it can be risky to allow application access to the registry to save a few configuration settings, especially if the application is untrusted. Lastly, the entire system depends on the registry, so any accidents using the regedit tool can cripple the operating system.

The .NET framework introduced application configuration files that allow applications to be configurable without the need for recompilation of source code. This approach is widely used by .NET applications, but a major limitation is that this is a read-only mechanism, because ConfigurationSettings does not support writing. The app.config files can be manipulated as normal XML documents, but this considered a hack and bad practice.

A custom solution could be used to save settings as either binary or text to regular files, but then there are even more deployment and maintenance issues that arise. Issues like data protection, where to save the files, and additional code to support loading and saving these files must also be written.

The ideal solution to the mentioned problems would be a mechanism that can separate files per application or per Windows user so that multiple users on the same machine can have their own settings. The ability to save arbitrary data is important, especially if the proposed solution is intended to be generic and widely reusable. Data should be storable in a variety of formats such as flat text, hierarchical, or binary. Lastly, the mechanism must be secure, so that untrusted callers like downloaded executables cannot access sensitive system information. Microsoft introduced another approach with the .NET framework called isolated storage that is the recommended approach when the need for data persistence arises.

Concept of Isolated Storage

Isolated storage is basically a virtual folder that only your application can access. You can create files and directories in your isolated storage and treat it pretty much like normal disk space. Application users never have to actually know where data is physically located on the hard drive. The physical location for isolated storage varies for each operating system, so you just have to tell .NET framework to persist data to isolated storage and it handles all the implementation details. Isolated storage can also be specific to each assembly, or to each Windows user. Perhaps one of the best features about isolated storage is that the application does not require file system permissions. This makes it even easier for an application to run under a least privilege account.

Isolated storage cannot be accessed by a different assembly than the one assigned to do so. Isolated storage locations for assemblies are even isolated from each other within the same process. The same security restriction applies to different Windows users.

Note

It is important to know that isolated storage data is limited to 10 MB per each assembly. Just be sure to efficiently manage the data placed in isolated storage and clean out cached data when it is no longer needed.

Accessing Isolated Storage

The code to access isolated storage for this chapter also makes use of binary serialization, generics, and the hashtable collection. The following code shows the appropriate namespaces that are needed for this functionality.

using System;
using System.IO;
using System.IO.IsolatedStorage;
using System.Collections;
using System.Deployment.Application;
using System.Runtime.Serialization.Formatters.Binary;

The first step for either the load or the save method is to retrieve the isolated storage file that represents the top-level directory in the storage. This object can be used to manipulate both files and directories. There are a couple different deployment situations that must be taken into consideration when working with isolated storage. The differences are later discussed in detail, but at this point you can be aware that network deployed means that the application has been installed using ClickOnce. The following code retrieves the appropriate isolated storage depending on the deployment situation.

private static IsolatedStorageFile OpenIsolatedStorage()
{
    IsolatedStorageFile storage = null;
    if (ApplicationDeployment.IsNetworkDeployed)
    {
        storage = IsolatedStorageFile.GetUserStoreForApplication();
    }
    else
    {
        storage = IsolatedStorageFile.GetUserStoreForDomain();
    }
    return storage;
}

Now that we have retrieved the isolated storage reference, we can begin saving files to it. Any type of data can be saved into isolated storage, but the purpose of this chapter is to show application state persistence.

Our system is going to operate on a single file that will store all the persisted information, so we are going to want a data structure to hold our state information while in memory. This will be accomplished through the use of the hashtable collection. A hashtable will be used to store the state information referenced by unique key. The save will serialize the hashtable into the isolated storage, while the load will deserialize the hashtable from the isolated storage and back into object form. Hashtable cannot be serialized to XML because it inherits from an interface, but BinaryFormatter can be used instead to serialize it to binary. The following code shows the save routine for the isolated storage interface.

public static void WriteSetting<KEY, TYPE>(string fileName,
                                                 KEY key,
                                                 TYPE setting)
{
    try
    {
        IsolatedStorageFile storage = OpenIsolatedStorage();

        BinaryFormatter formatter = new BinaryFormatter();
        Hashtable settings = null;

        using (Stream loadStream = new IsolatedStorageFileStream(fileName,
                                                FileMode.OpenOrCreate, storage))
        {
            try
            {
               settings = (Hashtable)formatter.Deserialize(loadStream);
            }
            catch (Exception)
            {
                // Quietly handle this error
            }
        }
        if (settings == null)
        {
            settings = new Hashtable();
        }
        settings[key] = setting;
        using (Stream writeStream = new IsolatedStorageFileStream(fileName,
                                                       FileMode.Create, storage))
        {
            formatter.Serialize(writeStream, settings);
        }
    }
    catch (Exception)
    {
        // Quietly handle this error
    }
}

The load routine for the isolated storage interface is just as easy as the save. First, we check to make sure the settings file exists in isolated storage, and we open it as a stream if possible. The data from the stream is then sent into BinaryFormatter, and data is deserialized into a hashtable containing the settings. The appropriate value referenced by the supplied key is then returned. The following code shows the load routine for the isolated storage interface.

public static TYPE ReadSetting<KEY, TYPE>(string fileName, KEY key)
{
    try
    {
         IsolatedStorageFile storage = OpenIsolatedStorage();
         string[] fileMatches = storage.GetFileNames(fileName);
          if (fileMatches.Length > 0 && fileMatches[0].Length > 0)
          {
              using (Stream loadStream = new IsolatedStorageFileStream(fileName,
                                                          FileMode.Open, storage))
              {
                  BinaryFormatter formatter = new BinaryFormatter();
                  Hashtable settings = formatter.Deserialize(loadStream)
                                                                     as Hashtable;
                  if (settings != null)
                  {
                      return (TYPE)settings[key];
                  }
              }
          }
          return default(TYPE);
    }
    catch (Exception)
    {
        return default(TYPE);
    }
}

Using the code is very straightforward, but you may want to know if an application is launching for the first time or not so you can save default settings into isolated storage. The following code shows how to determine whether an application is launching for the first time.

bool firstRun = !ReadSetting<string, bool>("MySettingsFile.dat",

                                                 "FirstTimeLaunching");
if (firstRun)
{
    WriteSetting("MySettingsFile.dat", "FirstTimeLaunching", true);
}

Levels of Isolation

The whole idea behind isolated storage is that the physical location of stored files is managed by the .NET framework, not the application directly. Ignoring politics, it is important to understand how the framework associates an isolated storage location with a particular application. The association with an isolated storage location all comes down to how applications are identified by the framework.

Every user gets a collection of isolated storages, which are contained in a unique directory on the hard drive. This prevents interference between different users. Remember that the physical location of isolated storage files varies among operating systems, but typically they can usually be found at:

Documents and Settings<username>Local SettingsApplication DataIsolatedStorage

Each record in isolated storage is referenced by a tag. If the assembly is signed, then this tag is the strong name as described by the Global Assembly Cache (GAC), and its location on the hard drive is not used for association. If the assembly is not signed, then the tag is the URL to where the assembly resides on the hard drive. Two identical copies of an assembly residing in different directories will be allotted separate isolated storages. This does mean that if an unsigned assembly is moved, the association to that assembly is broken and a new isolated storage will be created for it.

At this point, you may be wondering about the dangers of sharing assemblies between multiple applications, and corrupting data that does not belong to you. Another type of identification used by the framework for associations is at the domain (AppDomain) level. This level refers to the location where the code was executed from. A program executed from the local hard drive will have the same domain as the assembly tag. A program downloaded from the web would have a domain that is the URL where the program was downloaded from.

Management and Debugging

Isolated storage enforces a 10 MB data size limit per assembly, but it is still important to clean up data when it is no longer needed. There is a tool provided with the .NET framework that handles administration of isolated storage for each user. This tool is very simple and only offers limited functionality, but it is useful when debugging your storage.

Typing the command storeadm /help will produce the following usage text:

Microsoft (R) .NET Framework Store Admin 2.0.50215.44
Copyright (C) Microsoft Corporation. All rights reserved.

Usage    : StoreAdm [options]
options  : [/LIST] [/REMOVE] [/ROAMING | /MACHINE] [/QUIET]
/LIST    : Displays the existing isolated storage for the current user.
/REMOVE  : Removes all existing isolated storage for the current user.
/ROAMING : Select the roaming store.
/QUIET   : Only error messages will be output.
/MACHINE : Select the machine store.

Execute the demo application without a strong name key. Running the command storeadm /list will produce results similar to the following:

Microsoft (R) .NET Framework Store Admin 2.0.50215.44
Copyright (C) Microsoft Corporation. All rights reserved.

Record #1
[Domain]
<System.Security.Policy.Url version="1">
<Url>file:///C:/GETD/IsolatedStorageDemo.exe</Url>
</System.Security.Policy.Url>

[Assembly]
<System.Security.Policy.Url version="1">
<Url>file:///C:/GETD/IsolatedStorageDemo.exe</Url>
</System.Security.Policy.Url>
         Size : 1024

Notice that the domain and the assembly Url tags are identical. This means that if you move the assembly, the association to the isolated storage will be broken and a new one will be created.

Now give the assembly a strong name key and run the same command again. You should be shown results similar to the following:

Microsoft (R) .NET Framework Store Admin 2.0.50215.44
Copyright (C) Microsoft Corporation. All rights reserved.

Record #1
[Domain]
<StrongName version="1"
Key="00240000048000009400000006020000002400005253413100040000010001007353FE7EBDA40
8B323B72D672E003AF9F09659AF60C233333CDE3C7AC02AC57864B746E0029B3FBC66A31DA8BD75084
27A271E52EA5B7295D97839A038932D4BA50920BE848BDDBB2F536FCB396B9CE422C1AEE47730607D4
D20F22586D4B73AC5A39FA03D1DC796F34E5ABB6041416C13CCE66CDBAAB15D353978332AEB5BB"
Name="IsolatedStorageDemo"
Version="1.0.0.0"/>

[Assembly]
<StrongName version="1"
Key="00240000048000009400000006020000002400005253413100040000010001007353FE7EBDA40
8B323B72D672E003AF9F09659AF60C233333CDE3C7AC02AC57864B746E0029B3FBC66A31DA8BD75084
27A271E52EA5B7295D97839A038932D4BA50920BE848BDDBB2F536FCB396B9CE422C1AEE47730607D4
D20F22586D4B73AC5A39FA03D1DC796F34E5ABB6041416C13CCE66CDBAAB15D353978332AEBC5BB"
Name="IsolatedStorageDemo"
Version="1.0.0.0"/>

         Size : 1024

Notice now that the Url tag has disappeared, and the public strong name key is now set. The assembly can now reside at any location on the local hard drive and remain associated to this isolated storage.

You may at times wish to remove isolated storages. Removing all the storages for the current user can be done using the storeadm /remove command. This operation requires elevated permissions, generally admin status for the local machine. Finer control has to be done programmatically. storeadm is a managed application, so disassembling the executable will give you an idea of how it works, and how to enumerate isolated storages for the current user. It is generally common practice to create a hook in the assembly to remove data in isolated storage when it is no longer needed.

Conclusion

This chapter discussed traditional methods for storing legacy application data, and later covered the concept of isolated storage. Ideal uses for isolated storage include user settings and preferences, queued data waiting for a connection to be established to the Internet, and cached data retrieved from web services and databases.

So, having looked at the details of what isolated storage is and how it works, we looked at some general ideas of when and how to use it form. Do not store user documents or downloaded assemblies, because users will have a difficult time locating these documents on the hard drive for transfers or backups. Downloaded assemblies that are placed in isolated storage will also not be loadable because there is no path name. Finally, any data you place in isolated storage has the potential to be read by users because stored data is not encrypted. Do not store private or sensitive information in isolated storage unless you handle the encryption.

Isolated storage is a great concept, and the .NET framework provides a solid foundation for it. The standard is to use application configuration files (app.config) when handling read-only settings, and to use isolated storage when you need bidirectional data persistence.

 

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.163.197