Settings


Environment variables and command line arguments are both ways for the user to specify run-time settings to a particular application. .NET provides several more ways, including OS favorites like the Registry and special folders, as well as new ways like .config files and isolated storage.

Types of Settings

When saving settings, you should consider several different localities of settings:

  • Application . These settings are shared among all users of an application on the machine. For example, the list of directories in which to search for the assemblies to show in the Add Reference dialog is a per-application setting. [6]

    [6] This setting is stored at HKLM\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders in the Registry.

  • User . These settings are specific to an application and a user. For example, Minesweeper high scores are kept per user.

  • Roaming User . Application settings as well as user settings are specific to a machine, but roaming user settings are machine-independent . For example, if Minesweeper high scores were roaming, they'd be available to a specific user no matter what computer the user had logged in to. [7] Roaming user settings are good for things that don't depend on a machine, like a list of color preferences, but not for things that are related to current machine settings, like a window location. The use of roaming user settings presupposes that the machine is properly configured to support roaming. Otherwise, roaming user settings are equivalent to nonroaming user settings.

    [7] Roaming user settings do depend on very specific Windows domain network settings' being enabled.

  • Machine . These are application-independent settings, such as the current screen resolution or the PATH environment variable (although the PATH has a user portion).

  • Assembly . These settings are available on a per-assembly basis so that components can have their own versioned settings.

  • Domain . These settings are available on an application domain basis, which is the .NET equivalent of a process. ASP.NET hosts Web applications in their own domain and provides application domain settings.

Localities and Permissions

Different storage mechanisms support different localities (as well as other characteristics, such as whether they can be written to as well as read from). In addition, different localities require different user permissions. For example, writing application settings to the Registry requires Administrator group permissions, something you cannot assume that your user has. Before shipping an application that needs to read or write settings, you should test it under the most restricted set of permissions that your users could have.

.config Files

.NET provides .config files to serve as a read-only location for text-based application settings. A .config file is a file placed in the same folder as the application and having the same name as the application except for a .config extension. For example, the .config file associated with foo.exe would be named foo.exe.config. .NET itself uses .config files for all kinds of things, such as resolving assemblies and assembly versions. [8]

[8] Essential .NET, Volume 1: The Common Language Runtime (Addison-Wesley, 2003), by Don Box, with Chris Sells, covers assembly loading and versioning in detail.

You can add a new .config file to your VS.NET project by right-clicking on the project in Solution Explorer and choosing Add Add New Item Text File and naming the file "app.config" (without the quotation marks). This action will add an empty .config file to your project and, when your project is built, will copy and rename the app.config file to the output folder alongside your application. A minimal .config file looks like this:

 <configuration> </configuration> 

