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?
Aaron, great post. We used the .NET settings way back in a large scale WPF project and though it was useful, we had a number of issues:
+ you can’t easily save user settings on a per-application-userid basis since they are persisted to the store for the Windows user. We had so subclass the application settings base classes to help us.
+ along with that, we had to modify the settings upgrade path to ensure we brought along all the users’ data.
+ the use of XmlSerialization under the covers caused us startup performance pain, which we got around by pre-rolling serialization DLLs, but that was more pain. :)
I definitely think the .NET core settings framework has its advantages in simple cases where you have no complex types in the settings and not many settings, but beyond that it’s hard to manage.
@eburke — what would you use instead if you did it all over?
@aaron I would probably do what you did and rewrite a very simple settings framework that could store different settings based on a key (such as a user ID), as well as generic settings that are not user-specific.
At startup we could then say LoadSettings(key) and pass in the user ID that we are loading up, which would load them.
I would make the serialization technology swappable so that you could use XML, JSON, binary, database, etc. That way it’s useful for mobile apps, too.