Click here to Skip to main content
Click here to Skip to main content

Easier .NET settings management

By , 4 Feb 2013
 

Introduction       

A common requirement for both web and desktop applications is persisting some elements of the application state between work sessions. The user starts up an application, inputs some data, changes some settings, moves and resizes windows and then closes the application. The next time they start the application it would be very nice if the settings they entered were remembered, and UI elements showed up as they were before the application was closed.

This requires the application to persist this data (most likely in a file) before it shuts down, and applies it when it starts up again. This data could include locations and sizes of movable and resizable UI elements, user input (for example last entered username), as well as application settings and user preferences. 

After coming across this requirement quite a few times I decided to invest some time and make a reusable library that automates most of the work of persisting and applying settings, thus making life easier in the long run. 

In this article I present the solution I came up with and describe what it can do, what value it provides, how to use it and the basic ideas behind it. 

Platforms 

This library can be used for WPF, Windows Forms and ASP.NET application. The required version of .NET is 3.5 SP1 or higher. 

Reasoning and motivation behind this 

The usual approach to persisting settings in a .NET application would be to use .config and .settings files via the built in configuration API. It allows for type safe access to configuration data, defining complex configuration settings, separation of user-level and application-level settings, run-time reading and writing, as well as manual modification of the settings via an XML editor.    

It does however involve a little too much ceremony in my opinion, with stuff like sub-classing ConfigurationSection for complex settings and hacking when handling plug-ins with their own settings. Also, (to my knowledge) the Visual Studio tool that generates settings classes does not allow you to intervene in what it generates (suppose you want to implement INotifyPropertyChanged in your settings class).

But the biggest problem is that maintaining and using a large set of properties this way is tedious. The settings objects are usually not the ones that use the data, they just store it. This means that to use this data you must write code that copies the data from settings to the concerned object and later writes updated data back again to the settings before the application closes. 

Let me illustrate with an example: Suppose your application has several resizable and movable UI elements, and you want to remember and apply these sizes and locations the next time the application starts. Suppose you have 10 such UI elements, and for each of those you want to persist 4 properties (“Height”, “Width”, “Left”, “Top”) - a total of 40 properties just for this. You could add all those properties to your settings file, and write code that applies them to the corresponding UI element, than write additional code that updates the settings before the application closes. But manually adding settings and writing that code would be rather tedious and error prone. It would be much nicer if we could just declare that we want certain properties of certain objects tracked and have it taken care of more-or-less automatically. 

The main purpose of this library is just that - to enable you to persist and apply data directly on the object that uses it, and to do so in a declarative manner with minimal coding. In the following chapters I demonstrate the use of the library, and discuss it's implementation. 

Terminology 

In this article I use two terms which I think might need explaining: 

  • tracking a property - saving a property's value before the application shuts down, and re-applying the saved value once the application starts again. 
  • persistent  property - a property that is being tracked 

Usage 

The SettingsTracker is the main class in the library. When creating it, you need to tell it how to serialize data and where to store it. This is done by providing it with implementations of the ISerilizer and IDataStore interfaces. Let's take a look at how to create a SettingsTracker that uses a file to store and retrieve persistent data: 

string settingsFilePath = Path.Combine(Environment.GetFolderPath(
  Environment.SpecialFolder.ApplicationData), @"VendorName\AppName\settings.xml");
ISerializer serializer = new BinarySerializer(); //use binary serialization
IDataStore dataStore = new FileDataStore(localSettingsPath); //use a file to store data
SettingsTracker tracker = new SettingsTracker(dataStore, serializer); //create our settings tracker 

All the classes and interfaces in this block of code will be explained in more detail later in this article. For now it's important to note that we now have a SettingsTracker instance which automates tracking properties. It uses binary serialization to serialize data, and stores the data in a file. We should make this instance available to the rest of the application preferably by storing it in an IOC container, or for the sake of simplicity perhaps via a public static property. All we need to do now is to tell it which properties of which object to track. There are several ways of doing this.  

Example scenario 1: Persisting a WPF window location and size

The purpose of this library is best illustrated by an example. Consider the scenario where you want to track the location, size and WindowState of a WPF window. The work you would need to do if you were using a .settings file is shown on the left, while the code you would need to write with this library to achieve the same effect is shown on the right:

A) Using a .settings file 


Step 1: define a setting for each property of the main window 

 

Step 2: apply stored data to the window properties 

public MainWindow()
{
    InitializeComponent();

    this.Left = MySettings.Default.MainWindowLeft;
    this.Top = MySettings.Default.MainWindowTop;
    this.Width = MySettings.Default.MainWindowWidth;
    this.Height = MySettings.Default.MainWindowHeight;
    this.WindowState = MySettings.Default.MainWindowWindowState;
} 

Step 3: persist updated data before the window is closed

