Loading from and saving property values to a config file


Not an inspiring title, after all it’s not difficult to read from or save to a configuration file. That’s true, but this post is about a technique that’s useful in many scenarios but especially when using the MVVM pattern. It involves very little code: a custom attribute and three extension methods.

In the System.Configuration namespace Microsoft provides the ApplicationSettingsBase class which is able to load application parameters from a configuration file. However it requires the creation of a separate class (inheriting from ApplicationSettingsBase) just to hold the variables to be loaded which, somehow, must then be loaded into working variables on, say, a form. When working in an MVVM scenario (or System.Windows.Forms.Forms for that matter) it’s handy to be able to load view model properties directly from a configuration and save some back again. It’s especially handy to be able to write directly to properties when working on a Silverlight or WPF application because any notifications based on INotifyPropertyChanged will fire automatically and UI artifacts will update automatically.

This project shows how this objective can be met with just three extension methods (plus some utility ones) and a custom attribute class to declaritively mark the properties in a [form, mvvm or regular] class which are to be loaded from and saved to a configuration file. There’s a sample application which show these operations in action.

Using this technique offers these features and benefits

  • Because there is a tiny amount of code required to enable saving the properties of any class it can be embedded in your application properties don’t have to be public. Properties of any accessibility level can be saved.
  • There’s no separate ‘variables’ class instead the properties of your view model or form are updated directly
  • Theres no serialization involved so no need to mark your classes ISerializable or implement the serialization methods (though there’s nothing to stop you).
  • The name used to record a value in the configuration file does not need to be the same as the property name.
  • Properties can be saved in ‘groups’ so properties for class A can be loaded/saved independly of the properties of any other class.
  • You can declaritively define default values for your properties using System.Component.DefaultAttribute and these will be applied to any property that does not have a persisted value in a configuration file.

So what do you do

Using the code is really simple and can be applied to any project. To begin, add a reference to System.Configuration your project. Then include the ExtensionClass.cs and SavingAttribute.cs classes from the sample project. These three changes add and the minimum code you need.

Next, decorate the properties you want to be able to load and save with the SavingAttribute (the code only works with properties not fields). Only the values of decorated properties will be saved and loaded. You can also mark any properties, not just those to be loaded/saved, with the System.Component.DefaultAttribute and the code will apply the default values automatically as well. Here’s an example of what a decorated property might look like:

1
2
3
4
5
6
7
8
/// <summary>
/// Decorated to receive a default value and be
/// loaded and saved but with a different name
/// </summary>
[Saving(csOptions, "Quattro")]
[DefaultValue("Quattro")]
public string Four
{ get; set; }
/// <summary>
/// Decorated to receive a default value and be
/// loaded and saved but with a different name
/// </summary>
[Saving(csOptions, "Quattro")]
[DefaultValue("Quattro")]
public string Four
{ get; set; }

Finally call the SaveOptions() and RestoreOptions() extension methods. These methods extend object so are available on any class.

Also included is a partial class in MyClass.cs and use it if you want to save a bit of time. It implements methods that use the extension methods and which show how progress can be reported and errors can be captured and displayed. You can copy the two functions it contains into your classes containing properties you want to load and save or you can make your class the inherit from the example – like a base class. However in many scenarios the form class or view model class will already inherit from some ancestor. To accommodate this scenario, the implementation in MyClass.cs is partial class which mean if you rename it to match an existing class the functions will be available.

And how does it work?

