Wednesday, May 5, 2010

Caveats in deriving from System.Configuration.ApplicationSettingsBase

A friend of mine was using System.Configuration.ApplicationSettingsBase to provide a customized settings containing complex type for his application. Complex type is defined as a type whose Type.IsPrimitive is false. In his application he ran into some difficulties in saving the changes. Investigation into his issues leads me to uncover several gotchas in using customising ApplicationSettingsBase, in particular, handling complex type.

To illustrate some of the caveats discussed here I am using the following classes
class Person {
   public String Name { get; set; }
   public DateTime Dob { get; set; }
}

The customised application settings is defined as such:
class MySettings : ApplicationSettingsBase {
  [ UserScopedSetting() ]
  [ DefaultSettingValue( "123" ) ]
  public Int32 SomeNumber {
    get{ return (Int32)this[ "SomeNumber" ]; }
    set{ this[ "SomeNumber" ] = value; }
  }

  [ UserScopedSetting() ]
  public Person Person {
    get{ if( this["Person"] == null )
           this["Person"] = new Person();
         return this["Person"] as Person; }
    set{ this["Person"] = value; }
}

Cavaet 1 - ApplicationSettingsBase special treatment of complex property type

Each settings property is associated with an object of type SettingsPropertyValue, which can be retrieved by ApplicationSettingsBase.PropertyValues[].

The SettingsPropertyValue class has a member called IsDirty and it is this that will decide when if there is anything to save when one calls ApplicationSettingsBase.Save().

You can toggle this flag since it is public and is read-writable. But not wise and it is better to let the framework does the job.

What is interesting is that, when you access a complex property by means of the getter method of the property wrapper, behind the scene (actually in SettingsPropertyValue.PropertyValue of the required complex property) ApplicationSettingsBase.Item[] not only retrieves the property value but because it is a non-primitive type, not a string, not a DateTime, and not null, the call causes the complex property's IsDirty flag to toggle to true, even when just getting the property value.

The following line of code proves this:
  MySettings settings = new MySettings();
  String name = settings.Person.Name; // causing the settings data to be loaded.
  SettingsPropertyValue sv = settings.PropertyValues["Person"];
  Debug.Assert( sv != null );
  Debug.Assert( sv.IsDirty==true, "Set by SettingsPropertyValue.PropertyValue");

However, if you retrieve a primitive type you get a different result as illustrated by this:
  MySettings settings = new MySettings();
  Int32 number = settings.SomeNumber; // causing the settings data to be loaded.
  SettingsPropertyValue sv = settings.PropertyValues["SomeNumber"];
  Debug.Assert( sv != null );
  // SettingsPropertyValue.PropertyValue does not consider primitive type.
  Debug.Assert( sv.IsDirty==false );

I suspect Microsoft has not choice but to take this approach because one cannot foresee what one will do to that complex property object after it is retrieved from the settings. If one simply extracts values from it, then flipping it to a dirty property seems a bit harsh and incorrect. But if one uses that object to modify its state, then preemptively setting the property to a dirty property appears to pay off.

Caveat 2 - Do not cache complex setting object

When using settings containing complex property type, do not cache the property object and using that to modify the settings. This can cause persistence problem as illustrated by the following code fragment:
  MySettings settings = new MySettings();
  Person p = settings.Person;  // this is my cached object.
  SettingsPropertyValue sv = settings.PropertyValue["Person"];
  Debug.Assert( sv!=null );
  Debug.Assert( sv.IsDirty, "Set by SettingsPropertyValue.PropertyValue" );

  // this simulates one modifies the state of the
  // object at some later stage and then saving the settings.
  for(int n=0; n<5;n++) {
    p.Name = "Jack-"+n.ToString();  // L01
    Console.WriteLine("IsDirty before saving is {0}", sv.IsDirty );
    settings.Save();
  }

The output from the above run will produce
IsDirty before saving is True
IsDirty before saving is False
IsDirty before saving is False
IsDirty before saving is False
IsDirty before saving is False

and if you examine the generated user.config, it only contains the first change ("Jack0") and not "Jack4" as one would expect. This is so because the dirty flag stays false after the first save and, assuming all other settings properties have not been modified but the property Person, there is nothing to save in subsequent modification performed via a cached reference object to that property.

In order to be able to save the subsequent changes, one needs to modify line L01 above as follows:
  settings.Person.Name = "Jack-"+n.ToString(); // L02

In line L02, the access to the property is sufficient to toggle the IsDirty flag to true, in accordance to Caveat 1, and will then inform the ApplicationSettingsBase.Save() that something modified needs saving.

Summary

The recommendation is always access your property via the property wrapper in the ApplicationSettingsBase-derived class, which internally accesses the settings property by means of ApplicationSettingsBase.Item[]. Doing so keeps the house keeping data in check and everything should work smooth as designed.

No comments:

Post a Comment