demo_and_source Easier .pdf
Transcript of demo_and_source Easier .pdf
10,559,255 members (36,752 online) 637 Sign out
Member 1969759
home quick answers discussions features community
helpSearch for articles, questions, tips
Articles » Languages » C# » General
Article
Browse Code
Bugs / Suggestions
Stats
Revisions (36)
Alternatives
Comments (53)
View this article'sWorkspace
Fork this Workspace
Connect using Git
Add your ownalternative version
Share
About Article
Creating a library for
persisting the applicationstate data between work
sessions
Type Article
Licence CPOL
First Posted 14 Oct 2012
Views 59,409
Downloads 2,089
Bookmarked 273 times
C# ASP.NET .NETCF .NET
Dev WPF WebForms , +
Top News
How to protect yourself
from the Heartbleed Bug
Next
Rate:
Easier .NET settingsBy Antonio Nakić Alfirević, 17 Apr 2014
Download demo and source
1. 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 re-sizes 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. For a desktop application, this data could include locations and sizes of movable
and re-sizable UI elements, user input (for example last entered username), as well as application settings
and user preferences.
After coming across this requirement more times than I care to remember, I decided to spend some time
and make a reusable library that automates most of the work of persisting and applying settings. The
entire library is a few hundred lines of code and is not very hard to understand.
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.
2. Platforms
This library can be used for WPF, Windows Forms and ASP.NET (WebForms/MVC) applications. The
required version of .NET is 4.0 or higher.
3. 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 settings this way is tedious. The
settings objects are usually not the ones that use the data, they just store data from all over the
application. This means that to use this data you must write code that copies the data from settings to the
appropriate objects and later writes updated data back again to the settings some time before the
application closes.
Suppose your application has several re-sizable 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, then write additional code that updates the settings before
4.90 (84 votes)
articles
1 2 3 4 5
4.90/5 - 84 votesμ 4.89, σa 0.99 [?]
Get the Insider News free eachmorning.
Related Videos
Related Articles
Gios WORD .NET Library (using
RTF specification)
Multiple Subsequent "Main"Forms in C# Apps
.NET - COM Interoperability
.Net - Use The Framework
How to make your WebReference proxy URL dynamic
C# RangeBar control
Clean Up that BloatedWeb.Config! An ApplicationSettings Manager that's Easy toUse
Using a Generic Type toSimplify Flags EnumerationOperations
Modular InnoSetupDependency Installer
Handling Corrupt "user.config"
Settings
Application Settings the .NETway. INI, Registry, or XML
Get a User's Full Name
Don't Flicker! Double Buffer!
Debugging Made Easier withMole 2010
RegEx Tracer
Why, Where, and How of .NETConfiguration Files
Shape Control for .NET
DialogForm - An ExtendedWinForms Class
C# Application to Create and
Recognize Mouse Gestures(.NET)
Lifecycle Profile Settings
How to Easily Host WPF Controlinside Windows FormApplication
Related Research
Protecting Your Business Data:Five Do’s and Don’ts
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 (decorate a property with
an attribute).
In the following chapters I demonstrate the use of the library, and discuss it's implementation.
4. Terminology
In this article I use two terms which I think might need explaining:
tracking a property - saving the value of an object's property before the application shuts down,
and identifying the object and re-applying the saved value to it's property once the application
starts up again.
persistent property - a property that is being tracked
5. Usage
The SettingsTracker is the class that coordinates tracking. It takes care of applying any previously
stored data to your objects, and it stores new data from the desired objects to a persistent store when
appropriate.
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. For example:
Collapse | Copy Code
string settingsFilePath = Path.Combine(Environment.GetFolderPath( Environment.SpecialFolder.ApplicationData), @"VendorName\AppName\settings.xml");ISerializer serializer = new BinarySerializer(); //use binary serializationIDataStore dataStore = new FileDataStore(localSettingsPath); //use a file to store dataSettingsTracker tracker = new SettingsTracker(dataStore, serializer); //create our settings tracker
Now we have a SettingsTracker instance which can track properties. It will use binary serialization to
serialize data, and store the serialized 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.
5.1. Example scenario 1: Persisting a WPF window location and size
The idea 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
Collapse | Copy Code
public MainWindow(){ InitializeComponent();
this.Left =
B) Using this library
Steps 1 and 2: Configure tracking and apply state...
and we're done.
Collapse | Copy Code
public MainWindow(){ InitializeComponent();
//1. set up tracking for the main window Services.Tracker.Configure(this) .AddProperties<mainwindow>(w => w.Height, w => w.Width, w => w.Left, w => w.Top, w => w.WindowState); .SetKey("MainWindow") .SetMode(PersistModes.Automatic);
//2. apply persisted state to the window Services.Tracker.ApplyState(this);}
In this example the static property
Custom API Management forthe Enterprise: Learn how to
build a successful API strategy[Webinar]
Fine-Tuning the Engines of SMBGrowth: 4 strategies for growing
your business
Insider Secrets on API SecurityFrom Experts at Securosis
[Webinar]
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
Collapse | Copy Code
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);}
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 track, 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 automatically
persisted to the store before the application closes. No writing code that copies data back and forth.
We can also specify the list of properties to track by using a [Trackable] attribute on the class and/or
it's properties provided we control the source code of the class. I demonstrate this in the next example.
Example scenario 2: Persisting application settings (configuring tracking via
Attributes)
Suppose you want to use an instance of the following class to hold your application's settings:
Collapse | Copy Code
[Trackable]//applied to class - all properties will be trackedpublic class GeneralSettings{ public int FontSize{ get; set; } public Color FontColor{ get; set; } public string BackgroundImagePath { get; set; }}
Here is how we would configure tracking an instance of this class:
Collapse | Copy Code
Services.Tracker.Configure(settingsObj).AddProperties<GeneralSettings>( s => s.FontSize, s => s.FontColor, s => s.BackgroundImagePath);
There is also a slightly simpler way to specify the list of properties to track – using the [Trackable]
attribute. I applied it to the class to specify that all public properties of this class should be tracked. To
exclude a property, we would decorate it with [Trackable(false)]. Here is how to configure tracking
based on the use of [Trackable]:
Collapse | Copy Code
Services.Tracker.Configure(settingsObj).AddMetaData();Services.Tracker.ApplyState(settingsObj);
Note that the settings class does not need to inherit any specific class, 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 use it to set up tracking for
all objects it builds up. 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 implements ITrackable
(just an empty „marker“ interface to mark which objects to automatically track). 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.
6. Benefits
So what are the benefits of all this? To sum it up:
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 (1-
when defining the settings in a .config or .settings file. 2- when copying data from settings to other
objects, and 3- when copying data back to settings)
it's declarative - you can use attributes (Trackable and TrackingKey) to configure what needs to
be tracked and to identify the object
if using an IOC container you can apply tracking with virtually no code aside from attributes on
appropriate properties - more on this in the "IOC integration" chapter
for web applications it can make your controller/page properties stateful
For details on how all this is implemented, and how it can be used and customized, please read on...
7. 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:
7.1. 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:
Collapse | Copy Code
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:
Collapse | Copy Code
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.
Other implementations of the ISerializer interface might for example use:
JSON (JSON.NET implementation of ISerializer included in the library)
SoapFormatter
YAML
protobuf.net (a cool open source serialization library)
TypeConverter based solutions
custom solutions
7.2. 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:
Collapse | Copy Code
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
ASP.NET session state (can be used to add stateful properties to controllers and/or pages)
ASP.NET user profile
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:
Collapse | Copy Code
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.
7.3. 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:
Collapse | Copy Code
namespace Tracking.DataStoring{ public class ObjectStore : IObjectStore { IDataStore _dataStore; ISerializer _serializer;
public bool CacheObjects { get; set; }
Dictionary<string, object> _createdInstances = new Dictionary<string, object>();
public ObjectStore(IDataStore dataStore, ISerializer serializer) { _dataStore = dataStore; _serializer = serializer; CacheObjects = true; }
public void Persist(object target, string key) { _createdInstances[key] = target; _dataStore.SetData(_serializer.Serialize(target), key); }
public bool ContainsKey(string key) { return _dataStore.ContainsKey(key); }
public object Retrieve(string key) { if (!CacheObjects || !_createdInstances.ContainsKey(key)) _createdInstances[key] = _serializer.Deserialize(_dataStore.GetData(key)); 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...
7.4. 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 it's name suggests, 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:
Collapse | Copy Code
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 which means persist the
properties just before the application closes. If you don't like using hard coded strings when specifying
properties, you can instead use the other overload of the AddProperties method like so:
Collapse | Copy Code
AddProperties<MainWindow>(w => w.Height, w => w.Width, w => w.Left, w => w.Top, w => w.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 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(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:
Collapse | Copy Code
namespace Tracking{ 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 string TrackerName { 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>().Where( ta=>ta.TrackerName == TrackerName).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>( ).Where(ta=>ta.TrackerName == TrackerName).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 - each method returns the same TrackingConfiguration 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:
Collapse | Copy Code
public class SettingsTracker{ List<TrackingConfiguration> _configurations = new List<TrackingConfiguration>();
public string Name { get; set; }
IObjectStore _objectStore; public SettingsTracker(IObjectStore 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 if (System.Windows.Forms.Application.OpenForms.Count > 0)//winforms System.Windows.Forms.Application.ApplicationExit += (s, e) => { PersistAutomaticTargets(); }; }
public 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) { TrackerName = Name }; _configurations.Add(config); } return config; }
public void ApplyAllState() { foreach (TrackingConfiguration config in _configurations.Where(c=>c.TargetReference.IsAlive)) ApplyState(config.TargetReference.Target); }
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 type of application (WinForms, WPF, ASP.NET), the WireUpAutomaticPersist
method subscribes to the appropriate event that indicates when targets with PersistMode.Automatic
should be persisted.
All the other important methods (Configure, ApplyState, and PersistState) have already been
described...
7.5. Configuring tracking by attributes
An alternative way to configure tracking is to use the Trackable and TrackingKey attributes.
Collapse | Copy Code
/// <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 string TrackerName { 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 which properties to track and the 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
enables automatic tracking if we are using an IOC container- we simply hook into the container so that
after it has injected dependencies on an object we call AddMetadata and AppySettings if the object
implements the marker interface ITrackable.
7.6. 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:
Collapse | Copy Code
/// <summary>/// Allows the object that is being tracked to customize/// its persitence/// </summary>public interface ITrackingAware : ITrackable{ /// <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 after state aplied. /// </summary> /// <returns></returns> void OnAppliedState();
/// <summary> /// Called before persisting object state. /// </summary> /// <param name="configuration"></param> /// <returns>Return false to cancel persisting state</returns> bool OnPersistingState(TrackingConfiguration configuration); /// <summary> /// Called after state persisted. /// </summary> /// <param name="configuration"></param> /// <returns></returns> void OnPersistedState();}
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.
7.7. IOC integration
Now for the cool part... When using an IOC container (Unity/Castle Windsor/Ninject/Lin Fu etc...) in an
application, a lot of 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:
1. make sure the class that defines the property implements the empty marker interface ITrackable
and decorate the property with [Trackable], - or -
2. implement the ITrackingAware interface in the appropriate way
The ITrackable interface has no members, and serves only as a marker to let the IOC extension know
you want to automatically track objects that have it. I opted to use an interface instead of an attribute for
this because checking for the existence of an attribute is a little bit slower than checking for an interface.
Note: ITrackingAware already inherits from ITrackable.
So far, I have used this approach with Unity and Ninject 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:
Collapse | Copy Code
namespace Tracking{ /// <summary> /// Marker interface for classes that want their tracking to be handled /// by the IOC container. /// <remarks> /// Checking if a class implements an interface is faster that checking /// if its decorated with an attribute. /// </remarks> /// </summary> public interface ITrackable { }
/// <summary> /// Unity extension for adding (attribute based) state tracking to creted objects /// </summary> public class TrackingExtension : UnityContainerExtension { class TrackingStrategy : BuilderStrategy { IUnityContainer _container; public TrackingStrategy(IUnityContainer container) { _container = container; }
public override void PostBuildUp(IBuilderContext context) { base.PostBuildUp(context); ITrackable autoTracked = context.Existing as ITrackable; if (autoTracked != null) { IEnumerable<SettingsTracker> trackers = _container.ResolveAll<SettingsTracker>(); foreach (SettingsTracker tracker in trackers) { tracker.Configure(autoTracked).AddMetaData( ).SetMode(PersistModes.Automatic); tracker.ApplyState(autoTracked); } } } }
protected override void Initialize() { Context.Strategies.Add( new TrackingStrategy(Container), UnityBuildStage.Creation); } }}
This is how one would configure their Unity container for adding tracking support, using this extension:
Collapse | Copy Code
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<IObjectStore, ObjectStore>(new ContainerControlledLifetimeManager());_container.RegisterType<SettingsTracker>(new ContainerControlledLifetimeManager()); _container.AddExtension(new TrackingExtension());
8. What about web apps?
In web apps, objects have a very short lifespan indeed. They are created when the server starts processing
a request and discarded as soon as the response is sent. With the exception of manually storing data on
the server (for example by using the Session store or user profiles), the server does not keep any
application state. Instead, (any) state is passed arround from client to server and back again with each
request-response (inside the querry string, form data, cookies etc...).
The "Session" object can, for example, be used to maintain state but it's clunky, and the compiler can't
ensure type and name safety of the data inside it.
Using this library in web applications however allows for having ASP.NET pages and MVC controllers
whose properties seem to "survive" between postbacks. Depending on the IDataStore implementation
used, the data can be stored in the Session state, an ASP.NET user profile or somewhere else. We don't
need to do anything else but decorate the desired properties with the [Trackable] attribute, and ensure
that the page or controller is built up using an IOC container with an extension for managing tracking.
Using IOC for resolving Pages and Controllers can be done with a custom ControllerFactory (for MVC) or
with a custom IHttpModule (for regular ASP.NET) – I've included demo apps for both flavours of ASP.NET
with comments on the important parts. So let's see how we could use this library to handle counting the
number of visits to a page (MVC example).
a) Using Session directly
Collapse | Copy Code
[HandleError]public class HomeController : Controller{ public ActionResult Index() { uint numberOfVisits = 0; //1. Get the value from session //if present: no compile time checking //of type or identifier if (Session["numberOfVisits"] != null) numberOfVisits = (uint)Session["numberOfVisits"];
//2. do something with the value... ViewData["NumberOfVisits_User"] = numberOfVisits;
//3. increment the number of visits numberOfVisits++;
//4. store it in the Session state Session["SomeIdentifier"] = numberOfVisits;
return View(); }}
b) Using this library
Collapse | Copy Code
[HandleError]public class HomeController : Controller, ITrackable{ [Trackable] public uint NumberOfVisits { get; set; }
public ActionResult Index() { //no need to do anything //to fetch or save NumberOfVisits
//1. Do something with the value... ViewData["NumberOfVisits"] = NumberOfVisits; //2. increment the number of users NumberOfVisits++;
return View(); }}
In this scenario, option B has several advantages:
simplicity (just apply the Trackable attribute to desired properties)
name safety (no need to worry about naming the data in the session store when saving/retrieving)
type safety (no need to cast when retrieving data from the session store)
8.1. Configuring tracking in ASP.NET WebForms
In order to enable this behavior in ASP.NET (WebForms), I have created a custom IHttpModule so I can
do stuff before and after a page is processed. The module takes an IUnityContainer reference in it's
constructor and does the following:
1. Adds the tracking extension to the IOC container (so every object the container creates or injects
into gets tracked if it implements the ITrackable marker interface)
2. Before the HttpHandler (the ASP.NET page) starts executing, it uses the IOC container to inject it
with dependencies (and applies tracking to it and any other objects that are created in the process)
3. After the handler (the ASP.NET Page) is done processing, it calls PersistAutomaticTargets on all
SettingsTrackers that are registered in the container.
This is the code for the http module:
Collapse | Copy Code
namespace Tracking.Unity.ASPNET{ public class TrackingModule : IHttpModule { IUnityContainer _container; public TrackingModule(IUnityContainer container) { _container = container; _container.AddExtension(new TrackingExtension()); } public void Dispose() { } public void Init(HttpApplication context) { context.PreRequestHandlerExecute += new EventHandler(context_PreRequestHandlerExecute); context.PostRequestHandlerExecute += new EventHandler(context_PostRequestHandlerExecute); } void context_PreRequestHandlerExecute(object sender, EventArgs e) { if (HttpContext.Current.Handler is IRequiresSessionState || HttpContext.Current.Handler is IReadOnlySessionState) { object page = HttpContext.Current.Handler; _container.BuildUp(page.GetType(), page); }
} void context_PostRequestHandlerExecute(object sender, EventArgs e) { if (HttpContext.Current.Handler is IRequiresSessionState || HttpContext.Current.Handler is IReadOnlySessionState) { //named trackers foreach (SettingsTracker tracker in _container.ResolveAll<SettingsTracker>()) tracker.PersistAutomaticTargets(); //unnamed tracker if(_container.IsRegistered<SettingsTracker>()) _container.Resolve<SettingsTracker>().PersistAutomaticTargets(); } } }}
Since the module needs a reference to the IUnityContainer in it's constructor, it needs to be created in
code (instead of in app.config). This must be done in the Init() method in the global.asax file, like so:
Collapse | Copy Code
namespace WebApplication1{ public class Global : System.Web.HttpApplication { public static UnityContainer _uc = new UnityContainer(); static IHttpModule trackingModule = new TrackingModule(_uc);
public override void Init() { base.Init();
//Register services in the IOC container //...
//register appropriate SettingsTrackers //i use a factory method instead of a single // instance so each session can have it's own instance //so they don't interfere with each other _uc.RegisterType<SettingsTracker>(new SessionLifetimeManager(), new InjectionFactory(c => new SettingsTracker(new ObjectStore( new ProfileStore("TrackingData"), new BinarySerializer()) { CacheObjects = false })));
//initialize the tracking module trackingModule.Init(this); } }}
8.2. Configuring tracking in ASP.NET MVC
In MVC, the controllers are not handlers, so the HttpModule approach is not applicable. Instead, the
dependency injection and tracking can be set up using a custom controller factory. I've included one in the
library and here is what it looks like:
Collapse | Copy Code
namespace Tracking.Unity.ASPNET{ public class TrackingControllerFactory : DefaultControllerFactory { IUnityContainer _container; public TrackingControllerFactory(IUnityContainer container) { _container = container; _container.AddExtension(new TrackingExtension());
HttpContext.Current.ApplicationInstance.PostRequestHandlerExecute += new EventHandler(ApplicationInstance_PostRequestHandlerExecute); }
void ApplicationInstance_PostRequestHandlerExecute(object sender, EventArgs e) { //named trackers foreach (SettingsTracker tracker in _container.ResolveAll<SettingsTracker>()) tracker.PersistAutomaticTargets();
//unnamed tracker if (_container.IsRegistered<SettingsTracker>()) _container.Resolve<SettingsTracker>().PersistAutomaticTargets(); }
#region IControllerFactory Members
public override IController CreateController( System.Web.Routing.RequestContext requestContext, string controllerName) { IController controller = base.CreateController(requestContext, controllerName); _container.BuildUp(controller); return controller; }
#endregion
}}
The controller factory also needs to be set up in the Init() method in global.asax, because it is
subscribing to the PostRequestHandlerExecute event which can only work during Init. The global.asax
file might look like this:
Collapse | Copy Code
namespace MvcApplication1{ // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { static UnityContainer _uc = new UnityContainer();
public override void Init() { base.Init();
//register appropriate SettingsTrackers _uc.RegisterType<SettingsTracker>("USER", new RequestLifetimeManager(), new InjectionFactory(container => new SettingsTracker(new ObjectStore(new ProfileStore("TrackingData"), new JsonSerializer())) { Name = "USER" })); _uc.RegisterType<SettingsTracker>("SESSION", new SessionLifetimeManager(), new InjectionFactory(container => new SettingsTracker(new ObjectStore(new SessionStore(), new JsonSerializer())) { Name = "SESSION" }));
//IMPORTANT: use the TrackingControllerFactory to create controllers //so we can inject dependencies into them and apply tracking ControllerBuilder.Current.SetControllerFactory(new TrackingControllerFactory(_uc)); }
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); }
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); } }}
Now all we need to do is decorate properties we want to be persisted with [Trackable] and implement
the ITrackable interface in the controllers. You may have noticed I have registered more than one
settings tracker here. The brings me to the last point of interest...
9. Multiple trackers (named trackers)
There are times when you need to store some data e.g. at the user level, and other data at the machine
level, or perhaps at the session level. In this case, we can create multiple trackers, give each one a name,
register them in the IOC container by name, and specify the tracker name in the Trackable attribute. An
example is shown in the MVC demo, but here is what specifying the tracker name looks like:
Collapse | Copy Code
[Trackable(TrackerName = "USER")]public uint NumberOfVisits_User { get; set; }
[Trackable(TrackerName = "SESSION")]public uint NumberOfVisits_Session { get; set; }
I've included an example of using multiple trackers in the ASP.NET WebForms demo application where
some properties are tracked on a per-user level, and some are tracked on a per-session level.
10. The demo apps
In the desktop 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.
I have also included a ASP.NET WebForms application, and a MVC application with an example of using
multiple trackers. The apps use ASP.NET user profiles with a aspnetdb.mdf file for storing user data.
Antonio Nakić
AlfirevićSoftware Developer (Senior) Recro-Net
Croatia
I have been an a(tra)ctive 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.
Add a Comment or Question Search this forum Go
Depending on the SQL server you have installed, you may need to adjust the connection string in the
web.config for the demos to work.
11. 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.
12. Nuget
The package is available on NuGet, the Id of the project is "Deva.Tracking".
TODO
Optimization: cache tracking metadata for types
Host source (SourceForge/Github... )
History
Update 2013-06-18: WinForms example added.
Update 2013-06-12: usage in web apps, JSON serialization, multiple (named) trackers.
License
This article, along with any associated source code and files, is licensed under The Code Project Open
License (CPOL)
About the Author
Article Top
Comments and Discussions
Profile popups Spacing Relaxed Noise Medium Layout Normal Per page 25 Update
First Prev Next
Anupam Singh_ 19-Apr-14 14:49 An excellent work done..
Permalink | Advertise | Privacy | Mobile Web03 | 2.8.140421.2 | Last Updated 17 Apr 2014
Article Copyright 2012 by Antonio Nakić AlfirevićEverything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid
melnac 18-Apr-14 9:31
Martin Solovey 18-Apr-14 1:15
okdone 23-Nov-13 17:26
SakarSR 29-Oct-13 5:00
Antonio Nakić Alfirević 29-Oct-13 11:15
M Rayhan 24-Oct-13 15:01
holylust 2-Aug-13 11:08
MiguelCouto 26-Jun-13 17:59
CodeJarry 19-Jun-13 11:42
deepu9890 19-Jun-13 11:12
jfos 18-Jun-13 19:59
Antonio Nakić Alfirević 19-Jun-13 9:17
jfos 19-Jun-13 17:26
Darek Danielewski 18-Jun-13 18:07
SagarRS 18-Jun-13 14:44
benny856694 17-Jun-13 16:26
Antonio Nakić Alfirević 17-Jun-13 17:56
benny856694 17-Jun-13 16:21
Antonio Nakić Alfirević 17-Jun-13 17:50
benny856694 17-Jun-13 15:49
Renju Vinod 14-Jun-13 9:20
Volynsky Alex 12-Jun-13 18:45
Andy Eskridge 12-Jun-13 18:39
Stiaan van der
Westhuizen
28-May-13 14:20
Last Visit: 1-Jan-00 6:00 Last Update: 23-Apr-14 10:49 Refresh 1 2 3 Next »
General News Suggestion Question Bug Answer Joke Rant Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.
Custom Configuration Section
My vote of 5
My vote of 5
small suggestion to use NoSQL Database
Re: small suggestion to use NoSQL Database
[modified]
My vote of 5
My vote of 5
Sugestion
My vote of 5
My vote of 5
Clarifcation on the built-in tool
Re: Clarifcation on the built-in tool
Re: Clarifcation on the built-in tool
I still prefer a simpler solution by Jani
My vote of 5
too many dependencies
Re: too many dependencies
nuget
Re: nuget
My vote of 5
My vote of 5
My vote of 5
My vote of 5
IOC example with Caliburn.Micro