Click here to Skip to main content
15,860,859 members
Articles / Programming Languages / C#
Article

The very last Configuration Section Handler I hope I'll ever need

Rate me:
Please Sign up or sign in to vote.
4.93/5 (10 votes)
27 Jun 20044 min read 84.7K   278   44   14
Flexible and easy to use configuration section handler with change monitoring

Introduction

XmlSerializerSectionHandler is a flexible configuration section handler that allows you to easily serialize simple settings classes from custom sections of your application's config file. Working in tandem with the XmlSectionSettingsBase, your settings class can have its property values automatically reloaded when the configuration file changes.

Background

The inspiration for the title of this article as well as the original idea for the article itself comes from the excellent article The Last Configuration Section Handler I'll Ever Need written by Craig Andera. I recommend reading his short article before continuing to as this article is an extension of the approach he takes in building a configuration section handler. The feature that my approach adds is the ability for a simple setting class to watch for changes to the config file and update itself accordingly.

Initial Problem

Having read his article, it should be apparent why reloading the settings when the file changes is a challenge. Mainly because you most likely will have a reference to your simple settings object (such as the MyStuff class in the article) which has to somehow be notified that a change has occurred. As you can see from the sample below, classes used to hold settings (such as the MyStuff class) are deliberately kept very simple in order to facilitate writing many of them. There's no facility to watch for changes to the config file.

C#
public class MyStuff 
{ 
  private float foo ; 
  private string bar; 

  public float Foo 
  { 
    get { return foo ; } 
    set { foo = value ; } 
  } 

  public string Bar 
  { 
    get { return bar; } 
    set { bar = value ; 
  } 
} 

The Solution

So after a bit of thought and a bit more beer, I came up with a solution that I believe still retains the simplicity of his approach, while adding the ability to have the settings dynamically get updated when the config file changes. My approach is also backwards compatible with existing setting objects created using his approach. Existing setting objects will not need any changes to work, though they won't get the benefit of this new feature.

Modifying An Exisiting Settings Class

I've extended Craig's solution with the addition of the XmlSectionSettingsBase abstract class. The purpose of this class is to act as the base class for your simple setting object. To add the ability to reload itself, just have your class inherit from XmlSectionSettingsBase and call the UpdateChanges() method before each property getter. As an example, I've modified the above settings object:

C#
public class MyStuff : XmlSectionSettingsBase
{ 
  private float foo ; 
  private string bar; 

  public float Foo 
  { 
    get 
    { 
      UpdateChanges();
      return foo ; 
    } 
    set { foo = value ; } 
  } 

  public string Bar 
  { 
    get 
    { 
      UpdateChanges();
      return bar; 
    } 
    set { bar = value ; 
  } 
} 

Changes To The XmlSerializerSectionHandler Class

That's it! That's all the changes you have to make to the settings class. I've also updated the XmlSerializerSectionHandler class as well. It's now pretty small:

C#
public class XmlSerializerSectionHandler : IConfigurationSectionHandler 
{ 
  public object Create(object parent, object context, XmlNode section)
  {
    return XmlSectionSettingsBase.LoadSettings(section);
  } 
}

The Workhorse

Which leads us to the XmlSectionSettingsBase class which is the workhorse of my scheme. You'll notice that I've removed all the logic from the section handler for loading the settings object from the config file. The reason for this is that the instance of the settings object has to know how to load itself from the config file if its going to watch it for changes. Since I didn't want to duplicate logic, I made that change. I'll outline the class with some broad strokes and let you download the source code for the nitty gritty.

First, let's start with the static LoadSettings method.

C#
public static object LoadSettings(XmlNode section)
{
    object settings = DeserializeSection(section);

