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 null
ness. 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;
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
{
appDataPath = System.Environment.GetFolderPath(SpecialFolder);
}
catch (Exception ex)
{
throw ex;
}
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
{
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.
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()
{
m_defaultSettings = new AppSettings("DEMO_DEFAULT_SETTINGS", null);
m_defaultSettings.Load();
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.