The three extension method cover: applying default values (ResetToDefaultValues); if any values have been saved, loading them from a configuration file (RestoreOptions); and saving property values (SaveOptions). They all work by using reflection. Here’s the implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void ResetToDefaultValues(this object source, 
  CallbackHandler progress, ErrorEventHandler errors)
{
  (from pi in source.GetType().GetProperties(bindingFlags)
 
    let defaultItem = pi.GetAttributes<DefaultValueAttribute>(false).FirstOrDefault()
    where defaultItem != null
    let mi = pi.GetSetMethod(true)
    where mi != null
    select new { Key = pi.Name, Info = pi, Method = mi, DefaultValue = defaultItem.Value }
  ).ToList().ForEach(property =>
    {
    try
    {
        if (property.DefaultValue == null) return;
        property.Method.Invoke(source, new object[] { property.DefaultValue });
 
        if (progress == null) return;
        progress(property.Key, property.DefaultValue.ToString());
    }
    catch (Exception ex)
    {
        if (errors == null) return;
        errors(property.Key, new ErrorEventArgs(ex));
    }
  });
 
}
public static void ResetToDefaultValues(this object source, 
  CallbackHandler progress, ErrorEventHandler errors)
{
  (from pi in source.GetType().GetProperties(bindingFlags)

    let defaultItem = pi.GetAttributes<DefaultValueAttribute>(false).FirstOrDefault()
    where defaultItem != null
    let mi = pi.GetSetMethod(true)
    where mi != null
    select new { Key = pi.Name, Info = pi, Method = mi, DefaultValue = defaultItem.Value }
  ).ToList().ForEach(property =>
	{
    try
    {
        if (property.DefaultValue == null) return;
        property.Method.Invoke(source, new object[] { property.DefaultValue });

        if (progress == null) return;
        progress(property.Key, property.DefaultValue.ToString());
    }
    catch (Exception ex)
    {
        if (errors == null) return;
        errors(property.Key, new ErrorEventArgs(ex));
    }
  });

}