protected override void OnClosed(EventArgs e)
{
    MySettings.Default.MainWindowLeft = this.Left;
    MySettings.Default.MainWindowTop = this.Top;
    MySettings.Default.MainWindowWidth = this.Width;
    MySettings.Default.MainWindowHeight = this.Height;
    MySettings.Default.MainWindowWindowState = this.WindowState;

    MySettings.Default.Save();

    base.OnClosed(e);
}    

B) Using this library


 Steps 1 and 2: configure tracking and apply state... and we're done. 

public MainWindow()
{
    InitializeComponent();

    //1. set up tracking for the main window
    Services.Tracker.Configure(this)
        .AddProperties(()=>Height, ()=>Width, ()=>Left, ()=>Top, ()=>WindowState)
        .SetKey("MainWindow")
        .SetMode(PersistModes.Automatic);

    //2. apply persisted state to the window
    Services.Tracker.ApplyState(this);
}   

In this example the static property Services.Tracker holds a SettingsTracker instance. This is for simplicity sake, a better way would be to keep the instance in an IOC container and resolve it from there. 


The amount of work required for option A is quite substantial, even for a single window. Most likely it would be done using copy-paste and would be quite error prone and tedious work. If we had to track many controls throughout the application, the .settings file and intellisense would quickly become cluttered with a jungle of similarly named properties.  

In option B we just declare which properties of the main window we want to be persistent, and give the main window a tracking identifier so we don't mix it's properties up with properties of some other object. Calling ApplyState applies previously persisted data (if any) to the window, while new data is autmatically persisted to the store before the application closes. No writing code that copies data back and forth.

Example scenario 2: Persisting application settings (configuring tracking via Attributes)

Tracking properties of an object can be configured at the instance level (like in the previous example), but it can also be configured at the class level (for all instances of a class) via attributes, provided we control the source code of the class. The [Trackable] attribute, when applied to a property, specifies that a property should be tracked. When applied to a class this attribute specifies that all public properties of the class should be tracked (specific properties can be excluded by decorating them with [Trackabe(false)]). Suppose you want to use an instance of the following class to hold your application's settings:

[Trackable]//applied to class - all properties will be tracked
public class GeneralSettings
{
    public int AppSetting1 { get; set; }
    public string AppSetting2 { get; set; }
    public bool AppSetting3 { get; set; }
}  

In a common scenario, there would only be one instance of this class, and it would also be available to the rest of the application as a singleton (public static property) or through an IOC container. When the application starts we only need to write the following two lines of code to handle persistence of the properties of this object:  

Services.Tracker.Configure(generalSettingsInstance).SetKey("settings").AddMetaData();//add properties based on the use of [Trackable]
Services.Tracker.ApplyState(generalSettingsInstance);

Since we used [Trackable] to specify what we want tracked, we don't need to specify the properties using AddProperties(prop1...propN), we just call AddMetaData() instead.

This is a total of 3 lines of code to make all the properties of this object persistent: one line for the attribute, one line to configure tracking, and one to apply any previous state. 

Since this library can track any object, our settings object can be POCO, it can subclass whatever we like, and implement interfaces as we see fit (e.g., INotifyPropertyChanged).  

For extra coolness, if we use an IOC container to build up our objects, we can make tracking even more concise. Most IOC containers allow you to add custom steps when injecting an object with dependencies. We can use this to add tracking automatically to any object that has [Trackable] attributes applied. In that case, all a class needs to do to have its properties persisted is apply tracking attributes to itself and/or it's properties. The rest of the work will be done automatically by the extension we added to the IOC container.  

Benefits 

So what are the benefits of all this? Well there are several:    

  • it's lets you write less code - you just specify what properties of what object you want to track, you don't need to write code that copies values property-by-property back and forth from settings to other objects  
  • you don't have to explicitly add new properties in the .config or .settings file (and you don't have to come up with a name for each property of each object you want to persist)  
  • you specify the list of properties just once (when configuring tracking), instead of three times (when defining the settings in a .config or .settings file. when copying data from settings, and later when copying data back to settings)    
  • it's declarative - you can use attributes (Trackable and TrackingKey) to configure tracking 
  • if using an IOC container, even less code - you can apply tracking with virtually no code (aside from attributes on appropriate properties) - more on this in the "IOC integration" chapter     
For details on how all this is implemented, and how it can be used and customized, please read on...  

The implementation  

As with any complex problem, a sensible way to approach it would be to break it down into simple components. My approach here uses two basic components: serialization, and data storing mechanisms. These are the basis of my persistence library. Here is the class diagram of the library: 

Building block 1 - Serialization     

OK, so first things first - in order to store any data, we need to be able to convert the data into a persistable format. The obvious candidates for this format would be a string and a byte array. Byte array seems to be the lowest common denominator for data so I would suggest we use that. Let's declare the interface for serializers:  

