Login


A Custom Settings Class for WinForms

By Jonathan Wood on 12/31/2010
Language: C#
Technology: .NETWinForms
Platform: Windows
License: CPOL
Views: 26,795
Frameworks & Libraries » WinForms » General » A Custom Settings Class for WinForms

Demo Project Screenshot

Download Source Code Download Source Code

Introduction

When you create a WinForms application using .NET, you can create settings by adding them to the Settings tab of the Application Properties window in Visual Studio. You can then access those settings from code using the Properties.Settings class.

For example, if I add a setting called "Password", I can access it from code using Properties.Settings.Default.Password. And after I modify this setting from code, I can persist it to disk using Properties.Settings.Default.Save().

This is pretty cool. The Password property in the example above will be of the type I specified when I created the setting. This gives my code type-safe access to that setting with very little work on my part. It's very convenient.

But I don't like it! For starters, the settings are stored in a file in a very obscure location. Your average end user would never be able to locate this file if they needed to copy or back it up. Even worse, this location changes depending on if you are doing a Debug or Release build!

Writing a Custom Settings Class

I was working on an application with a lot of important settings and just got burned by this arrangement too many times. I'm not sure why it's set up like it is, but I found it much easier if I could store my settings in an application folder under My Documents. What's more, those same settings will work whether I'm running a Debug or Release build. So I decided to implement my own settings class.

What I came up with isn't quite as convenient as Properties.Settings. A little more typing is required to set it up. But it can store my settings in a more convenient location. And I added a couple of extras such as the ability to encrypt sensitive fields.

Listing 1 shows my SettingsBase class. It is an abstract class, which means it cannot be instantiated. Instead, you must create your own class that derives from it. SettingsBase has two abstract methods, ReadSettings() and WriteSettings(). Your derived class must override these two methods in order for your class to be able to be instantiated.

The idea is that the ReadSettings() and WriteSettings() methods need to be implemented by your custom class in order to know exactly what needs to be saved. When the Load() method is called, your custom ReadSettings() method will be called with a UserSettingsReader object as an argument. This object can be used to read your custom settings.

A similar thing happens when the Save() method is called. This causes your custom WriteSettings() method to be called with a UserSettingsWriter object as an argument. This object can be used to write your custom settings.

The UserSettingsReader and UserSettingsWriter classes support most data types, and both include a method for dealing with encrypted strings. Both classes employ the SimpleEncryption class, which I haven't shown here.