Linq is being used to process the properties of the class to find those properties decorated with DefaultAttribute. The default value in the attribute is applied to the property by invoking the ‘set’ method of each decorated property. If a progress callback delegate is provided it is called after each property has been processed and any error is passed to the error callback if one is available. This method sets the pattern. Here is saving the properties of a class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public static void SaveOptions(this object source, 
  KeyValueConfigurationCollection appSettings,
  string saveGroup, CallbackHandler progress, ErrorEventHandler errors)
{
  (from pi in source.GetType().GetProperties(bindingFlags)
   let saveList = pi.GetAttributes<SavingAttribute>(false).ToArray()
   where saveList.Length > 0 && saveList[0].Group == saveGroup
   let mi = pi.GetGetMethod(true)
   where mi != null
   select new { Key = saveList[0].SaveName == null 
      ? pi.Name 
      : saveList[0].SaveName, Info = pi, Method = mi }
  ).ToList().ForEach(property =>
  {
    try
    { 
      object val = property.Method.Invoke(source, new object[0]);
      if (val is string && ((string)val).IsNullOrEmpty())
      {
        // If it used to exist but is now blank, remove it.
        if (appSettings.AllKeys.Contains(property.Key))
          appSettings.Remove(property.Key);
      }
      else
      {
        if (appSettings.AllKeys.Contains(property.Key))
          appSettings[property.Key].Value = val.ToString();
        else
          appSettings.Add(property.Key, val.ToString());
      }
 
      if (progress == null) return;
      progress(property.Key, val.ToString());
    }
    catch (Exception ex)
    {
      if (errors == null) return;
 
      errors(property.Key, new ErrorEventArgs(ex));
    }
  });
public static void SaveOptions(this object source, 
  KeyValueConfigurationCollection appSettings,
  string saveGroup, CallbackHandler progress, ErrorEventHandler errors)
{
  (from pi in source.GetType().GetProperties(bindingFlags)
   let saveList = pi.GetAttributes<SavingAttribute>(false).ToArray()
   where saveList.Length > 0 && saveList[0].Group == saveGroup
   let mi = pi.GetGetMethod(true)
   where mi != null
   select new { Key = saveList[0].SaveName == null 
      ? pi.Name 
      : saveList[0].SaveName, Info = pi, Method = mi }
  ).ToList().ForEach(property =>
  {
    try
    { 
      object val = property.Method.Invoke(source, new object[0]);
      if (val is string && ((string)val).IsNullOrEmpty())
      {
        // If it used to exist but is now blank, remove it.
        if (appSettings.AllKeys.Contains(property.Key))
          appSettings.Remove(property.Key);
      }
      else
      {
        if (appSettings.AllKeys.Contains(property.Key))
          appSettings[property.Key].Value = val.ToString();
        else
          appSettings.Add(property.Key, val.ToString());
      }

      if (progress == null) return;
      progress(property.Key, val.ToString());
    }
    catch (Exception ex)
    {
      if (errors == null) return;

      errors(property.Key, new ErrorEventArgs(ex));
    }
  });

Again Linq is used to query the properties but this time queries for the SavingAttribute. Any properties not decorated or decorated but not with the group being used are discarded. The ‘get’ method is used to retrieve the value of properties to be saved and the value is written to the configuration file settings section using the alternative name provided in the SavingAttribute or the property name if no alternative name has been supplied. Again progress and errors are reported if the caller provided callbacks.

The final and most complicated of the extension methods is the one to restore.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static void RestoreOptions(this object source, 
  KeyValueConfigurationCollection appSettings, 
  string saveGroup, CallbackHandler progress, 
  ErrorEventHandler errors)
{
  (from pi in source.GetType().GetProperties(bindingFlags)
   let saveList = pi.GetAttributes<SavingAttribute>(false).FirstOrDefault()
   where saveList != null && saveList.Group == saveGroup
   from key in appSettings.AllKeys
   where key == (saveList.SaveName.IsNullOrEmpty() ? pi.Name : saveList.SaveName)
   let mi = pi.GetSetMethod(true)
   where mi != null
   select new { Key = key, Info = pi, Value = appSettings[key].ValueOrNull(), Method = mi }
  ).ToList().ForEach(property =>
 {
    try
    {
      object obj = null;
 
      if (TypeDescriptor.GetAttributes(property.Info.PropertyType).Count == 0)
      {
        ConstructorInfo ctor = property.Info.PropertyType.GetConstructor(new Type[] { typeof(string) });
        obj = ctor.Invoke(new object[] { property.Value });
      }
      else obj = property.Info.PropertyType.ConvertFrom(property.Value);
 
      if (obj != null) property.Method.Invoke(source, new object[] { obj });
      if (progress == null) return;
 
      progress(property.Info.Name, property.Value);
    }
    catch (Exception ex)
    {
      if (errors == null) return;
      errors(property.Key, new ErrorEventArgs(ex));
    }
  });
}
public static void RestoreOptions(this object source, 
  KeyValueConfigurationCollection appSettings, 
  string saveGroup, CallbackHandler progress, 
  ErrorEventHandler errors)
{
  (from pi in source.GetType().GetProperties(bindingFlags)
   let saveList = pi.GetAttributes<SavingAttribute>(false).FirstOrDefault()
   where saveList != null && saveList.Group == saveGroup
   from key in appSettings.AllKeys
   where key == (saveList.SaveName.IsNullOrEmpty() ? pi.Name : saveList.SaveName)
   let mi = pi.GetSetMethod(true)
   where mi != null
   select new { Key = key, Info = pi, Value = appSettings[key].ValueOrNull(), Method = mi }
  ).ToList().ForEach(property =>
 {
    try
    {
      object obj = null;

      if (TypeDescriptor.GetAttributes(property.Info.PropertyType).Count == 0)
      {
        ConstructorInfo ctor = property.Info.PropertyType.GetConstructor(new Type[] { typeof(string) });
        obj = ctor.Invoke(new object[] { property.Value });
      }
      else obj = property.Info.PropertyType.ConvertFrom(property.Value);

      if (obj != null) property.Method.Invoke(source, new object[] { obj });
      if (progress == null) return;

      progress(property.Info.Name, property.Value);
    }
    catch (Exception ex)
    {
      if (errors == null) return;
      errors(property.Key, new ErrorEventArgs(ex));
    }
  });
}

Again, Linq is used to process the properties of the class and again the list is restricted to those decorated by SavingAttribute. But the list is further restricted by joining the properties list with the settings from the configuration file so that an attempt is made to update only those properties which are represented in the configuration file. Values from the configuration file are text representations and the code attempts to convert the string to: value, reference, nullable and enumeration types.

Somehow, custom reference types need to be constructed from their text representation. Two options are available: create a constructor which takes a single string argument; or implement a TypeConverter. You can see the application of these two alternatives in the RestoreOptions extension method. Also, the example project includes an example of both mechanism at work.

IEnumerable types are not supported directly. These types need to be serialized and deserialized so it is upto the implementer to provide a corresponding class with a single string constructor or implement a TypeConverter.

Information and Links

Join the fray by commenting, tracking what others have to say, or linking to it from your blog.


Other Posts

Write a Comment

Take a moment to comment and tell us what you think. Some basic HTML is allowed for formatting.

Reader Comments

Be the first to leave a comment!