public interface ISerializer 
{ 
    byte[] Serialize(object obj);
    object Deserialize(byte[] bytes);
}  

Each class that implements this interface represents a mechanism of turning an object into a byte array and vice versa. Now let’s create a simple implementation of this interface:  

public class BinarySerializer : ISerializer
{
    BinaryFormatter _formatter = new BinaryFormatter();
 
    public byte[] Serialize(object obj)
    {
        using (MemoryStream ms = new MemoryStream())
        {
            _formatter.Serialize(ms, obj);
            return ms.GetBuffer();
        }
    }
 
    public object Deserialize(byte[] bytes)
    {
        using (MemoryStream ms = new MemoryStream(bytes))
        {
            return _formatter.Deserialize(ms);
        }
    }
}

There we go. Now we have a class which can take on object graph and turn it into a series of bytes. Serialization is tricky business though, and regarding this implementation I should note that the use of BinaryFormatter does impose certain limitations: serialized classes must be decorated with the [Serializable] attribute, events must be explicitly ignored (via [field:NonSerialized] attribute), complex object graphs with circular references may break the serialization. That being said I have used this implementation in my own projects in several different scenarios and have yet to run into serious issues. One potential big issue is the Compact framework - the BinaryFormatter class is not supported on it, so this implementation cannot be used on it. Other implementations of the ISerializer interface might for example use:

  • SoapFormatter
  • TypeConverter based solutions 
  • protobuf.net (a very good free serialization library that also works on .NETCF and is really fast)  
  • custom solutions

Building block 2 - DataStore    

Now that we can turn an object into a series of bytes we need to be able to store the serialized data into a persistent location. We can declare our interface for data stores as follows:   

public interface IDataStore 
{
    byte[] GetData(string identifier);
    void SetData(byte [] data, string identifier);
}    

Like the ISerilizer interface, this interface is also rather minimal. Classes implementing it enable us to store and retrieve (named) binary data to/from a persistent location. Candidate locations to persist data might include: 

  • file system (current application directory, %appsettings%, %allusersprofile%), 
  • registry (I would not recommend this due to access rights issues) 
  • database   
  • cookie  
  • other      

The implementation I am using here stores the data in an XML file - each entry is stored as a Base64 encoded string inside an XML tag with an Id attribute. Here is the code for the implementation:  

public class FileDataStore : IDataStore
{
    XDocument _document;
 
    const string ROOT_TAG = "Data";
    const string ITEM_TAG = "Item";
    const string ID_ATTRIBUTE = "Id";
 
    public string FilePath { get; private set; }
 
    public FileDataStore(string filePath)
    {
        FilePath = filePath;
 
        if (File.Exists(FilePath))
        {
            _document = XDocument.Load(FilePath);
        }
        else
        {
            _document = new XDocument();
            _document.Add(new XElement(ROOT_TAG));
        }
    }
 
    public byte[] GetData(string identifier)
    {
        XElement itemElement = GetItem(identifier);
        if (itemElement == null)
            return null;
        else
            return Convert.FromBase64String((string)itemElement.Value);
    }
 
    public void SetData(byte[] data, string identifier)
    {
        XElement itemElement = GetItem(identifier);
        if (itemElement == null)
        {
            itemElement = new XElement(ITEM_TAG, new XAttribute(ID_ATTRIBUTE, identifier));
            _document.Root.Add(itemElement);
        }
 
        itemElement.Value = Convert.ToBase64String(data);
        _document.Save(FilePath);
    }
 
    private XElement GetItem(string identifier)
    {
        return _document.Root.Elements(ITEM_TAG).SingleOrDefault(el => (string)el.Attribute(ID_ATTRIBUTE) == identifier);
    }
 
    public bool ContainsKey(string identifier)
    {
        return GetItem(identifier) != null;
    }
} 

Depending on the location of the file we choose to use, the data will be persisted in a user specific location or a global location.  For instance if the file is located somewhere under %appsettings% it will be user specific, while if it is located under %allusersprofile% it will be global for all users. 

So now we can take an object, get its binary representation, and store that in a persistent store. These are all the building blocks we need. Let's move on and see how we can use them.

* %appsettings% and %allusersprofile% refer to environment variables.

ObjectStore class  

Using these two building blocks, we can easily create a class which can store and retrieve entire objects - an object store. To distinguish between objects in the store we need to provide an identifier for the object when storing/retrieving it. The code for the object store class looks like this: 

public class ObjectStore
{
    IDataStore _dataStore;
    ISerializer _serializer;
 
    Dictionary<string, object> _createdInstances = new Dictionary<string, object>();
 
    public ObjectStore(IDataStore dataStore, ISerializer serializer)
    {
        _dataStore = dataStore;
        _serializer = serializer;
    }
 
    public void Persist(object target, string key)
    {
        _createdInstances[key] = target;
        _dataStore.SetData(_serializer.Serialze(target), key);
    }
 
