Cleaning up the settings


If you ever looked at the CWSRestart code, you might have noticed that every settings class had a fairly good amount of copy-and-pasted Spaghetti code. So I took the time to abstract the config loading into its own class, that can either be inherited or instantiated, to save and load configuration files. Let's take a look on how to create such a class.

The assembly has an empty configuration file located in the subfolder Embedded, with the build action set to embedded ressource:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
  </appSettings>
</configuration>

The constructor of the settings class requires one parameter: the filename of the configuration file. This parameter is stored in the private field file. When the class is instantiated, the constructor will check if the given file exists. If not, the configuration file from the embedded ressource is copied to the given location.

Settings(string ConfigurationFile)
{
    file = ConfigurationFile;

    if (!File.Exists(file))
    {
        //Replace Embedded with your folder name
        using (Stream resource = GetType().Assembly.GetManifestResourceStream("Utilities.Embedded.empty.config"))
        {
            using (Stream output = File.OpenWrite(file))
            {
                resource.CopyTo(output);
            }
        }
    }
}

Now that we have created the file, we need a function to open the configuration. Add a reference to System.Configuration and a using directive at the top with using System.Configuration;. Now we can create the function to open the file with OpenMappedExeConfiguration:

private Configuration openConfiguration()
{
    ExeConfigurationFileMap configurationMap = new ExeConfigurationFileMap();
    configurationMap.ExeConfigFilename = file;

    Configuration config = ConfigurationManager.OpenMappedExeConfiguration(configurationMap, ConfigurationUserLevel.None);
    return config;
}

Reading and saving values

Now that everything is in place, we can actually get a value from the configuration file. I created a small helper method to do this. It utilizes the openConfiguration method we created above:

public string GetAppSettingValue(string key)
{
    Configuration config = openConfiguration();
    return config.AppSettings.Settings[key] != null ? config.AppSettings.Settings[key].Value : null;
}

To prevent exceptions, we first check if the given key exists in our configuration file. If it does, we return the value, otherwise we return null. If you are unfamiliar with the ? : notation:

return config.AppSettings.Settings[key] != null ? config.AppSettings.Settings[key].Value : null;

//Will expand to

if(config.AppSettings.Settings[key] != null)
    { return config.AppSettings.Settings[key].Value; }
else
    { return null; }

Now we still need a way to save values. We can do this with the following function:

public void SetAppSetting(string Key, object Value)
{
    Configuration config = openConfiguration();

    if (config.AppSettings.Settings[Key] != null)
        config.AppSettings.Settings.Remove(Key);

    config.AppSettings.Settings.Add(Key, Value.ToString());
    config.Save(ConfigurationSaveMode.Modified);
}

This function will check if the setting already exists. In this case, the setting is removed. Then a new setting is added with the given key and value. Finally, the modified values are saved to the file.

Getting values with the correct type

Not every setting is a string. CWSRestart for instance stores booleans, IPAddresses, integers and many more types in the configuration file. Therefore it would be nice, if we could add methods that return the correct type. Also, since we use an empty config file as template, we need to add default values if settings are not present.

Basically we need a function GetAppSettingWithStandardValue(string key, Type fallback), that returns the value for key if present. If key does not exist in the settings, we create it with fallback as value and return fallback. We could use a generic method that boxes the value to T like


//pseudo code
public T GetAppSettingWithStandardValue<T>(string key, T fallback)
{
    return (T)GetAppSettingValue(key);
}

However, this function could throw errors if someone messes with our config file. The best thing would be a generic TryParse, however there is no such thing. But here is a small workaround. First, we create generic TryParseHandler<T>:

private delegate bool TryParseHandler<T>(string value, out T result);

This delegate matches every TryParse method out there. Now we create a helper function that uses this delegate to parse our value and create a setting if parsing fails (or no setting is present):

private T readOrCreateValue<T>(string key, T value, TryParseHandler<T> handler)
{
    if (!(GetAppSettingValue(key) != null && handler(GetAppSettingValue(key), out value)))
    {
        SetAppSetting(key, value);
    }
    return value;
}

Now we can create overloaded functions to get settings with the correct type:

public int GetAppSettingWithStandardValue(string key, int fallback)
{
    return readOrCreateValue<int>(key, fallback, int.TryParse);
}

public bool GetAppSettingWithStandardValue(string key, bool fallback)
{
    return readOrCreateValue<bool>(key, fallback, Boolean.TryParse);
}

public IPAddress GetAppSettingWithStandardValue(string key, IPAddress fallback)
{
    return readOrCreateValue<IPAddress>(key, fallback, IPAddress.TryParse);
}

Conclusion

Creating a settings class isn't really rocket science. A generic TryParse (or an interface with TryParse) would be helpful. You could use reflection to find your TryParse method, but if you have only a small number of types you can use overloaded functions as well. The full code can bew viewed at GitHub.

from by


comments powered by Disqus