    XmlSectionSettingsBase xmlSettings = settings as XmlSectionSettingsBase;
    if(xmlSettings != null)
    {
        xmlSettings._rootName = section.Name;
        ((XmlSectionSettingsBase)settings).WatchForConfigChanges();
    }
    return settings;
}

This method deserializes an instance of your settings object and checks to see if it inherits from XmlSectionSettingsBase. If not, it just returns it, hence the backwards compatibility. Otherwise it calls WatchForConfigChanges which sets up a FileSystemWatcher to monitor for changes to the config file. Also notice that it stores the name of the config section node in a private member (_rootName) of the settings class. This becomes important later when we need to reload the settings.

C#
void WatchForConfigChanges()
{
    FileInfo configFile = new FileInfo(
      AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);
    try
    {
        _watcher = new FileSystemWatcher(configFile.DirectoryName);
        _watcher.Filter = configFile.Name;
        _watcher.NotifyFilter = NotifyFilters.LastWrite;
        _watcher.Changed += new FileSystemEventHandler(OnConfigChanged);
        _watcher.EnableRaisingEvents = true;
    }
    catch(Exception ex)
    {
        Log.Error("Configuration problem.", ex);
        throw new ConfigurationException(
 "An error occurred while attempting to watch for file system changes.", ex);
    }
}

When a change occurs, the OnConfigChanged method is called. This method simply updates a boolean flag. This is the flag that the UpdateChanges method mentioned earlier checks before actually applying changes to the settings object.

C#
void OnConfigChanged(object sender, FileSystemEventArgs e)
{
    _isDataValid = false;
}

protected void UpdateChanges()
{
    if(!_isDataValid)
        ReloadSettings();
}

Now we hit upon the ReloadSettings method. This method uses the name of the node containing the config section we stored earlier (_rootName) in order to load an XmlDocument containing the new settings from the config file.

C#
void ReloadSettings()
{
    XmlDocument doc = new XmlDocument();
    doc.Load(AppDomain.CurrentDomain.SetupInformation.ConfigurationFile);
    XmlNodeList nodes = doc.GetElementsByTagName(_rootName);
    if(nodes.Count > 0)
    {
        //Note: newSettings should not watch for config changes.
        XmlSectionSettingsBase newSettings = 
          DeserializeSection(nodes[0]) as XmlSectionSettingsBase;
        newSettings._isDataValid = true;
        CopySettings(newSettings);
    }
    else
        throw new System.Configuration.ConfigurationException(
            "Configuration section " + _rootName + " not found.");
}

The path to the config file is retrieved via a the AppDomain.CurrentDomain.SetupInformation.ConfigurationFile property. Once we have the section, we can call DeserializeSection to get an instance of our settings class with the new settings. This method is pretty is the same as the Create method in Craig's version.

C#
static object DeserializeSection(XmlNode section)
{
    XPathNavigator navigator = section.CreateNavigator(); 

    string typename = (string)navigator.Evaluate("string(@type)");

    Type type = Type.GetType(typename);
    if(type == null)
        throw new ConfigurationException("The type '" + typename + 
"' is not a valid type. Double check the type parameter.");
    XmlSerializer serializer = new XmlSerializer(type); 

    return serializer.Deserialize(new XmlNodeReader(section));
}

After loading this new settings instance, I overwrite the current instance's property values with the values from the new settings instance via a call to CopySettings.

C#
void CopySettings(object newSettings)
{
    if(newSettings.GetType() != this.GetType())
        return;

    PropertyInfo[] properties = newSettings.GetType().GetProperties();

    foreach(PropertyInfo property in properties)
    {
        if(property.CanWrite && property.CanRead)
        {
            property.SetValue(this, property.GetValue(newSettings, null), null);
        }
    }
}

Copy Settings uses reflection to iterate through all the public get properties of the new settings object and sets the corresponding property of the current instance if the current property can be written to.

And that's the gist of it. Let me know if you find this useful, if you find any bugs, or if you have suggestions for improvement. Just drop me a line in the comments. I created a Visual Studio.NET 2003 solution that contains all the code for the classes I mentioned. Also included is a sample configuration file, settings object, and MSDN style documentation generated via NDoc.

References

History

  • June 25, 2004 - Initial Release.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralSuggestions Pin
sadavoya30-Jun-04 3:47
sadavoya30-Jun-04 3:47 
GeneralRe: Suggestions Pin
haacked30-Jun-04 6:24
haacked30-Jun-04 6:24 
GeneralRe: Suggestions Pin
haacked30-Jun-04 6:46
haacked30-Jun-04 6:46 
GeneralRe: Suggestions Pin
sadavoya30-Jun-04 12:06
sadavoya30-Jun-04 12:06 
GeneralRe: Suggestions Pin
zucchini1-Jul-04 15:01
zucchini1-Jul-04 15:01 
GeneralRe: Suggestions Pin
Roger Willcocks4-Jul-04 10:47
Roger Willcocks4-Jul-04 10:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.