    public bool ContainsKey(string key)
    {
        return _dataStore.ContainsKey(key);
    }
 
    public object Retrieve(string key)
    {
        byte[] data = _dataStore.GetData(key);
        if (!_createdInstances.ContainsKey(key))
            _createdInstances[key] = _serializer.Deserialize(data);
        return _createdInstances[key];
    }
} 

The implementation of the ObjectStore is pretty straightforward. It will use any implementation of ISerializer and IDataStore you give it (those familiar with DI/IOC will recognize constructor injection). One more thing you have perhaps noticed is the dictionary which is there to handle object identity (1 key = 1 object) and caching.   

So, instances of this class can save entire objects in a persistent location. This can be rather handy on its own, but we can do more...   

SettingsTracker class     

Suppose we want to persist the size and location of the main window of our application. It would not make sense to persist an entire window object just to maintain its size and location (even if it could be done).  Instead we have to track just the values of specific properties.   

As you may expect by its name, the SettingsTracker class is the one that orchestrates the tracking of the properties of objects. This class uses the previously described ObjectStore to store and retrieve the values of tracked properties.     

To track your object you must first tell the SettingsTracker instance what properties of the target you want to track, and when to persist those properties to the store. To accomplish this you must call the Configure(object target) method. This method returns a TrackingConfiguration object which you use to specify how to track your object.     

Here is an example showing how to configure persisting the size and location of a window:    

public MainWindow(SettingsTracker tracker)
{
    InitializeComponent();
 
    //configure tracking of the main window
    tracker.Configure(this)
        .AddProperties("Height", "Width", "Left", "Top", "WindowState")
        .SetKey("TheMainWindowKey")
        .SetMode(PersistModes.Automatic);
 
    //apply persisted state to the window
    tracker.ApplyState(this);
 
    
    //...
} 

Here we fetch the configuration for tracking our window, we tell it which properties to persist, we specify the identifier (key) for the target object, and lastly we specify automatic mode – persist the properties just before the application closes. If (like me) you feel uneasy about using hard coded strings when specifying properties, you can instead use the other overload of the AddProperties method like so:    

AddProperties(()=>Height, ()=>Width, ()=>Left, ()=>Top, ()=>WindowState);  

This overload analyzes the expression trees to determine the correct properties, thus eliminating the need for hard coded strings.   

The SettingsTracker stores a list of all TrackingConfiguration objects it creates. It makes sure that there is exactly one configuration object per target, so each time you call Configure() for the same target, you always get the same TrackingConfiguration object.    

Applying state: After you have configured what properties you want to track, you can apply any previously persisted state to your those properties by calling the tracker.ApplyState(object target) method.  

Storing state: In the configuration, you can set the tracking mode to be manual or automatic. If you have chosen the automatic tracking mode (this is the default), the values of the target's properties will be stored just before the application closes (or before the session ends for web apps). If, instead, you want to store them at some earlier time, use manual mode, and explicitly call the tracker.PersistState(object target) method when appropriate.     

When persisting a target object's properties, the settings tracker will: 

  1. locate the TrackingConfiguration for the target  
  2. for each property that is specified in the target's configuration:
    1. construct a key by concatenating the target object type, the target's tracking key, and the property name ([TargetObjetType]_[TargetObjectKey].[PropertyName])
    2. get the value of the property using reflection, and save it to the store using the constructed key as the identifier.  

So for the window in the previous example the PersistState method would store 5 objects to the ObjectStore and the keys would be: 

  • DemoTracking.MainWindow_TheMainWindowKey.Height
  • DemoTracking.MainWindow_TheMainWindowKey.Width
  • DemoTracking.MainWindow_TheMainWindowKey.Left
  • DemoTracking.MainWindow_TheMainWindowKey.Top
  • DemoTracking.MainWindow_TheMainWindowKey.WindowState

Note: Since there will only ever be one instance of the MainWindow class in the application, we didn't really have to specify the key for the window object (using the SetKey method) since it is already uniquely identified by it's class name.  

The ApplyState method does almost the same thing as PersistState but moves the data in the opposite direction, from the store to the object's properties.   

Ok, let's get back to the code, the following is the code for the TrackingConfiguration class: 

public enum PersistModes
{
    /// <summary>
    /// State is persisted automatically upon application close
    /// </summary>
    Automatic,
    /// <summary>
    /// State is persisted only upon request
    /// </summary>
    Manual
}

public class TrackingConfiguration
{
    public string Key { get; set; }
    public HashSet<string> Properties { get; set; }
    public WeakReference TargetReference { get; set; }
    public PersistModes Mode { get; set; }

    public TrackingConfiguration(object target)
    {
        this.TargetReference = new WeakReference(target);
        Properties = new HashSet<string>();
    }

