After dealing with lost settings, an unclear upgrade path, and my own confusion
surrounding the magic of
Settings
in a .NET client application, I decided to build my own.
You’re probably familiar with this UI in Visual Studio. It hasn’t changed much since
it was first created:
A list of properties, data type, scope and a default value. Admittedly, it makes
things simple. However, with my WPF .NET application that I created a few years ago
(SnugUp), I’ve always been troubled
by the magic of the settings. It was too easy to get in a situation where a user
would loose their settings doing uninstalls, reinstalls, upgrades.
While I’m sure it’s possible to make the built in settings classes to work, it
wasn’t worth the effort for me to understand them and learn what the nuances of
where they’re placed, how to do a decent upgrade, how not to loose them, etc.
In the SnugUp WPF UI, the code uses a two-way bindings to directly edit the settings
of the application (like: "{Binding AppSettings.DebugMode}"). It was
simple, and all I needed. It’s handy that ApplicationSettingsBase implements the
INotifyPropertyChanged interface which WPF needs for simple two-way data bindings.
My solution, which I admit is heavier than the original as it requires a large
additional assembly is to use
JSON.NET as the
serializer/deserializer for a new settings class I created.
So, the basic pattern:
1: public class ApplicationSettings : INotifyPropertyChanged
2: {
3: public event PropertyChangedEventHandler PropertyChanged;
4:
5: private bool _debugMode;
6: private string _albumNameFormat;
7: private string _extraFileExtensions;
8: private bool _automaticRun;
9: private string _galleryCreationSubCategory;
10: private bool _filenameOnlyCheck;
Properties created the standard way for INotifyPropertyChaged:
1: private DateTime _nextUpdateCheck;
2: public DateTime NextUpdateCheck
3: {
4: get { return _nextUpdateCheck; }
5: set
6: {
7: if (_nextUpdateCheck != value)
8: {
9: _nextUpdateCheck = value;
10: RaisePropertyChanged("NextUpdateCheck");
11: }
12: }
13: }
I wanted a predictable path for storing settings (so it would be easy to document
and backup for users). I used the AssemblyCompany attribute and the AssemblyProduct
attribute as the folder names:
1: internal static string GetSettingsDirectory()
2: {
3: string path = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
4: var attrs = Assembly.GetEntryAssembly().GetCustomAttributes(typeof(AssemblyCompanyAttribute), false);
5: if (attrs.Length == 1)
6: {
7: path = Path.Combine(path, ((AssemblyCompanyAttribute)attrs[0]).Company);
8:
9: }
10: attrs = Assembly.GetEntryAssembly().GetCustomAttributes(typeof(AssemblyProductAttribute), false);
11: if (attrs.Length == 1)
12: {
13: path = Path.Combine(path, ((AssemblyProductAttribute)attrs[0]).Product);
14: }
15: return path;
16: }
In this WPF application, in the AssemblyInfo.cs file, the attributes are set as
follows:
1: [assembly: AssemblyCompany("WiredPrairie.us")]
2: [assembly: AssemblyProduct("SnugUp")]
On my machine, that maps to this path:
d:\Users\Aaron\AppData\Roaming\WiredPrairie.us\SnugUp\
Loading settings then is straightforward using JSON.NET:
1: public static ApplicationSettings Load(string filename)
2: {
3: ApplicationSettings settings = null;
4: var directory = GetSettingsDirectory();
5: var path = Path.Combine(directory, filename);
6:
7: if (File.Exists(path))
8: {
9: string fileData = File.ReadAllText(path);
10: try
11: {
12: settings = JsonConvert.DeserializeObject<ApplicationSettings>(fileData, new JsonSerializerSettings { });
13: }
14: catch { }
15: }
16: if (settings == null)
17: {
18: settings = new ApplicationSettings();
19: SetDefaults(settings);
20: // initialize settings once
21: Save(settings, filename);
22: }
23: return settings;
24: }
In my code, if the settings file didn’t exist or fails to serialize into something
meaningful, a new settings file is created with a few defaults. (I haven’t decided
what to do when there’s an exception when reading the file, hence the empty catch).
Saving the settings is just as easy:
1: public static void Save(ApplicationSettings settings, string filename)
2: {
3: Debug.Assert(settings != null);
4: var directory = GetSettingsDirectory();
5: var path = Path.Combine(directory, filename);
6:
7: JsonConvert.SerializeObject(settings);
8:
9: if (!Directory.Exists(directory))
10: {
11: Directory.CreateDirectory(directory);
12: }
13:
14: var fileData = JsonConvert.SerializeObject(settings, Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate });
15: try
16: {
17: using (StreamWriter writer = File.CreateText(path))
18: {
19: writer.Write(fileData);
20: writer.Close();
21: }
22: }
23: catch { }
24: }
The SerializeObject method returns a string which is then written to a file using a
StreamWriter.
I added a Save method to the instance of the ApplicationSettings:
1: public void Save()
2: {
3: ApplicationSettings.Save(this);
4: }
This preserved the functionality that exists in the built in Settings support in
.NET (which was being used in my application).
By keeping all of the property names the same and making a few tweaks to the types
of some fields in my application, I had swapped out the entire “settings”
infrastructure in about 45 minutes.
I’m planning some other JSON activities within my application, so the overhead of
using JSON.NET is acceptable.
The best part of this alternative is that there isn’t any magic. It’s all easy to
manage. Further, I can easily modify my installer to properly handle/update, etc.,
the settings file with just a few clicks.
I’m not going back to the built-in .NET settings support again. I’ve learned my
lesson.
What have you done for “user” settings?