Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Share User Settings Between Applications

17 Dec 2008 1  
A not-so-dotNet way to allow multiple programs to share a single settings file

Introduction

In the process of writing another article scraping application, I came to need the ability to share user settings between two applications. I looked around and came to the conclusion that all of the solutions available that used the built-in Settings functionality in the .Net framework were a) cumbersome, and b) just plain overly difficult to implement, resulting in obscure, dodgy-looking and difficult to maintain code. Granted, I didn't look around too hard, but what I found was enough to convince me to roll my own solution.

This project requires the use of Visual Studio 2008 and dotNet 3.5 (or higher).

The Basic Idea

I decided that I should build a separate class library so that I would only have to write the settings-handling code one time, and just use that assembly in the various applications that needed access to the settings file. I wanted to come up with a base class that handled most of the drudgery, and leave the programmer with as little required work as possible.>/p>

Further, I wanted to provide "default setting" functionality so that your app could specify default settings that could be retrieved in the event the user need them. Lastly, I wanted to make the whole thing as self-sufficient as possible, so there are a couple of utility methods included that make the programmers life a little easier.

The settings are stored in a XML file in the specified (special) folder, and I use System.Xml.Linq to load/save the file and to transfer data between the settings objects (that's why it needs dotNet 3.5).

The AppSettingsBase Class

The AppSettingsBase class provides the bulk of the functionality for the library and is intended to be derived from. The original version of this code/article pointed out that the class was abstract even though there were no abstract methods in it. Navaneenth (a user here at CP) pointed out that I could achieve the desired goal of preventing instantiating of the class by making the constructor protected instead of public. The article and code now reflect this comment.

First, we have the data members and properties. There's really nothing remarkable about this block of code with the exception of the XElement property. If you don't override this property in your derived class, this object will throw an exception, reminding you that you MUST provide an override in the derived class. The reason I did this is because properties cannot be made abstract.

public abstract class AppSettingsBase
{
    protected System.Environment.SpecialFolder m_specialFolder = 
                                    Environment.SpecialFolder.CommonApplicationData;
    protected bool     m_isDefault           = true;
    protected string   m_fileName            = "";
    protected string   m_dataFilePath        = "";
    protected string   m_settingsFileComment = "";
    protected string   m_settingsKeyName     = "";
    protected XElement m_defaultSettings     = null;

    //--------------------------------------------------------------------------------
    public bool IsDefault
    {
        get { return m_isDefault; }
        set { m_isDefault = value; }
    }
    //--------------------------------------------------------------------------------
    public string FileName
    {
        get { return m_fileName; }
        set { m_fileName = value; }
    }
    //--------------------------------------------------------------------------------
    public string SettingsFileComment
    {
        get { return m_settingsFileComment; }
        set { m_settingsFileComment = value; }
    }
    //--------------------------------------------------------------------------------
    public string SettingsKeyName
    {
        get { return m_settingsKeyName; }
        set { m_settingsKeyName = value; }
    }
    //--------------------------------------------------------------------------------
    public virtual XElement XElement
    {
        get { throw new Exception("You must provide your own XElement property."); }
        set { throw new Exception("You must provide your own XElement property."); }
    }
    //--------------------------------------------------------------------------------
    public XElement DefaultSettings
    {
        get { return m_defaultSettings; }
        set { m_defaultSettings = value; }
    }

Next comes the constructor. The only item of note here is that the class determines whether or not it's being created as a "default settings" object by evaluating the defaultSettings parameter for nullness. If it is null, then this object will be used to represent the application's default settings. Otherwise, it will represent the user settings.

    //--------------------------------------------------------------------------------
    protected AppSettingsBase(string appFolder, string fileName, string settingsKeyName, 
                           string fileComment, XElement defaultSettings)
    {
        m_defaultSettings      = defaultSettings;
        m_isDefault            = (m_defaultSettings == null);
        m_fileName             = fileName;
        m_settingsKeyName      = settingsKeyName;
        m_settingsFileComment  = fileComment;
        m_dataFilePath         = CreateAppDataFolder(appFolder);
        if (!IsDefault && m_defaultSettings != null)
        {
            XElement = m_defaultSettings;
        }
    }

There are several virtual functions that provide basic functionality, and they were specified as virtual in case there was some special processing needed by the programmer. Essentially, these are the methods that load and save the settings.

    //--------------------------------------------------------------------------------
    public virtual void Load()
    {
        bool loaded = false;
        string fileName = System.IO.Path.Combine(m_dataFilePath, m_fileName);
        if (File.Exists(fileName))
        {
            try
            {
                XDocument doc = XDocument.Load(fileName);
                var settings = doc.Descendants(SettingsKeyName);
                if (settings.Count() > 0)
                {
                    foreach (XElement element in settings)
                    {
                        XElement = element;
                        loaded = true;
                        // just in case we have more than one, only take the first one
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
        if (!loaded && !IsDefault && m_defaultSettings != null)
        {
            Reset();
        }
    }


    //--------------------------------------------------------------------------------
    public virtual void Save()
    {
        if (!IsDefault)
        {
            string fileName = System.IO.Path.Combine(m_dataFilePath, m_fileName);
            try
            {
                XDocument doc = new XDocument(new XDeclaration("1.0", "utf-8", "yes"), 
                                              new XComment(SettingsFileComment));
                var root = new XElement("ROOT", XElement);
                doc.Add(root);
                doc.Save(fileName);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
    }


    //--------------------------------------------------------------------------------
    public virtual void SetAsDefaults(ref XElement element)
    {
        if (!IsDefault)
        {
            element = XElement;
        }
    }

    //--------------------------------------------------------------------------------
    public virtual void Reset()
    {
        if (!IsDefault && m_defaultSettings != null)
        {
            XElement = m_defaultSettings;
        }
    }

Finally, we have the two utility methods. The first one is used to cast an integer value back to a possible enum ordinal. This method accepts an integer, and tries to find a matching ordinal within the defined set of ordinals. If a match is found, it is returned. Otherwise, the specified default value is returned. It amounts to a sanity check for values that are obtained form a data file or other external source.

    //--------------------------------------------------------------------------------
    public static T IntToEnum<T>(int value, T defaultValue)
    {
        T enumValue = (Enum.IsDefined(typeof(T), value)) ? (T)(object)value : defaultValue;
        return enumValue;
    }

The last utility method is used to create the required application data folder. By default, this is set to the special folder CommonApplicationData, but there is a property available that exposes the setting to external objects. If the specified folder exists, or if it is successfully created, the complete folder name is returned to the calling method.

    //--------------------------------------------------------------------------------
    protected string CreateAppDataFolder(string folderName)
    {
        string appDataPath = "";
        string dataFilePath = "";

        folderName = folderName.Trim();
        if (folderName != "")
        {
            try
            {
                // Set the directory where the file will come from.  The folder name 
                // returned will be different between XP and Vista. Under XP, the default 
                // folder name is "C:\Documents and Settings\All Users\Application Data\[folderName]"
                // while under Vista, the folder name is "C:\Program Data\[folderName]".
                appDataPath = System.Environment.GetFolderPath(SpecialFolder);
            }
            catch (Exception ex)
            {
                throw ex;
            }
			// make sure w're only going to create a top-level folder
            if (folderName.Contains("\\"))
            {
                string[] path = folderName.Split('\\');
                int folderCount = 0;
                int folderIndex = -1;
                for (int i = 0; i < path.Length; i++)
                {
                    string folder = path[i];
                    if (folder != "")
                    {
                        if (folderIndex == -1)
                        {
                            folderIndex = i;
                        }
                        folderCount++;
                    }
                }
                if (folderCount != 1)
                {
                    throw new Exception("Invalid folder name specified (this function" +
                                        "only creates the root app data folder for the" + 
                                        " application).");
                }
                folderName = path[folderIndex];
            }
        }
        if (folderName == "")
        {
            throw new Exception("Processed folder name resulted in an empty string.");
        }
        try
        {
            dataFilePath = System.IO.Path.Combine(appDataPath, folderName);
            if (!Directory.Exists(dataFilePath))
            {
                Directory.CreateDirectory(dataFilePath);
            }
        }
        catch (Exception ex)
        {
            throw ex;
        }
        return dataFilePath;
    }

}

The AppSettings Class

Since all of the heavy lifting is done in the AppSettingsBAse class, there's not really much to do here other than add our data members and properties.

public class AppSettings : AppSettingsBase
{
    // used to seed the base constructor
    private	const string APP_DATA_FOLDER	= "SharedSettingsDemo";
    private const string APP_DATA_FILENAME	= "Settings.xml";
    private const string FILE_COMMENT		= "Demo Shared User Seettings";

The constants defined above are defined there because this class is the upper level of the hierarchy chain, and the specified values will never change for this particular class (of course, they WILL change for other derived versions of this class, but for our demo app, this is perfectly fine.

    // actual data
    protected string	m_name		= "john";
    protected int		m_age		= 52;
    protected bool		m_old		= true;

    //--------------------------------------------------------------------------------
    public string Name
    {
        get { return m_name; }
        set { m_name = value; }
    }
    //--------------------------------------------------------------------------------
    public int Age
    {
        get { return m_age; }
        set { m_age = value; }
    }
    //--------------------------------------------------------------------------------
    public bool Old
    {
        get { return m_old; }
        set { m_old = value; }
    }
    //--------------------------------------------------------------------------------
    public override XElement XElement
    {
        get
        {
            return new XElement(m_settingsKeyName,
                                new XElement("Name", Name),
                                new XElement("Age",  Age.ToString()),
                                new XElement("Old",  Old.ToString()));
        }
        set
        {
            Name = value.Element("Name").Value;
            Age  = Convert.ToInt32(value.Element("Age").Value);
            Old  = Convert.ToBoolean(value.Element("Old").Value);
        }
    }


    //--------------------------------------------------------------------------------
    public AppSettings(string settingsKeyName, XElement element)
            : base(APP_DATA_FOLDER, APP_DATA_FILENAME, settingsKeyName, FILE_COMMENT, element)
    {
    }

    //--------------------------------------------------------------------------------
    public AppSettings(string appDataFolder, string appDataFilename, string settingsKeyName, 
                       string fileComment, XElement element)
            : base(appDataFolder, appDataFilename, settingsKeyName, fileComment, element)
    {
    }

}

Remember, we have to override the XElement property so we have something to hand off to the load/save functions. I also provided an overloaded constructor so I can manually specify the items that are handled by the constants defined at the top of the class if the need arises.

The DemoSettingsManager Class

I love compartmentalizing things. It wraps up a lot of the ugly code into a nice neat package and centralizes access to internally held components. This class is simply a wrapper around the user settings object and the default settings object.

public class DemoSettingsManager
{
    private AppSettings m_userSettings = null;
    private AppSettings m_defaultSettings = null;

    //--------------------------------------------------------------------------------
    public AppSettings UserSettings
    {
        get { return m_userSettings; }
    }
    //--------------------------------------------------------------------------------
    public AppSettings DefaultSettings
    {
        get { return m_defaultSettings; }
    }

    //--------------------------------------------------------------------------------
    public DemoSettingsManager()
    {
        // create the default settings first
        m_defaultSettings = new AppSettings("DEMO_DEFAULT_SETTINGS", null);
        m_defaultSettings.Load();
        // so we can pass them to the user settings for initial setup
        m_userSettings = new AppSettings("DEMO_USER_SETTINGS", m_defaultSettings.XElement);
        m_userSettings.Load();
    }
}

I could certainly have added the handling of the default settings to this class, buut I was only eager to verify that the AppSettings classes would work as intended. I therefore leave this as an exercise for the programmer.

The Sample Application Solution

To test the code, the solution contains two identical applications. If you run them both at the same time, you'll see how the two applications pull data from the same data file.

History

  • 12/18/2008: Changed the code/article to reflect a comment about making the base class constructor protected instead of making the whole class abstract (with abstract methods). Thanks Navaneeth!
  • 12/17/2008: Original article posted.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here