Before you can use your derived class, you must set the SettingsPath property to the full path and name of your settings file. In addition, if you plan to use any of the encryption methods, you must set the EncryptionKey property to a unique encryption key. (So that your encryption scheme isn't identical to everyone else who uses this code.)

Note that the Save() method does not support incremental writes. It overwrites the entire file and any previous settings it contained.

Listing 1: SettingsBase Class

public abstract class SettingsBase
{
    public string SettingsPath { get; set; }
    public string EncryptionKey { get; set; }

    public SettingsBase()
    {
        // These properties must be set by derived class
        SettingsPath = null;
        EncryptionKey = null;
    }

    /// <summary>
    /// Loads user settings from the specified file. The file should
    /// have been created using this class' Save method.
    /// 
    /// You must implement ReadSettings for any data to be read.
    /// </summary>
    public void Load()
    {
        UserSettingsReader reader = new UserSettingsReader(EncryptionKey);
        reader.Load(SettingsPath);
        ReadSettings(reader);
    }

    /// <summary>
    /// Saves the current settings to the specified file.
    /// 
    /// You must implement WriteSettings for any data to be written.
    /// </summary>
    public void Save()
    {
        UserSettingsWriter writer = new UserSettingsWriter(EncryptionKey);
        WriteSettings(writer);
        writer.Save(SettingsPath);
    }

    // Abstract methods
    public abstract void ReadSettings(UserSettingsReader reader);
    public abstract void WriteSettings(UserSettingsWriter writer);
}

public class UserSettingsWriter
{
    protected XmlDocument _doc = null;
    protected string _encryptionKey;

    public UserSettingsWriter(string encryptionKey)
    {
        _encryptionKey = encryptionKey;
        _doc = new XmlDocument();

        // Initialize settings document
        _doc.AppendChild(_doc.CreateNode(XmlNodeType.XmlDeclaration, null, null));
        _doc.AppendChild(_doc.CreateElement("Settings"));
    }

    /// <summary>
    /// Saves the current data to the specified file
    /// </summary>
    /// <param name="filename"></param>
    public void Save(string filename)
    {
        _doc.Save(filename);
    }

    /// <summary>
    /// Writes a string value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    public void Write(string key, string value)
    {
        WriteNodeValue(key, value != null ? value : String.Empty);
    }

    /// <summary>
    /// Writes an integer value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    public void Write(string key, int value)
    {
        WriteNodeValue(key, value);
    }

    /// <summary>
    /// Writes a floating-point value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    public void Write(string key, double value)
    {
        WriteNodeValue(key, value);
    }

    /// <summary>
    /// Writes a Boolean value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    public void Write(string key, bool value)
    {
        WriteNodeValue(key, value);
    }

    /// <summary>
    /// Writes a DateTime value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    public void Write(string key, DateTime value)
    {
        WriteNodeValue(key, value);
    }

    /// <summary>
    /// Writes an encrypted string value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    public void WriteEncrypted(string key, string value)
    {
        SimpleEncryption enc = new SimpleEncryption(_encryptionKey);
        WriteNodeValue(key, enc.Encrypt(value));
    }

    /// <summary>
    /// Internal method to write a node to the XML document
    /// </summary>
    /// <param name="key"></param>
    /// <param name="value"></param>
    protected void WriteNodeValue(string key, object value)
    {
        XmlElement elem = _doc.CreateElement(key);
        elem.InnerText = value.ToString();
        _doc.DocumentElement.AppendChild(elem);
    }
}

public class UserSettingsReader
{
    protected XmlDocument _doc = null;
    protected string _encryptionKey;

    public UserSettingsReader(string encryptionKey)
    {
        _encryptionKey = encryptionKey;
        _doc = new XmlDocument();
    }

    /// <summary>
    /// Loads data from the specified file
    /// </summary>
    /// <param name="filename"></param>
    public void Load(string filename)
    {
        _doc.Load(filename);
    }

    /// <summary>
    /// Reads a string value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="defaultValue"></param>
    /// <returns></returns>
    public string Read(string key, string defaultValue)
    {
        string result = ReadNodeValue(key);
        if (result != null)
            return result;
        return defaultValue;
    }

    /// <summary>
    /// Reads an integer value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="defaultValue"></param>
    /// <returns></returns>
    public int Read(string key, int defaultValue)
    {
        int result;
        string s = ReadNodeValue(key);
        if (int.TryParse(s, out result))
            return result;
        return defaultValue;
    }

    /// <summary>
    /// Reads a floating-point value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="defaultValue"></param>
    /// <returns></returns>
    public double Read(string key, double defaultValue)
    {
        double result;
        string s = ReadNodeValue(key);
        if (double.TryParse(s, out result))
            return result;
        return defaultValue;
    }

    /// <summary>
    /// Reads a Boolean value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="defaultValue"></param>
    /// <returns></returns>
    public bool Read(string key, bool defaultValue)
    {
        bool result;
        string s = ReadNodeValue(key);
        if (bool.TryParse(s, out result))
            return result;
        return defaultValue;
    }

    /// <summary>
    /// Reads a DateTime value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="defaultValue"></param>
    /// <returns></returns>
    public DateTime Read(string key, DateTime defaultValue)
    {
        DateTime result;
        string s = ReadNodeValue(key);
        if (DateTime.TryParse(s, out result))
            return result;
        return defaultValue;
    }

    /// <summary>
    /// Reads an encrypted string value
    /// </summary>
    /// <param name="key"></param>
    /// <param name="defaultValue"></param>
    /// <returns></returns>
    public string ReadEncrypted(string key, string defaultValue)
    {
        string result = ReadNodeValue(key);
        if (result != null)
        {
            SimpleEncryption enc = new SimpleEncryption(_encryptionKey);
            return enc.Decrypt(result);
        }
        return defaultValue;
    }

    /// <summary>
    /// Internal method to read a node from the XML document
    /// </summary>
    /// <param name="key"></param>
    /// <returns></returns>
    protected string ReadNodeValue(string key)
    {
        XmlNode node = _doc.DocumentElement.SelectSingleNode(key);
        if (node != null && !String.IsNullOrEmpty(node.InnerText))
            return node.InnerText;
        return null;
    }
}

Using the Class

As you can see, there are a couple of steps required before you can see any benefits from the SettingsBase class. Listing 2 shows a form I created that uses the class.

The code starts by creating a custom settings class, MySettings, which derives from SettingsBase. MySettings starts by declaring any settings used by the application. Next, as described earlier, it overrides the WriteSettings() and ReadSettings() methods. These methods either read or write all of the settings declared by the class using the reader/writer object passed as an argument.

After that, the form declares an instance of MySettings and calls it Settings. This object is initialized in my form's constructor. That code starts by getting the path to my application's folder under "My Documents" and creating the path if it doesn't already exist. It then instantiates Settings and initializes its SettingsPath and EncryptionKey properties.

The form loads these settings into the form's controls in the form's Load event. It saves the values from the controls to the settings file in the form's FormClosing event.

Listing 2: Implementing the SettingsBase Class

public partial class Form1 : Form
{
    // Create customized Settings class
    class MySettings : SettingsBase
    {
        // Declare application settings
        public string Text { get; set; }
        public int Value { get; set; }
        public string Password { get; set; }

        // Must override WriteSettings() to write values
        public override void WriteSettings(UserSettingsWriter writer)
        {
            writer.Write("Text", Text);
            writer.Write("Value", Value);
            writer.WriteEncrypted("Password", Password);
        }

        // Must override ReadSettings() to read values
        public override void ReadSettings(UserSettingsReader reader)
        {
            Text = reader.Read("Text", "");
            Value = reader.Read("Value", 0);
            Password = reader.ReadEncrypted("Password", "");
         }
    }

    // Declare application settings object
    private MySettings Settings;

    public Form1()
    {
        InitializeComponent();

        // Get "My Documents" folder
        string path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
        path = Path.Combine(path, "TestSettings");

        // Create folder if it doesn't already exist
        if (!Directory.Exists(path))
            Directory.CreateDirectory(path);

        // Initialize settings
        Settings = new MySettings();
        Settings.SettingsPath = Path.Combine(path, "Settings.xml");
        Settings.EncryptionKey = "sampleKey";
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        // Load settings
        Settings.Load();
        txtText.Text = Settings.Text;
        numValue.Value = Settings.Value;
        txtPassword.Text = Settings.Password;
    }

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        // Save settings
        Settings.Text = txtText.Text;
        Settings.Value = (int)numValue.Value;
        Settings.Password = txtPassword.Text;
        Settings.Save();
    }
}

Listing 3 shows the contents of a sample settings file. It's just regular XML and may be edited by hand if necessary. Note the encrypted password.

Listing 3: Sample Settings Data

<?xml version="1.0" ?>
<Settings>
    <Text>Sample Text</Text> 
    <Value>50</Value> 
    <Password>uHMM27U5ctQhOieXa1o3nw==</Password> 
</Settings>

Conclusion

It occurs to me that some readers will be satisfied to stick with the settings mechanism that comes with WinForms and that's fine. I've presented another option for you to consider. As I've shown, it's a little more work to set up, but it provides a number of advantages.

End-User License

Use of this article and any related source code or other files is governed by the terms and conditions of The Code Project Open License.

Author Information

Jonathan Wood

I'm a software/website developer working out of the greater Salt Lake City area in Utah. I've developed many websites including Black Belt Coder, Insider Articles, and others.