Writing your own settings manager

.NET offers built-in settings manager for desktop applications that intend to store and load some data using a persistent storage. It’s tightly integrated with Visual Studio, so you can easily add/remove/change entries, while a back-end XML structure silently re-assembles itself. You can also define the serialization type of your entries, which lets you treat them as instances of certain classes, rather than plain strings or boxed objects.

Despite the obvious convenience the integration offers, the Application Settings module has a lot of downsides.


  1. Settings file location cannot be changed

For every assembly, the settings will be stored inside %localappdata%\[Project Name]\[Assembly Name + Assembly Location]\[Assembly Version]\ directory. In this path, %localappdata% resolves to wherever the user has their Local Application Data and the Assembly Location part is a base64 encoded string of the path that leads to the executing assembly.

It has a lot of major problems with it:

  • No control whatsoever on where the files are stored (what if I want to store my settings in Roaming App Data? what if I want a different directory name?).
  • When you change assembly version, the settings file that was generated using the older version will not be usable. There’s a manual workaround to this using a call to Upgrade() method. The whole process is explained here.
  • When you launch the same app from a new location, a different settings file will be used.
  • Messy folder names that pollute application data directory.
  • Changing assembly name will render the settings unusable.
  1. Only a few types can be serialized directly

You can serialize all of the primitive types, such as int, float, double, uint, string and so on, with addition of DateTime and some other .NET provided types. However, to serialize enums you will have to manually use a proxy property that will convert it from/to its integer or string representation. You can’t store any of your custom types either.

This can be partially worked around by overriding the settings controller class and changing the way respective properties get/set their values, but then you’d have to manually write (de)serializers for every complex type.

  1. Default values must be compile-time constants

You cannot directly assign defaults to settings that are not constant values. If you want it to evaluate some method or command to populate the default value, you are forced to override the property definition for particular entry and implement custom logic.

  1. Only stores data in XML format

The settings file itself is a verbose, human-readable XML file. Perhaps not a big deal, but what if you wanted to change the settings file format?


So eventually I just decided to make my own settings manager

My main goals were:

  • It should let the middle-user select where it stores the settings file.
  • It should work with enums and custom defined classes/structs without any extra work.
  • It should be able to set default values at run-time.
  • It should work right away but also be very customizable.

For serialization/deserialization I originally went with the built-in .NET binary serializer, but as it later turned out it wasn’t the best choice. It wasn’t very version-friendly – the generated binary files were tied to a specific schema and it would break (completely or sometimes partially) if the settings file was outdated. I’ve also tried using two XML serializers, the built-in one and a custom one, both shared some issues with no clear benefit over the binary serializer, but at least there was a way to change that by overriding the serialization process. Not wanting to do that, I ultimately decided to go with (and later stayed with) JSON.net. Almost all of my projects used that library regardless, so the extra dependency wasn’t a big deal. On the other hand, JSON.net handles a lot of dirty work for me, which I’d have to take care of myself otherwise.

I wanted to make my settings class in such way that the middle-users (the programmers who use my library) would just derive from the class, define whatever properties they want stored and have the library take care of the rest by itself. For that, I came up with the following abstract class, called SettingsManager:

In this simple version, the middle-user can derive their own SettingsManager and either use a constructor that specifies a subdirectory name or use the parameter-less one that sets the directory name, based on the assembly name. This still does not fit the requirements for path customization, but I’d keep it simple for now.

Other points of interest are the methods. Let me go over them:

  • CopyFrom – shallow copies another instance of settings manager into current instance.
  • Save – serializes the current instance and saves it to file.
  • Load – deserializes settings data from file and populates the current instance with it.
  • Reset – creates a new instance of the settings manager (which has all properties set to their default values) and shallow copies it into the current instance.
  • Delete – deletes settings file and (optionally) settings storage directory.

Right outside of the box, this class can be used to make a very basic settings manager, like so:

The properties can be changed freely and then saved to persistent storage, using the Save method. Here’s a basic example:

Notice how the usage is almost identical to the built-in .NET settings manager, making it easy to replace one with another.


Extending

As I started using the SettingsManager in my own apps, I found myself in need of some extra features.

  1. I added INotifyPropertyChanged implementation that raises events when the properties change

Note: [CallerMemberName] is a .NET 4.5 attribute and if you’re targeting earlier versions of the framework, you need to remove it.

The middle-user will have to re-define properties that they want to be notified of, like so:

The Set method will check if the new value is actually different and only raise events in that case. Since JSON.net uses properties for deserialization (as opposed to writing to fields directly), we will get events for CopyFrom, Load, Reset as well as any other indirect change to properties. Very useful in WPF applications.

  1. IsSaved property that tells whether the settings have been saved to persistent storage since they were last changed

Now, the properties that are implemented in the derived class via the Set method will cause IsSaved property to be set to false when they are changed. Using the Save or Load methods will reset the IsSaved back to true.

  1. Adding fail-safe save/load methods that can be used in places where the outcome doesn’t matter

This is convinient because you can just check the outcome and display a message in case the settings were not loaded. No exception handling is necessary.

  1. More convinient configuration approach

I created a configuration class that defines various options affecting the SettingsManager behavior.

And then updated the SettingsManager constructors to make use of them:

I limited base directory to only the following options – LocalAppData, RoamingAppData, MyDocuments, as recommended in Microsoft guidelines.

  1. Staging context for settings that can be changed while they are used

Typically, if you have a user interface to change settings you don't want them to be useable by the rest of your application until the user clicks the save button. Since we are writing directly to properties, we need two SettingsManager instances: one with stable settings and one that's currently being edited.

To utilize the stager - just replace the static SettingsManager instance property with the Stager instance.

Most of the application will be accessing the settings via Stager.Current to get the last saved data, while the settings window (or wherever the settings can be changed by the user) will access the Stager.Staging property. When the settings are ready to be synchronized, a call to Stager.Save() should be made. Note that the Save and Load methods should now be called on the Stager object, instead of the SettingsManager objects. Once the settings are saved, the values in Stager.Staging are automatically copied to Stager.Current.


Final implementation of my settings manager can be found on GitHub.

Comments