    /// <summary>
    /// Based on Trackable and TrackingKey attributes, adds properties
    /// and setts the key.
    /// </summary>
    /// <returns></returns>
    public TrackingConfiguration AddMetaData()
    {
        PropertyInfo keyProperty = TargetReference.Target
            .GetType()
            .GetProperties()
            .SingleOrDefault(pi => pi.IsDefined(typeof(TrackingKeyAttribute), true));
        if (keyProperty != null)
            Key = keyProperty.GetValue(TargetReference.Target, null).ToString();

        //see if TrackableAttribute(true) exists on the target class
        bool isClassMarkedAsTrackable = false;
        TrackableAttribute targetClassTrackableAtt = 
          TargetReference.Target.GetType().GetCustomAttributes(true).OfType<TrackableAttribute>().FirstOrDefault();
        if (targetClassTrackableAtt != null && targetClassTrackableAtt.IsTrackable)
            isClassMarkedAsTrackable = true;

        //add properties that need to be tracked
        foreach (PropertyInfo pi in TargetReference.Target.GetType().GetProperties())
        {
            TrackableAttribute propTrackableAtt = 
              pi.GetCustomAttributes(true).OfType<TrackableAttribute>().FirstOrDefault();
            if (propTrackableAtt == null)
            {
                //if the property is not marked with Trackable(true), check if the class is
                if(isClassMarkedAsTrackable)
                    AddProperties(pi.Name);
            }
            else
            {
                if(propTrackableAtt.IsTrackable)
                    AddProperties(pi.Name);
            }
        }
        return this;
    }

    public TrackingConfiguration AddProperties(params string[] properties)
    {
        foreach (string property in properties)
            Properties.Add(property);
        return this;
    }
    public TrackingConfiguration AddProperties(params Expression<Func<object>>[] properties)
    {
        AddProperties(properties.Select(p => ((p.Body as 
          UnaryExpression).Operand as MemberExpression).Member.Name).ToArray());
        return this;
    }
        
    public TrackingConfiguration RemoveProperties(params string[] properties)
    {
        foreach (string property in properties)
            Properties.Remove(property);
        return this;
    }
    public TrackingConfiguration RemoveProperties(params Expression<Func<object>>[] properties)
    {
        RemoveProperties(properties.Select(p => ((p.Body as 
          UnaryExpression).Operand as MemberExpression).Member.Name).ToArray());
        return this;
    }

    public TrackingConfiguration SetMode(PersistModes mode)
    {
        this.Mode = mode;
        return this;
    }

    public TrackingConfiguration SetKey(string key)
    {
        this.Key = key;
        return this;
    }
}

This class uses method chaining, where each method returns the concerned object thus facilitating further method calls. The implementation is mostly straightforward. One thing to mention is the AddMetaData method - it is used when tracking is configured via attributes.  

Note that the configuration object stores a WeakReference to the target so it does not make it live longer than it needs to.

And here is the code for the SettingsTracker class:   

public class SettingsTracker
{
    List<TrackingConfiguration> _configurations = new List<TrackingConfiguration>();

    ObjectStore _objectStore;
    public SettingsTracker(ObjectStore objectStore)
    {
        _objectStore = objectStore;
        WireUpAutomaticPersist();
    }

    #region automatic persisting
    protected virtual void WireUpAutomaticPersist()
    {
        if (System.Windows.Application.Current != null)//wpf
            System.Windows.Application.Current.Exit += (s, e) => { PersistAutomaticTargets(); };
        else //winforms
            System.Windows.Forms.Application.ApplicationExit += (s, e) => { PersistAutomaticTargets(); };
        //todo: add support for asp.net (catch Session.End event)
    }

    protected void PersistAutomaticTargets()
    {
        foreach (TrackingConfiguration config in _configurations.Where(
                  cfg => cfg.Mode == PersistModes.Automatic && cfg.TargetReference.IsAlive))
            PersistState(config.TargetReference.Target);
    }
    #endregion

    public TrackingConfiguration Configure(object target)
    {
        TrackingConfiguration config = FindExistingConfig(target);
        if (config == null)
        {
            config = new TrackingConfiguration(target);
            _configurations.Add(config);
        }
        return config;
    }

    public void ApplyState(object target)
    {
        TrackingConfiguration config = FindExistingConfig(target);
        Debug.Assert(config != null);

        ITrackingAware trackingAwareTarget = target as ITrackingAware;
        if ((trackingAwareTarget == null) || trackingAwareTarget.OnApplyingState(config))
        {
            foreach (string propertyName in config.Properties)
            {
                PropertyInfo property = target.GetType().GetProperty(propertyName);
                string propKey = ConstructPropertyKey(target.GetType().FullName, config.Key, property.Name);
                try
                {
                    if (_objectStore.ContainsKey(propKey))
                    {
                        object storedValue = _objectStore.Retrieve(propKey);
                        property.SetValue(target, storedValue, null);
                    }
                }
                catch
                {
                    Debug.WriteLine("Applying of value '{propKey}' failed!");
                }
            }
        }
    }

