Q. I have a C# Winforms application which uses the .config file for application settings, etc. I’d like to programmatically maintain various settings in this file, but there's no API like the old VB6 "GetPrivateProfileString." One of my co-workers said it's now preferred to use the registry and not .ini files. How do you suggest I handle this?

Asked by Anne Brumme. Answered by the Wonk on January 30, 2003

A.

.ini files provided a simple way to keep per-application settings. However, when Windows got multi-user support, per-application settings weren't enough, we also needed per-user settings. At the same time that Windows started supporting multiple users, it also provided the Registry for both per-application and per-user settings.

However, recently the Registry has fallen out of favor, mostly due to problems with corruption and an editor that puts every application-specific setting at risk. Towards that end, .NET provides two new mechanisms for dealing with settings, .config files and isolated storage.

.config Files

As a read-only, per-application replacement for .ini files, .NET provides .config files. A .config file is a file placed in the same folder as the application and named just like 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 CLR kinds of things, like how to resolve assemblies and versioning. You can add a new .config file to your VS.NET project by right clicking on the project in the Solution Explorer and choosing Add->Add New Item->Text File and naming the file “app.config” (no quotes). This will add an empty .config file to your project and, when your project is built, copy and rename the app.config file to the output folder along side your application. A minimal .config file looks like this:

 

<configuration>

</configuration>

 

In addition to the CLR-specific settings, .config files can be extended with custom XML sections as designated with uniquely named elements. By convention, the custom section is usually represented with an XML 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 isn’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 system-wide machine.config, a 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. The NameValueFileSectionHandler implementation knows how to read a section in the appSettings format and return a NameValueCollection from the System.Collections.Specialized namespace. However, instead of creating a NameValueFileSectionHandler implementation yourself, you’re better off to use the ConfigurationsSettings class (from the System.Configuration namespace) to map the name of the section to a section reader for you, putting out the NameValueCollection from the result, like so:

 

using System.Configuration;

using System.Collections.Specialized;

...

static void Main() {

NameValueCollection settings =

(NameValueCollection)ConfigurationSettings.GetConfig("appSettings");

 ...

}

 

The ConfigurationSettings class find the appropriate section handler, which will look in the current app configuration data for the appSettings section (parts of which can be inherited from machine.config), parse the contents, build the NameValueCollection and return 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 short-cut, 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"]);

}

 

Once 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's not much good as a string), you can parse the string manually using the type in question or 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));

 

  ...

}

Isolated Storage

While .config files are good for read-only application settings, for read/write user settings, .NET provides 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, 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 of streams and sub-containers 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 sub-folders and files.

 

The special folder you get depends on the scope you specify when getting the store you want to work with. The scope is specified 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 at least be scoped by assembly and user, which means that there are no user-neutral application settings available from isolated storage, just user and roaming user settings (depending on whether the Roaming flag is used or not). In addition, settings can be scoped to a .NET AppDomain using the Domain flag, but that’s not typically useful in a WinForms application. The valid combinations of scope flags as related to settings localities and example folder roots under Windows XP are shown in Table 1.

 

IsolatedStorageScope Flags

Locality

Folder Root

Assembly, User

User

C:\Documents and Settings\csells\Local Settings\Application Data\IsolatedStorage

Assembly, User, Roaming

Roaming User

C:\Documents and Settings\csells\Application Data\IsolatedStorage

Assembly, User, Domain

Domain

C:\Documents and Settings\csells\Local Settings\Application Data\IsolatedStorage

Assembly, User, Domain, Roaming

Roaming User Domain

C:\Documents and Settings\csells\Local Settings\Application Data\IsolatedStorage

Table 1: Isolated storage scope, locality and folder roots

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);

 

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();

 

Once you’ve got the store, you can treat it like a container of streams and sub-containers 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 that the IsolatedStorageFile class is actually implemented as a directory the file system. This is but one implementation of the IsolatedStorageStorage abstract base class on the underlying OS's file system. 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 the IsolatedStorageFileStream. The IsolatedStorageFileStream class is just another implementation of the virtual methods of the FileStream to hide the details of the underlying implementation. Once you've got the stream, you can write to it or read from it just as if you'd opened it as a file yourself. Here's the code 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( IsolatedStorageFileStream stream =

            new IsolatedStorageFileStream("MainForm.txt",

            FileMode.Create,

            store) )

  using( StreamWriter writer = new StreamWriter(stream) ) {

    // 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));

  }

}

 

// Convert an object to a string

string ToString(object obj) {

  TypeConverter converter =

    TypeDescriptor.GetConverter(obj.GetType());

  return converter.ConvertToString(obj);

}

 

void MainForm_Load(object sender, EventArgs e) {

  // Restore the form's position

  try {

    IsolatedStorageFile store =

      IsolatedStorageFile.GetUserStoreForAssembly();

    using( IsolatedStorageFileStream stream =

              new IsolatedStorageFileStream("MainForm.txt",

              FileMode.Open,

              store) )

    using( StreamReader reader = new StreamReader(stream) ) {

      // 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 ) {}

}

 

// Convert a string to an object

object FromString(object obj, Type type) {

  TypeConverter converter = TypeDescriptor.GetConverter(type);

  return converter.ConvertFromString(obj.ToString());

}

 

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. Besides remembering to restore the window state before saving the location and the client size, notice the use of the ToString helper function. Likewise, when the form is loaded again, it reads the settings from isolated storage using a FromString helper that converts back from a string to an object. These helper functions use a type converter, which is a helper for a type to aid in the conversion between instances of the type and strings, which is important for saving settings in a text format. Any type can have an associated type converter and most of the simple ones do.

 

How I Figured This Out

How applications store settings between sessions has evolved greatly over the years. I was there when .ini files were the rage, then the Registry, then special folders and now .config and isolated storage has come along. I found out how each worked by experimenting with the formats, trying out things that I know I want to do, reading the documentation for the various APIs provided by each mechanism, writing a bunch of sample code and asking questions.

 

Based on these experiments, I still don't think that things are perfect. There should be a user-neutral application story for isolated storage as well as user-specific settings, for example. Also, there should be a roaming implementation that doesn't depend on an appropriately configured LAN, which doesn't allow me to roam between work and home. But, as my friend Don used to say, "the largest room in the world is the room for improvement." : )

 

Where Are We?

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. If you've got user settings, than isolated storage is a good choice because it supports reading and writing, partial trust, roaming (although not roaming in combination with partial trust) and has a nice versioning story. The Registry is really only useful for legacy applications or read/write application settings (which are pretty darn rare).

 

Feedback Response

Anne Brumme responded: "Thanks for the thorough answer. So IsolatedStorage provides a assembly-user-specific read/write wrapper on a free form text file? I was hoping for some simple name=value handling ala SetPrivateProfileString, but I guess I have to invent my own format for these items. Am I missing something obvious here?"

 

That's a darn good question, Anne. It turns out that what you do with an isolated storage stream isn't any different than what you do with any other stream when serializing types. And that's such I good question that I dedicated an entire response to answering just that.

Feedback

I have feedback on this Ask The Wonk answer