For those ardent followers of my blog, my apologies for the noticeable absence. I've been out of town working on the next release of our software package and that has consumed every single waking hour (from 8:00 AM to 3:00 AM) each day. So while I've had a mountain of ideas and concepts to blog about, time has simply not been on my side. Today, however, I find myself with at least a breather.
I'd like to comment on a concept that I actually formulated and coded a few months ago, and always meant to blog about but simply never got around to it. In fact, a little over a year ago, I blogged about serializing a class from Xml to simplify reading Xml elements from a .config file. The issue with this particular approach is that the data doesn't roundtrip; that is, the Xml doesn't ever get written back to the .config file. Sure, there are ways to do it, but my solution didn't address any of those techniques primarily for one reason: I'm not of the camp that believes the .config file is for storing user preferences. Config files are there so that an application can be tweaked to operate with a set of predefined, but customizable parameters. They are designed to be set before the application starts and remain that way.
User settings, on the other hand, are preferences that the user may customize from usage to usage. For that we need a mechanism separate from, but sort of related to, the .config file. If you're a user of the .NET 2.0 Framework, you may already be familiar several of the new classes in the System.Configuration namespace (namely the SettingsBase, SettingsProperty, SettingsProvider, et al). These classes facilitate a very similar set of functionality to what I've created, but using a different mechanism.
The primary issue I set out to resolve for my particular applications was that I wanted to create a mechanism through which I could load and read user settings/preferences and simultaneously be able to persist their preferences back. This is pretty trivial in concept but there can often be hiccups and gotchas in real world scenarios. For example, suppose your application is being run in a heightened security zone (such as the intranet or internet zones). If you want to persists user settings to disk you can't simply open a pipe to the root c:\ and start writing.
To this end I created a set of utility classes that encapsulate the logic needed to store and retrieve settings. The principal class is called UserSettingsManager. The UserSettingsManager is responsible for loading, saving, and deleting user settings. For simplicity (both in implementation and details) I chose to rely on XmlSerialization for the storage of user settings. I also decided to store user settings using the System.IO.IsolatedStorage classes for a few reasons:
- The location of the file is something I don't have to worry about (the OS handles that for me) - all I care about is the file name - even then, I only care that the information can be retrieved and persisted.
- I can make settings roaming if roaming profiles are used. That way, settings move with the user as he logs in to different computers.
- The files are just that: isolated. I can only see settings pertaining to my application/user. Isolated storage is a protected (but not encrypted) storage mechanism when using managed code.
- Users/applications with lower privileges on the system can still save and read data from IsolatedStorage.
In order to make it generic, I also created a marker interface (that is, an interface with no methods) called IUserSettings. Implementing classes can then be passed to the Save() method or retrieved via the Load() method in a generic way. Classes that implement this interface should also support XmlSerialization (possibly using XmlRootAttribute and friends) because the UserSettingsManager relies on XmlSerialization to save and read the settings.
Lastly, I created an attribute called UserSettingsFileAttribute that can applied to the assembly. This attribute simply designates the name of the file used for storing user settings.
That's pretty much all there is to it. Here's a little run down of the code (without comments). You can download the full source complete will comments here.
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple=false)]
public sealed class UserSettingsFileAttribute : Attribute {
private string _settingsFile;
public UserSettingsFileAttribute(string settingsFile) {
_settingsFile = settingsFile;
}
public string SettingsFile {
get { return _settingsFile; }
}
}
///////////////////////////////////////
public interface IUserSettings { }
///////////////////////////////////////
using System;
using System.IO;
using System.IO.IsolatedStorage;
using System.Reflection;
using System.Xml.Serialization;
public sealed class UserSettingsManager {
private UserSettingsManager() { }
public static void Save(IUserSettings settings) {
Type type = settings.GetType();
XmlSerializer ser = new XmlSerializer(type, string.Empty);
using ( IsolatedStorageFile file = getIsolatedStore() )
using ( IsolatedStorageFileStream fs = new IsolatedStorageFileStream(getFileName(type), FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, file) ) {
ser.Serialize(fs, settings);
fs.SetLength(fs.Position);
}
}
public static IUserSettings Load(Type type, out bool createdNew) {
XmlSerializer ser = new XmlSerializer(type, string.Empty);
IUserSettings settings;
try {
using ( IsolatedStorageFile file = getIsolatedStore() )
using ( IsolatedStorageFileStream fs = new IsolatedStorageFileStream(getFileName(type), FileMode.Open, FileAccess.Read, FileShare.Read, file) ) {
settings = (IUserSettings)ser.Deserialize(fs);
createdNew = false;
}
}
catch ( FileNotFoundException ) {
ConstructorInfo ci = type.GetContructor(Type.EmptyTypes);
settings = (IUserSettings)ci.Invoke(null);
createdNew = true;
}
return settings;
}
public static void Delete(IUserSettings settings) {
Type type = settings.GetType();
using ( IsolatedStorageFile file = getIsolatedStore() )
file.DeleteFile(getFileName(type));
}
private static string getFileName(Type type) {
Assembly asm = Assembly.GetAssembly(type);
object[] usfa = asm.GetCustomAttributes(typeof(UserSettingsFileAttribute), false);
if ( null == usfa || 0 == usfa.Length )
throw new ArgumentException(“Unable to resolve the user settings file name. UserSettingsFileAttribute is undefined.“);
else
return ((UserSettingsFileAttribute)usfa[0]).SettingsFile;
}
private static IsolatedStorageFile getIsolatedStore() {
return IsolatedStorageFile.GetStore(IsolatedStorageScope.Assembly | IsolatedStorageScope.User, null, null);
}
}
Once incorporated into your project or into a utility assembly you can implement it as simply as the following:
[XmlRoot("userSettings")]
public sealed class UserSettings : IUserSettings {
// ...
// ...
}
// when you need to load the user settings:
private UserSettings _settings;
bool createdNew;
_settings = UserSettingsManager.Load(typeof(UserSettings), out createdNew) as UserSettings;
if ( createdNew ) UserSettingsManager.Save(_settings);
// when you need to save the settings:
UserSettingsManager.Save(_settings);
It's as simple as that and it sure makes managing user settings a snap!