In addition to the .NET-specific settings, .config files can be extended with custom XML sections as designated with uniquely named elements. One general-purpose custom section built into .NET is designated with the element named appSettings. For example, the following .config file contains a custom value for pi (in case the 20 digits provided by System. Math.Pi just aren't enough):

 <configuration>  <appSettings>   <add key="pi" value="3.141592653589793238462" />   </appSettings>  </configuration> 

Each .config section has a specific section reader that knows how to read values from that section. These section readers can be defined in an application's .config file or in the systemwide machine.config, as shown here for the appSettings section reader:

 <configuration>   <configSections>     ...     <section       name="appSettings"       type="System.Configuration.NameValueFileSectionHandler, ..." />     ...   </configSections>   ... </configuration> 

A section reader is a class that implements IConfigurationSectionHandler and is registered with an entry in the configSections section of a .config file. For example, the NameValueFileSectionHandler class knows how to read a section in the appSettings format and return a NameValueCollection from the System.Collections.Specialized namespace. However, instead of creating an instance of NameValueFileSectionHandler yourself, it's more robust to use the ConfigurationsSettings class (from the System. Configuration namespace) to map the name of the section to a section reader for you:

  using System.Configuration;   using System.Collections.Specialized;  ... static void Main() {  NameValueCollection settings =   (NameValueCollection)ConfigurationSettings.GetConfig("appSettings");  ... } 

The ConfigurationSettings class finds the appropriate section handler. The section handler then looks in the current app configuration data for the appSettings section ( parts of which can be inherited from machine.config), parses the contents, builds the NameValueCollection, and returns it.

Because different section handlers can return different data types based on the data provided in their sections, the GetConfig method returns an object that must be cast to the appropriate type. As a shortcut that doesn't require the cast, the ConfigurationSettings class provides built-in support for the appSettings section via the AppSettings property:

 static void Main() {  NameValueCollection settings = ConfigurationSettings.AppSettings;  MessageBox.Show(  settings["pi"]  ); } 

When you've got the settings collection, you can access the string values using the key as an indexer key. If you'd like typed data (pi is not much good as a string), you can manually parse the string using the type in question. Alternatively, you can use the AppSettingsReader class (also from the System.Configuration namespace) to provide typed access to the appSettings values:

 static void Main() {   // Parse the value manually   NameValueCollection settings = ConfigurationSettings.AppSettings;   Decimal pi1 =  Decimal.Parse(settings["pi"])  ;  // Let AppSettingsReader parse the value   AppSettingsReader reader = new AppSettingsReader();  Decimal pi2 =  (Decimal)reader.GetValue("pi", typeof(Decimal));  ... } 

The AppSettingsReader class's GetValue method uses .NET type conversion classes to do its work, making things a bit easier for you if your application's .config file uses different types.

Dynamic Properties

If you'd like even easier access to values from the appSettings section of the .config file, you can bind a property of a form or control to a value by using the Property Browser and dynamic properties. A dynamic property is a property value that's pulled from the .config file, with the extra benefit that WinForms Designer writes the reader code for you in InitializeComponent. For example, to bind the Opacity property of a form to a value in the .config file, you bring up the properties for the form and press the " " button under the Advanced property of the DynamicProperties item, as shown in Figure 11.6.

Figure 11.6. Dynamic Properties in the Property Browser

In the Dynamic Properties dialog, check the box next to Opacity and notice the Key mapping, as shown in Figure 11.7.

Figure 11.7. Dynamic Properties Dialog

The Key mapping defaults to <objectName>.<propertyName>, but you can name it whatever you like. When you press the OK button and open the project's app.config, you'll notice a new key in the appSettings section:

 <?xml version="1.0" encoding="Windows-1252"?> <configuration>   <appSettings>  <add key="Form1.Opacity" value="1" />  <add key="pi" value="3.141592653589793238462" />   </appSettings> </configuration> 

Similarly, each dynamic property has a little document icon next to it in the Property Browser, as shown in Figure 11.8.

Figure 11.8. Opacity Marked as a Dynamic Property

When a property is marked as dynamic, the Designer writes the value you set in the Property Browser to the app.config file instead of to InitializeComponent. Subsequently, InitializeComponent will read the property from the .config:

 void InitializeComponent() {  AppSettingsReader configurationAppSettings =   new AppSettingsReader();  ...  this.Opacity =   ((System.Double)(configurationAppSettings.GetValue(   "Form1.Opacity", typeof(System.Double))));  ... } 

Now, when your application is deployed and your power users want to change the Opacity of the form, they can crack open the .config file with any text editor and have at it. Unfortunately, if the power user removes the Opacity setting or gives it an invalid format, the application will throw an exception at run time, and the InitializeComponent method won't do anything to deal with it. If you'd like to guard against that, you need to provide a UI for your users to set their preferences that is more robust than Notepad. And if that's the case, .config files are not for you. There is no API in .NET for writing .config files, only for reading them. This makes .config files effectively read-only for applications (although read/write for humans facile in XML). [9]

[9] Some humans have also posted code on the Web to add write capabilities to .config files, although as of .NET 1.1, .config files are still officially read-only.

The Registry

The Registry, on the other hand, has been the place to keep read/write application settings and roaming user settings from Windows 3.1 through Windows NT (it has fallen out of favor in more recent versions of Windows). The Registry gives you hierarchical, machinewide storage of arbitrary name/value pairs split into application and roaming user localities based on the path to the value. The Registry Editor (regedit.exe) is a built-in tool for setting and updating Registry values, [10] as shown in Figure 11.9.

[10] Be careful when editing Registry values. You're working on live data that's used by the entire system. One wrong move and you're reinstalling the OS, and there's no Undo!

Figure 11.9. The Registry Editor

The Registry is used a lot by Win32 applications, including the Explorer shell, so you can find yourself reading and writing Registry values whether or not you use it to store your own application's settings. For example, to use the Registry to associate a particular file extension with your application, you use the RegistryKey class from the Microsoft.Win32 namespace:

 using Microsoft.Win32; ... static void Main(string[] args) {   // Create a key and set its default value   using( RegistryKey key =            Registry.ClassesRoot.CreateSubKey(".tlf") ) {     // Map .tlf extension to a ProgID     key.SetValue(null, "tlffile");   }   // Create another key and set its default value   string cmdkey = @"tlffile\shell\open\command";   using( RegistryKey key =            Registry.ClassesRoot.CreateSubKey(cmdkey) ) {     // Map ProgID to an Open action for the shell     key.SetValue(null, Application.ExecutablePath + " \"%L\"");   }   ... } 

The RegistryKey class is a named "folder" in the Registry. This folder can have one or more named values, which are like the "files" (a name of null denotes the default value for a key). The values can be of several types, including string, unsigned integer, and arbitrary bytes. Writing to the Registry is a matter of opening or creating a subkey from one of the hive keys (which represent the top-level localities) and writing values. The hive keys are properties on the Registry object and translate into keys with well-known names in the Registry, as shown in Table 11.2.

Reading values from the Registry is similar to writing them:

 static void Main(string[] args) {   // Check whether someone has hijacked the .tlf extension   bool mapExtension = true;  // Open an existing key   using( RegistryKey key =   Registry.ClassesRoot.OpenSubKey(".tlf") ) {   // If the reference is null, the key doesn't exist  if( (  key != null  ) &&         (  key.GetValue(null).ToString()  .ToLower() != "tlffile" ) ) {       string ask = "Associate .tlf with this application?";       DialogResult res =         MessageBox.Show(ask, "Oops!", MessageBoxButtons.YesNo);       if( res == DialogResult.No ) mapExtension = false;     }  }  if( mapExtension ) {...}   ... } 
Table 11.2. Registry Properties and Key Names

Registry Class Static Property

Registry Hive Key Name

Registry.ClassesRoot

HKEY_CLASSES_ROOT

Registry.CurrentConfig

HKEY_CURRENT_CONFIG

Registry.CurrentUser

HKEY_CURRENT_USER

Registry.DynData

HKEY_DYN_DATA [a]

Registry.LocalMachine

HKEY_LOCAL_MACHINE

Registry.PerformanceData

HKEY_PERFORMANCE_DATA

Registry.Users

HKEY_USERS

[a] Win9x only

To use the Registry to store settings, Microsoft recommends putting them under the hive key using the following format:

 <hiveKey>\Software\<companyName>\<productName>\<productVersion> 

Here's an example:

 HKEY_CURRENT_USER\Software\Sells Brothers, Inc.\My Settings Test.0.1124.31077 

The variable values are, coincidentally, exactly the same values provided by Application.CompanyName, Application.ProductName, and Application.Version, so you can construct a top-level key name by using the following:

 string appkey =   string.Format(     @"Software\{0}\{1}\{2}",     Application.CompanyName,     Application.ProductName,     Application.ProductVersion); using( RegistryKey key = Registry.LocalMachine.OpenSubKey(appkey) ) {   ... } 

Similarly, for roaming user settings, Microsoft recommends using the same subkey but under the HKEY_CURRENT_USER hive instead of the HKEY_LOCAL_MACHINE hive. To accommodate the many people desiring to open subkeys for application and roaming user settings in the Microsoft-recommended spots, the WinForms Application object includes two properties that provide preopened registry keys at the right spot depending on whether you'd like application data or roaming user data. These properties are named CommandAppDataRegistry and UserAppDataRegistry. For example, to save the main form's position you could use the UserAppDataRegistry key:

 void MainForm_Closing(object sender, CancelEventArgs e) {  // Save the form's position before it closes   using( RegistryKey key = Application.UserAppDataRegistry ) {   // Restore the window state to save location and   // client size at restored state   FormWindowState state = this.WindowState;   this.WindowState = FormWindowState.Normal;   key.SetValue("MainForm.Location", ToString(this.Location));   key.SetValue("MainForm.ClientSize", ToString(this.ClientSize));   key.SetValue("MainForm.WindowState", ToString(state));   }  } // Convert an object to a string string ToString(object obj) {   TypeConverter converter =     TypeDescriptor.GetConverter(obj.GetType());   return converter.ConvertToString(obj); } 

This example uses the Closing event to notice when the main form is about to close (but before it does) to save the window state, location, and client size. In addition to remembering to restore the window state before saving the location and the client size, this code uses the ToString helper function. This function uses a type converter, which is a helper to aid in the conversion between instances of a type and strings (for more on type converters, see Chapter 9: Design-Time Integration). Any type can have an associated type converter, and most of the simple ones do. After the application is run once, notice the settings that are saved in the Registry, as shown in Figure 11.10.

Figure 11.10. Using the Registry for User Settings

Notice that the size is shown as "292, 54" instead of "{292, 54}," as would have happened if we had used the built-in Size class's ToString method. Because Size's type converter doesn't know how to translate the surrounding braces, it's important to use the type converter's conversion to the string format instead of the type's conversion to ensure that the value can be read back during loading of the form:

 void MainForm_Load(object sender, EventArgs e) {  // Restore the form's position   using( RegistryKey key = Application.UserAppDataRegistry ) {  try {       // Don't let the form's position be set automatically       this.StartPosition = FormStartPosition.Manual;       this.Location =         (Point)FromString(  key.GetValue("MainForm.Location"),  typeof(Point));       this.ClientSize =         (Size)FromString(  key.GetValue("MainForm.ClientSize"),  typeof(Size));       this.WindowState =         (FormWindowState)FromString(  key.GetValue("MainForm.WindowState"),  typeof(FormWindowState));     }     // Don't let missing settings scare the user     catch {}   } } // Convert a string to an object object FromString(object obj, Type type) {   TypeConverter converter = TypeDescriptor.GetConverter(type);   return converter.ConvertFromString(obj.ToString()); } 

In this case, the form's Load method uses the Registry key to load the settings that it saved during the Closing event, this time using the type converters to convert from a string to the appropriate type. Notice also that the Load method uses a try-catch block to wrap the attempts to pull values from the Registry; in this way, we avoid throwing an exception if the values aren't present. These values typically are missing when the application is first run. If you'd like, you can set defaults to avoid the exception when a value is not found:

 this.WindowState =   (FormWindowState)FromString(     key.GetValue("MainForm.WindowState",  "Normal"  ),     typeof(FormWindowState)); 

Although the Registry can be used for application and user settings, its use has fallen out of fashion. Corruption issues with early implementations of the Registry have given it a bad reputation. There's nothing inherently wrong with it that more modern versions of Windows haven't fixed long ago. However, because the Registry doesn't support nonroaming user settings or use from partially trusted applications, you'll want to consider special folder-based settings media, discussed next.

Special Folders

Special folders are folders that Windows designates as having a special purpose. For example, the default folder where programs are installed is special and is available this way:

 // Generally "C:\Program Files" string programFiles =  Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)  ; 

There are three special folders for settings: one each for the application, user, and roaming user localities. Table 11.3 shows them, along with some sample paths running on Windows XP.

The special folder serves as the top-level folder in the folder under which applications can store application settings, user settings, and roaming user settings (just as Registry.LocalMachine and Registry.CurrentUser provide the top level for application and roaming user settings). Under that folder, an application is expected to construct a subfolder to avoid colliding with other applications or even versions of itself. This subfolder has the following format:

 <specialFolder>\<companyName>\<productName>\<productVersion> 

For example:

[View full width]
 
[View full width]
C:\Documents and Settings\csells\Local Settings\Application Data\Sells Brothers, Inc.\My Settings Test.0.1124.33519
Table 11.3. Special Folders, Localities, and Examples

SpecialFolder Enum Value

Locality

Example Path

CommonApplicationData

Application

C:\Documents and Settings\All Users\Application Data

LocalApplicationData

User

C:\Documents and Settings\ < user > \Local Settings\Application Data

ApplicationData

Roaming user

C:\Documents and Settings\ < user > \Application Data

And just as the Application object provides shortcut access to Registry keys via properties, it also provides shortcut access to prefabricated folder names and folders via the CommonAppDataPath, LocalUserAppDataPath, and UserAppDataPath properties. For example, here's how to rewrite the Registry-based setting code using special folders:

 using System.IO; ... void MainForm_Closing(object sender, CancelEventArgs e) {   // Save the form's position before it closes   string fileName =  Application.LocalUserAppDataPath  + @"\MainForm.txt";   using( StreamWriter writer = new StreamWriter(fileName) ) {     // Restore the window state to save location and     // client size at restored state     FormWindowState state = this.WindowState;     this.WindowState = FormWindowState.Normal;     writer.WriteLine(ToString(this.Location));     writer.WriteLine(ToString(this.ClientSize));     writer.WriteLine(ToString(state));   } } void MainForm_Load(object sender, EventArgs e) {   AppSettingsReader asReader = new AppSettingsReader();   Decimal pi = (Decimal)asReader.GetValue("pi", typeof(Decimal));   piTextBox.Text = pi.ToString();   // Restore the form's position   try {     string fileName =  Application.LocalUserAppDataPath  + @"\MainForm.txt";     using( StreamReader reader = new StreamReader(fileName) ) {       // Don't let the form's position be set automatically       this.StartPosition = FormStartPosition.Manual;       this.Location =         (Point)FromString(         reader.ReadLine(),         typeof(Point));       this.ClientSize =         (Size)FromString(         reader.ReadLine(),         typeof(Size));       this.WindowState =         (FormWindowState)FromString(         reader.ReadLine(),         typeof(FormWindowState));     }   }   // Don't let missing settings scare the user   catch( Exception ) {} } 

In this case, only two things are different from the use of the Registry. The first is the use of a file to hold the form's settings instead of a Registry key to hold the data. This means that the values must be read in the same order as they're written (unless you use a smarter serialization strategy). The second is the use of the path to the user settings data instead of roaming user settings data. With the Registry, all user data is roaming, whether you want it to be or not (unless you do something custom under the HKEY_LOCAL_MACHINE hive). User settings that are related to the capabilities of the machine itself, such as the location, size, and state of a form, are better suited to nonroaming data.

Settings and Streams

It is convenient to use type conversions with the Registry because it maintains distinct values and makes them available via the Registry Editor, but when you've got a stream, real .NET serialization [11] becomes an attractive option:

[11] If you're not familiar with .NET serialization, you can read up on the basics in Appendix C: Serialization Basics.

 using System.Runtime.Serialization; using System.Runtime.Serialization.Formatters; using System.Runtime.Serialization.Formatters.Soap; ...  // Custom type to manage serializable form data   [SerializableAttribute]   class FormData {   public Point Location;   public Size ClientSize;   public FormWindowState WindowState;   public FormData(Form form) {   this.Location = form.Location;   this.ClientSize = form.ClientSize;   this.WindowState = form.WindowState;   }   }  void MainForm_Closing(object sender, CancelEventArgs e) {   // Save the form's position before it closes   string fileName =     Application.LocalUserAppDataPath + @"\MainForm.txt";   using( Stream stream =            new FileStream(fileName, FileMode.Create) ) {     // Restore the window state to save location and     // client size at restored state     FormWindowState state = this.WindowState;     this.WindowState = FormWindowState.Normal;  // Serialize custom FormData object   IFormatter formatter = new SoapFormatter();   formatter.Serialize(stream, new FormData(this));  } } void MainForm_Load(object sender, EventArgs e) {   // Restore the form's position   try {     string fileName =       Application.LocalUserAppDataPath + @"\MainForm.txt";     using( Stream stream =              new FileStream(fileName, FileMode.Open) ) {       // Don't let the form's position be set automatically       this.StartPosition = FormStartPosition.Manual;  // Deserialize custom FormData object   IFormatter formatter = new SoapFormatter();   FormData data = (FormData)formatter.Deserialize(stream);  // Set data from FormData object       this.Location = data.Location;       this.ClientSize = data.ClientSize;       this.WindowState = data.WindowState;     }   }   // Don't let missing settings scare the user   catch( Exception ex ) {     MessageBox.Show(ex.Message, ex.GetType().Name);   } } 

This example serializes an instance of a custom type that represents the setting data that we'd like to keep between sessions. To serialize an object is to read it from or write it to a stream. A stream is an object that provides access to a storage medium, such as a file or a database.

To have something to serialize, we've got a custom type called FormData, which keeps track of the location, client size, and window state. When it's time to save the form data, the code creates an instance of the new type and then hands it to the formatter, along with the file stream opened in the special folder. Similarly, when loading, we use a formatter to deserialize the form data and use it to restore the form. This is a much easier way to go than the type converter because it takes less code. In addition, serialization provides some nice extension options as time goes on and the FormData class needs to include extra information, such as font and color preferences.

Isolated Storage

One more technology that .NET provides for reading and writing settings data is isolated storage . It's called "isolated" because it doesn't require the application to know where on the hard drive the settings files are stored. In fact, it's just like using the special folder shortcuts provided by the Application class except that the path to the root of the path on the file system isn't even available to the application. Instead, named chunks of data are called streams, and containers (and subcontainers) of streams are called stores . The model is such that the implementation could vary over time, although currently it's implemented on top of special folders with subfolders and files.

The special folder you get depends on the scope you specify when getting the store you want to work with. You specify the scope by combining one or more flags from the IsolatedStorageScope enumeration:

 enum IsolatedStorageScope {   Assembly, // Always required   Domain,   None,   Roaming,   User, // Always required } 

Isolated storage stores must be scoped by, at a minimum, assembly and user. This means that there are no user-neutral settings available from isolated storage, only user and roaming user settings (depending on whether the Roaming flag is used). In addition, you can scope settings to a .NET application domain using the Domain flag, but typically that's not useful in a WinForms application.

Table 11.4 shows the valid combinations of scope flags as related to settings localities and sample folder roots under Windows XP.

Obtaining a store to work with is a matter of specifying the scope to the GetStore method of the IsolatedStorageFile class from the System.IO. IsolatedStorage namespace:

 IsolatedStorageScope scope =   IsolatedStorageScope.Assembly    IsolatedStorageScope.User; IsolatedStorageFile store =   IsolatedStorageFile.GetStore(scope, null, null); 
Table 11.4. Isolated Storage Scope, Locality, and Folder Roots

IsolatedStorageScope Flags

Locality

Folder Root

Assembly, User

User

C:\Documents and Settings\<user>\Local Settings\Application Data\IsolatedStorage

Assembly, User, Roaming

Roaming User

C:\Documents and Settings\<user>\Application Data\IsolatedStorage

Assembly, User, Domain

Domain

C:\Documents and Settings\<user>\Local Settings\Application Data\IsolatedStorage

Assembly, User, Domain, Roaming

Roaming User Domain

C:\Documents and Settings\<user>\Local Settings\Application Data\IsolatedStorage

Because getting the user store for the assembly is so common, the IsolatedStorageFile class provides a helper with that scope already in place:

 // Scope = User  Assembly IsolatedStorageFile store =   IsolatedStorageFile.  GetUserStoreForAssembly  (); 

After you've got the store, you can treat it like a container of streams and subcontainers by using the members of the IsolatedStorageFile class:

 sealed class IsolatedStorageFile : IsolatedStorage, IDisposable {   // Properties   public object AssemblyIdentity { get; }   public UInt64 CurrentSize { virtual get; }   public object DomainIdentity { get; }   public UInt64 MaximumSize { virtual get; }   public IsolatedStorageScope Scope { get; }   // Methods  public void Close();   public void CreateDirectory(string dir);   public void DeleteDirectory(string dir);   public void DeleteFile(string file);   public string[] GetDirectoryNames(string searchPattern);   public static IEnumerator GetEnumerator(IsolatedStorageScope scope);   public string[] GetFileNames(string searchPattern);  public static IsolatedStorageFile GetStore(...);   public static IsolatedStorageFile GetUserStoreForAssembly();   public static IsolatedStorageFile GetUserStoreForDomain();  public virtual void Remove();   public static void Remove(IsolatedStorageScope scope);  } 

Don't be confused by the fact that the IsolatedStorageFile class is actually implemented as a directory in the file system. This is only one implementation of the IsolatedStorageStorage abstract base class. Other implementations are certainly possible (although none are currently provided by .NET).

The most common thing you'll want to do with a store is to create a stream on it using an instance of IsolatedStorageFileStream. The IsolatedStorageFileStream class is just another implementation of the virtual methods of the FileStream class to hide the details of the underlying implementation. After you've got the stream, you can write to it or read from it just as if you'd opened it yourself as a file. Here's the same code again to store the main form's location using isolated storage:

 void MainForm_Closing(object sender, CancelEventArgs e) {   // Save the form's position before it closes  IsolatedStorageFile store =   IsolatedStorageFile.GetUserStoreForAssembly();   using( Stream stream =   new IsolatedStorageFileStream("MainForm.txt",   FileMode.Create,   store) ) {  ...  }  } void MainForm_Load(object sender, EventArgs e) {   // Restore the form's position   try {  IsolatedStorageFile store =   IsolatedStorageFile.GetUserStoreForAssembly();   using( Stream stream =   new IsolatedStorageFileStream("MainForm.txt",   FileMode.Open,   store) ) {  ...     }   }   // Don't let missing settings scare the user   catch( Exception ) {} } 
Managing Isolated Storage and Streams

You can manage the isolated storage using the Store Admin tool (storeadm .exe) from the command line. To list the user settings, use storeadm /list, as shown in Figure 11.11.

Figure 11.11. Using the Store Admin Tool to List Stores and Streams

Some records indicate storage, and others indicate streams. To remove all the user isolated storage, use storeadm /remove. Both of these commands operate by default on user settings. To act on roaming user settings, add the /roaming switch to either command. Unfortunately, the Store Admin tool does not show the contents of any storage or stream, nor does it show the mapping in the file system. So your best bet is to use the dir and find shell commands, starting at the root of the isolated storage folder you're interested in exploring.

Isolated Storage and Partial Trust

Although it may seem that isolated storage is only a more complicated use of special folders, there is an important benefit to using isolated storage instead of opening settings files directly: Using isolated storage lets you use partially trusted assemblies. A partially trusted assembly is one that runs in a security sandbox that limits its permissions ”for example, a WinForms control hosted in Internet Explorer. Partially trusted assemblies are not allowed to read or write to the Registry or the file system as needed for settings, but they are allowed to read and write to isolated storage. If you need settings for a partially trusted assembly, your only option is to use isolated storage for read/write user settings and to use .config files for read-only settings. Chapter 15: Web Deployment covers partially trusted assemblies in detail.

Versioned Data Paths

You may have noticed that all the paths provided by the Application object for the Registry and the special folders are built using a version number, including the build number and revision. This means that the next version of your application will be protected from the changing format of settings data. It also means that the next version of the application cannot use the paths provided by Application to find the settings written by the preceding version.

If you're stuck on using the Registry or special folders for user settings, you can get version-independent settings in one of two ways. One way is to bypass the path helper properties on the Application object and build your own by omitting the version. For example, you could use a roaming user special folder path in the following format:

 <specialFolder>\<companyName>\<productName> 

Another way to get version-independent settings is to migrate the settings to the new version's path during the installation of the new version. This allows you access to the old version's settings in the new version while still letting you change the format of any settings values that have changed between versions. Of course, it also requires that you write the code to do the migration.

Each of the remaining two forms of settings management ”.config files and isolated storage ”has a built-in form of version independence. Settings in the appSettings portion of the .config file aren't keyed to versions at all, so those values are carried along from version to version (whether or not you want them to be). Isolated storage has two forms of versioned access to settings.

If an assembly is unsigned ” that is, there is no key file associated with it at build time via the assemblywide AssemblyKeyFileAttribute [12] ”then isolated storage is just as version-independent as .config files are (again, whether or not you want it to be). All versions of the assembly will share the same settings.

[12] Why you'd actually want to sign an assembly beyond versioning user settings data for isolated storage is beyond the scope of this book, but it is covered in gory detail in Essential .NET (Addison-Wesley, 2003) by Don Box, with Chris Sells.

Signing an assembly requires first obtaining a key:

 C:\>sn -k key.snk 

After you have a key, you can sign an assembly by passing the name of the key as the value to the AssemblyKeyFileAttribute:

 [assembly: AssemblyKeyFile(@"..\..\key.snk")] 

After an assembly is signed, access to isolated storage is versioned, but only on the major part of the version number. In other words, all 1.x versions of an application will share the same settings, but 2.x versions will need their own settings. This is probably the best of both worlds . Minor versions aren't likely to change the format of existing settings and will at most gain new settings (with appropriate defaults). Major versions are likely to change fundamental things, including the format of settings data, so it makes sense to give a new major version number a clean slate when it comes to settings data. When using isolated storage, you'll still have to move user settings forward during the installation of the new version, but only if it's a major version upgrade, such as 1.1 to 2.0. No migration is needed for an upgrade from 1.1 to 1.2.

Note that this version discussion is about the paths to the data stores themselves . If you're serializing versioned types to an isolated storage stream, for example, you'll still need to deal with standard .NET versioning on those.

Choosing a Settings Mechanism

Table 11.5 summarizes the characteristics of the various mechanisms for retrieving and storing settings for WinForms applications.

Choosing which settings mechanism to use depends on what your needs are. If you have read-only application settings, the .config file is a good choice because it's simple, it has some built-in Designer support, and it works from a partially trusted environment. If you've got user settings, then isolated storage is a good choice because it supports reading and writing, partial trust, and roaming (although not roaming in combination with partial trust), and it has a nice versioning story. Special folders or the Registry is really useful only for legacy applications or read/write settings (which are pretty darn rare).

Table 11.5. Summary of Settings Mechanisms

Mechanism

Localities

Access

Notes

.config files

Application

Read-only

Not versioned

Designer support

Can be used from partially trusted assembly

Registry

Application

Roaming user

Machine

Read/write

Versioned when using Application paths

Special folders

Application

User

Roaming user

Read/write

Versioned when using Application paths

Isolated storage

User

Roaming user

Domain

Read/write

Not versioned when unsigned

Versioned on major version number when signed

Can be used from partially trusted assembly



Windows Forms Programming in C#
Windows Forms Programming in C#
ISBN: 0321116208
EAN: 2147483647
Year: 2003
Pages: 136
Authors: Chris Sells

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net