October 24, 2006
Zen of Settings: Part 4
We're now ready to take a look at how to use the System.Configuration. ApplicationSettingsBase class in a .NET 2.0 application. Unlike many .NET classes that you can instantiate and use, the ApplicationSettingsBase class is meant to be derived from and the derivation used in your application. The reason for this requirement is ApplicationSettingsBase's reliance on attributes to designate what settings are to be persisted, their data types, persistence types, and default values. Attributes form the communication channel between a .NET application and ApplicationSettingsBase providing context about the settings that are being manipulated.
Let's take a closer look.
The first concept to absorb when using ApplicationSettingsBase (and one that I touched on earlier) is that settings come in two flavors - a per-user, read/write variety, and a per-application, read-only variety. The former is typically referred to by engineers as user.config after the typical storage location, and the latter as app.config. It's likely that in your early readings on .NET you became acquainted with app.config as an XML formatted file containing interesting directives, commands and other values used by an application. It replaced the concept of a .INI file in earlier technologies with a more robust and maintainable structure by using XML.
However, app.config has a serious drawback you may have picked up on from the description; it is inherently read-only and as such is not a place that you should (although technically possible) write user data into. Even settings which are application (not per-user) specific but which are runtime generated should not be placed in app.config. This led a lot of .NET developers to plead for some place to store such per-user data and undoubtably led to the creation of ApplicationSettingsBase.
Fortunately, ApplicationSettingsBase hides the notion of per-user and per-application settings by the use of two attributes - UserScopedSetting and ApplicationScopedSetting. The former as you might guess is the one used for per-user settings and the latter for per-application settings. To mark a setting as either (but not both) per-user or per-application, you add the attribute to the setting declaration in a class that derives from ApplicationSettingsBase. Let's look at an example:
public class MySettings : ApplicationSettingsBase
{
[UserScopedSetting]
public string MyUserSetting
{
get { return (string)this["MyUserSetting"];
set { (string)this["MyUserSetting"];
}
[ApplicationScopedSetting]
public string MyAppSetting
{
get { return (string)this["MyAppSetting"];
}
}
In this example, I created two settings named MyUserSetting and MyAppSetting - the names are just ones I picked but could be anything desired. Each setting is defined as a typical accessor/mutator method but instead of accessing a local variable as would be typical, they access a dictionary of items provided by the base class (ApplicationSettingsBase). Since the dictionary deals in the base object type, the value has to be casted on return.
Notice the use of the UserScopedSetting and ApplicationScopedSetting value. The ApplicationSettingsBase class will use reflection to interrogate the derived class to find properties which contain either of these attributes. It is interesting to note that you could have tag-along properties and methods in the derived class that don't persist and this is perfectly acceptable.
Since I'm using the default storage provider (XML persistence to user.config) you may notice that I don't have anything yet that indicates where the settings will be stored, or loaded. And I won't as I mentioned last time since this is handled by the default storage provider automatically. The goal with this class was to make your life as a .NET developer as easy as possible when working with settings and much of the plumbing is hidden from you be default.
Now, it may be the case that each of my properties has a value that I would like to be used if the setting does not yet exist (such as after initial installation of my application). Again, this is accomplished with an attribute called DefaultSettingValue. Let's update our example with this new attribute:
public class MySettings : ApplicationSettingsBase
{
[UserScopedSetting]
[DefaultSettingValue("foouser")
public string MyUserSetting
{
get { return (string)this["MyUserSetting"];
set { (string)this["MyUserSetting"];
}
[ApplicationScopedSetting]
[DefaultSettingValue("fooapp");
public string MyAppSetting
{
get { return (string)this["MyAppSetting"];
}
}
The rule is that if the setting cannot be located in storage, ApplicationSettingsBase will call the setter for the property with the default value. This ensures that your properties have some base value when created, but the attribute is not required.
Interestingly, you might be wondering what if the property isn't a string? Well, you can think of the DefaultSettingValue "value" as though the property's ToString() method was called. So an "int" property would have a default value formatted as "5", and a complex data type like "Size" would have a default value formatted as "100,100".
Finally, you might have noticed that the MyAppSetting setting declared with the ApplicationScopedSetting attribute does not have a setter. This is intentional since the value cannot be persisted with the default storage provider; the value is assumed to come from app.config which by definition is intended to be read-only.
Next time, we'll continue looking at this derived class and how to handle custom validation for values loaded at startup, and why you really want to create a base class for all of your settings classes.
Posted by Mark M. Baker at 10:11 AM Permalink
|