    public void PersistState(object target)
    {
        TrackingConfiguration config = FindExistingConfig(target);
        Debug.Assert(config != null);

        ITrackingAware trackingAwareTarget = target as ITrackingAware;
        if ((trackingAwareTarget == null) || trackingAwareTarget.OnPersistingState(config))
        {
            foreach (string propertyName in config.Properties)
            {
                PropertyInfo property = target.GetType().GetProperty(propertyName);

                string propKey = ConstructPropertyKey(target.GetType().FullName, config.Key, property.Name);
                try
                {
                    object currentValue = property.GetValue(target, null);
                    _objectStore.Persist(currentValue, propKey);
                }
                catch 
                {
                    Debug.WriteLine("Persisting of value '{propKey}' failed!");
                }
            }
        }
    }

    #region private helper methods
        
    private TrackingConfiguration FindExistingConfig(object target)
    {
        //.TargetReference.Target ---> (TrackedTarget).(WeakReferenceTarget)
        return _configurations.SingleOrDefault(cfg => cfg.TargetReference.Target == target);
    }

    //helper method for creating an identifier from the object type, object key, and the propery name
    private string ConstructPropertyKey(string targetTypeName, string objectKey, string propertyName)
    {
        return string.Format("{0}_{1}.{2}", targetTypeName, objectKey, propertyName);
    }
    #endregion
} 

Depending on the context (WinForms, WPF, ASP.NET), the WireUpAutomaticPersist method  subscribes to the appropriate event (closing the app, session end) that indicates when targets with PersistMode.Automatic should be persisted.  

All the other important methods (Configure, ApplyState, and PersistState) have already been described...  

Configuring tracking by attributes 

An alternative way to configure tracking is to use the Trackable and TrackingKey attributes.  

/// <summary>
/// If applied to a class, makes all properties trackable by default.
/// If applied to a property specifies if the property should be tracked.
/// <remarks>
/// Attributes on properties override attributes on the class.
/// </remarks>
/// </summary>
[AttributeUsage(AttributeTargets.Property | 
      AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class TrackableAttribute : Attribute
{
    public bool IsTrackable { get; set; }

    public TrackableAttribute()
    {
        IsTrackable = true;
    }

    public TrackableAttribute(bool isTrackabe)
    {
        IsTrackable = isTrackabe;
    }
} 
/// <summary>
/// Marks the property as the tracking identifier for the object.
/// The property will in most cases be of type String, Guid or Int
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class TrackingKeyAttribute : Attribute
{
} 

Instead of calling configuration.AddProperties([list of properties]) for a target, we can mark the relevant properties of the target's class (or the entire class) with the TrackableAttribute. Also, instead of calling configuration.SetKey(“[some key]”), we can mark a property with the TrackingKey attribute, which will cause that property to behave like an ID property – the value of this property will be the identifier (key) of the target object.  

These two attributes allow us to specify the tracked properties and tracking key at the class level, instead of having to specify this data for every instance we want to track. Another benefit to this is that it allows automatic tracking if using IOC with no additional work. 

The ITrackingAware interface  

When defining a class, it’s not always possible to decorate the properties with attributes. For instance, when we subclass System.Windows.Window we don't have control over the properties that are defined in it (unless they are virtual) because we don’t control the source code of the Window class, so we can't decorate them with attributes. In this case, we can, instead, implement the ITrackingAware interface which looks like this:

/// <summary> 
/// Allows the object that is being tracked to customize
/// its persitence
/// </summary>
public interface ITrackingAware
{
    /// <summary>
    /// Called before applying persisted state to the object.
    /// </summary>
    /// <param name="configuration"></param>
    /// <returns>Return false to cancel applying state</returns>
    bool OnApplyingState(TrackingConfiguration configuration);
    /// <summary>
    /// Called before persisting object state.
    /// </summary>
    /// <param name="configuration"></param>
    /// <returns>Return false to cancel persisting state</returns>
    bool OnPersistingState(TrackingConfiguration configuration);
}

This interface allows us to modify the tracking configuration before applying and persisting state, and even to cancel either of those. This can also come in handy for WindowsForms, where Forms have bogus sizes and locations when minimized – in this case we can cancel persisting a minimized window.    

IOC integration  

Now for the cool part... When using an IOC container (like Unity/Castle Windsor/Ninject/Lin Fu) in an application, a lot of the objects are either created or built up (have their dependencies injected) by the IOC container. So why not have the container automatically configure tracking and apply state to all trackable objects it builds up!   

This way, if your object is going to be built up by the  container, all you need to do to make a property persistent is either decorate it with the TrackableAttribute, or implement ITrackingAware in the appropriate way.  

So far, I have done this only with Unity but I suspect it should not be hard to do with other IOC containers. Here is the code for the UnityContainerExtension which automatically adds tracking to objects:    

/// <summary>
/// Unity extension for adding (attribute based) tracking to built objects
/// </summary>
public class TrackingExtension : UnityContainerExtension
{
    class TrackingStrategy : BuilderStrategy
    {
        SettingsTracker _tracker;
        public TrackingStrategy(SettingsTracker tracker)
        {
            _tracker = tracker;
        }
        public override void PostBuildUp(IBuilderContext context)
        {
            base.PostBuildUp(context);
            _tracker.Configure(context.Existing).AddMetaData().SetMode(PersistModes.Automatic);
            _tracker.ApplyState(context.Existing);
        }
    }
    protected override void Initialize()
    {
        if (!Container.IsRegistered<SettingsTracker>())
            throw new Exception("No settings tracker registered!");
        Context.Strategies.Add(Container.Resolve<TrackingStrategy>(), UnityBuildStage.Creation);
    }
}  

This is how one would configure their Unity container for adding tracking support, using this extension: 

IUnityContainer _container = new UnityContainer();
string localSettingsFilePath = Path.Combine(Environment.GetFolderPath(
  Environment.SpecialFolder.ApplicationData), "testsettingswithIOC.xml");
 
_container.RegisterType<IDataStore, FileDataStore>(
  new ContainerControlledLifetimeManager(), new InjectionConstructor(localSettingsFilePath));
_container.RegisterType<ISerializer, BinarySerializer>(new ContainerControlledLifetimeManager());
_container.RegisterType<ObjectStore>(new ContainerControlledLifetimeManager());
_container.RegisterType<SettingsTracker>(new ContainerControlledLifetimeManager());
 
_container.AddExtension(new TrackingExtension());   

Note that SettingsTracker must be registered with the container before adding this extension. 

The demo apps  

In the demo apps I have used the tracking library for persisting UI state, as well as persisting application settings (without using the standard .NET configuration API). Note that I had no problem implementing INotifyPropertyChanged in one of my settings classes. If my application was plugin-enabled I would also not have any problems allowing the plugins to have settings of their own. In the demo, there is one app that uses Unity IOC Container, and one that doesn't. 

Conclusion   

The work of saving settings and applying them to concerned objects involves a lot of copying data back and forth, and can be quite monotonous and error prone. In this article I aimed to present a more declarative approach in which you only specify what needs to be persisted and when, and have the copying (the "how") taken care of automatically. This approach results in a lot less effort, code, and repetition. 

TODO 

  • Thread safety 
  • Add type information to the ObjectStore and ISerializer - this will facilitate some implementations of ISerializer, as well as allow it to selectively serialize different types in different ways   

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Antonio Nakić Alfirević
Software Developer (Senior) Riz Transmitters LLC
Croatia Croatia
Member
I have been an active software developer since 2005 mostly working on .NET. Currently living and working in Zagreb Croatia. I have earned my masters degree in Computer Science at the Faculty of Electrical Engineering and Computer Science in Zagreb in 2006.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberiJam_j7 Apr '13 - 5:18 
Wow. This saves a lot of headache.
GeneralRe: My vote of 5memberAntonio Nakić Alfirević19 Apr '13 - 23:01 
Thanks, I'm glad you find it usefull!
QuestionWindows Forms Example and UserControlsmembergersis7615 Feb '13 - 5:05 
Hi,
Your is an excellent job!
 
I have two questions:
- Can you provide a small example of a Windows Forms App? (VB.NET or C#)
- How to track one or more user control's properties dragged on main form an external project (dll)
with your library in the same xml file (an example should be appreciated)?
 
Thank you very much Smile | :)
 
Gersis
AnswerRe: Windows Forms Example and UserControlsmemberAntonio Nakić Alfirević19 Feb '13 - 2:27 
Thanks!Smile | :) To answer your questions:
 
1. Yes, I will add a WinForms example and a new chapter about using this in ASP.NET when I have the time.
 
2. You distinguish between instances of the same class by the Key, much like objects that are persisted to the databse are distinguished by some ID property. You specify the key at the instance level by calling SetKey for each instance's tracking configuration, or you can do it at the class level by using the [TrackingKey] attribute or by implementing ITrackingAware. Ill probably upload an example some time next week so drop by later on...
 
Antonio
QuestionCan we tracking and persisting properties of controls within the form?memberpkradioman10 Feb '13 - 8:23 
The form has checkbox, textbox, etc. Can we track and persist those properties of the controls directly as we do with the form?
 
Thank you. Good job!Thumbs Up | :thumbsup:
AnswerRe: Can we tracking and persisting properties of controls within the form?memberAntonio Nakić Alfirević10 Feb '13 - 22:27 
Thanks!
 
Yes, you can track any object. For instance if you have two textbox controls named textbox1 and textbox2 and you want to track their "Text" properties, you would write:
 
tracker.Configure(textBox1).SetKey("tb1").AddProperties("Text");
tracker.ApplyState(textBox1);
 
tracker.Configure(textBox2).SetKey("tb2").AddProperties("Text");
tracker.ApplyState(textBox2);
 
I would recomment though if you track UI controls that you only track their UI-related properties, not the user data - for instance it would be better to track a Customer object's Name property, that to track the text property of the Textbox that displays it.
 
One more note: in the source I uploaded there is a bug so you can't use the AddProperties(()=>textbox1.Text) overload, it will throw an exception, but I will upload a fixed version when I have the time, and ofcourse you can fix it on your own if you feel like it Smile | :)
GeneralMy vote of 5memberZaid Pirwani4 Feb '13 - 20:32 
I had to come down here and vote, once I saw the first example... I have been through the left-side example a few times now..
GeneralMy vote of 5memberPh.E4 Feb '13 - 4:24 
Quick and simple.
Good work!
SuggestionWell donemvpEspen Harlinn4 Feb '13 - 2:25 
Thumbs Up | :thumbsup: Interesting, I guess it wouldn't be too hard to use an ASP.NET profile to store the settings.
 
ASP.NET Profile Properties Overview[^]
Using ASP.NET Providers in Non-ASP.NET Applications[^]
Espen Harlinn
Principal Architect, Software - Goodtech Projects & Services AS

Projects promoting programming in "natural language" are intrinsically doomed to fail. Edsger W.Dijkstra

GeneralRe: Well donememberAntonio Nakić Alfirević5 Feb '13 - 9:57 
ThanksCool | :cool: I did wince a little bit when I suggested cookies as a datastore given the network traffic implications. It's been a while since I last dabbled in web development so I hadn't even heard of profiles until I visited your links. You are in fact right, it would be quite easy to implement an IDataStore that uses profiles and it seems like a better approach than cookies.
 
On a related matter - it would also be easy to implement an IDataStore that uses session state (or TempData). In ASP.NET MVC a custom ControllerFactory could be used to add DI and tracking to all controllers. This would in effect allow the controllers to have stateful properties (they would just need the [Trackable] attribute). I think this might be a nice feature. Seems preferable to copying data back and forth from TempData manually. I'd like to hear what do you think about this, could it be a good idea?
GeneralRe: Well donemvpEspen Harlinn5 Feb '13 - 10:13 
Antonio Nakić Alfirević wrote:
I think this might be a nice feature.

Certainly Thumbs Up | :thumbsup:
 
You could still use profiles for storage, as the provider architecture is not limited to ASP.Net forms applications.
 
Since there are many provider implementations for various databases, and other backends, it kind of makes sense to build on this architecture, and for what it's worth: It also works well with Sharepoint.
Espen Harlinn
Principal Architect, Software - Goodtech Projects & Services AS

Projects promoting programming in "natural language" are intrinsically doomed to fail. Edsger W.Dijkstra

GeneralRe: Well donememberAntonio Nakić Alfirević5 Feb '13 - 21:08 
I'll check it out and see how it works, thanks for the comments and the tip.
GeneralMy vote of 5memberBerryl Hesh27 Jan '13 - 1:20 
Useful, well designed AND well written.
GeneralMy vote of 5memberJohn B Oliver6 Nov '12 - 10:51 
That is really sweet.
GeneralRe: My vote of 5memberAntonio Nakić Alfirević6 Nov '12 - 11:37 
Thanks for sayin' so!Smile | :)
Questionnice articlememberGun Gun Febrianza15 Oct '12 - 18:56 
i'm interested with your article this very usefull for me. Wink | ;)
Indonesian IT Intelijensi
Freedom of Revealing And Sharing Knowledge.
 
www.indonesiaitintelijensi.com

AnswerRe: nice articlememberAntonio Nakić Alfirević16 Oct '12 - 0:56 
Thanks, kind of you to say so!
GeneralMy vote of 5memberGun Gun Febrianza15 Oct '12 - 18:55 
Vote 5 For your information!
Thanks for sharing
GeneralGood postmemberShahriar Iqbal Chowdhury15 Oct '12 - 10:58 
Nice to see some good work
GeneralRe: Good postmemberAntonio Nakić Alfirević15 Oct '12 - 11:55 
Thanks!
GeneralMy vote of 5memberGiandroid15 Oct '12 - 0:01 
Very well designed and commented!
GeneralRe: My vote of 5memberAntonio Nakić Alfirević15 Oct '12 - 0:48 
Thank you, I'm glad you like it!

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130516.1 | Last Updated 4 Feb 2013
Article Copyright 2012 by Antonio Nakić Alfirević
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid