Put the AppData.xml in the bin/debug and/or bin/release of the demo application project in order to run the demo application with an already created List of employees.
Introduction
Whenever I make a Windows desktop application, I always need to save some of the data objects and variables used by the application in order to keep them persistent between sessions. The type of data that needs to be saved can usually be divided into 3 categories:
- Data that the application works with, which is dynamic by nature. The datatype is known at compile time but the number and content is not. This can for example be lists with objects - The minimal example list of employees in the demo project for example, a list of directories to watch for changes in or a dynamic list for an editable combobox to just mention some. For larger amounts of data, a database is more appropriate but there are several needs for smaller amounts of dynamic data in a normal application. Data that is loaded, maintained and then saved if changed without any fancy crossindexing or need for multi field sorting or searching.
- Configuration settings, preferences, properties and options that change or configure different aspects and operating modes of the application. This can for instance be a language setting, a difficulty setting, default paths, limits and which COM port to use. These are settings that normally are changed or set by the user with well defined interfaces, an options dialog for example, and then left alone for the majority of the application run time. Many times it is also critical for the use of the application that these settings don't change once they are set. The settings are normally loaded from file at application start and then saved back to file only when they are changed by the user.
- Background persistent variables that are entirely maintained by the application, indirectly changed by the user or the state of the application. This could be position, size and state of forms, dialogs or controls, counters and last used files to mention some. These variables and settings normally change frequently during the lifetime of the application but are only saved back to file when the application is closed. They are normally not critical for the way the application works. If lost, default values for the settings and variables can be used.
The classes in this library, called RJConfig
, handles all 3 categories above. In addition to that, it also has the following features:
- Data is stored in XML files.
- Data is organized in a 3 level hierarchy in the XML file. Sections contain Items which in turn contain Variables and their values. The section represents an area of the application, the item represents a sub area or an object and the variable is the setting, property, application variable or a member of an object.
- The properties and application variables (cat. 2 and 3 above) are declared and defined as member variables in the class it is used. The declaration contains the link to the file, the type of the variable, the location in the file hierarchy and a default value which is used if the variable doesn't already exist in the file or the file doesn't exist at all. Other information, depending on the type of the value, limits for example, can also be specified here.
- A value of a variable (the type of the variable) is defined with a class, making it easy to add new types. Even complex types.
- The link to a specific file, which is an instance of a
Config
class, is accessed through static
members and standardized names. The name represents a Config
object and its associated file. The filename is not used directly which, together with the fact that variables are declared where they are used, makes the design very "object oriented friendly". The class that uses the Config
object doesn't have to know the filename, it only needs to know if the data is to be stored as an application property, a persistent variable or any other predefined, named type. - More than one instance of a property or application variable can share the same value in the file. Just declare the application member variable with the same link to the file. When one instance changes the value, events can be raised to inform the other instances that a change has taken place. This mechanism can be used as a means of communication between classes.
- The associated XML file for a
Config
object can be monitored for changes made by external or internal sources. Whenever the file has been modified, events can be raised for the Section, Item or the single Variable that has been changed. This is useful for application properties or dynamic application data which allows other applications to change the mode of this application, at runtime. This can also be used to share the settings for several instances of one application running simultaneously. Change the setting in one instance and it will immediately be reflected in the other instances.
Background
This library has evolved in several generations and is originally based on code written for MFC, C++ applications under Visual Studio 6 which I also released as a CodeProject article named Non volatile variables and configuration settings for MFC applications. Since then it has been totally remade for .NET with C# and has had a lot of new features added.
Using the Code
There are two main classes in the library. The first is the FileConfig
and the other is the Config.
The FileConfig Classes
The FileConfig
classes are the link between the XML file and the application or the Config
class. It handles reading and writing and makes collections of all variables found in the XML file. Technically, this would not be needed since the XML classes already do this when the file is read into an XmlDocument
. However, I have chosen to make my own interface to the file since then it is fairly easy to change to another type of file or storage, for example an .INI file, the registry or a database. Another reason is to reduce the overhead of going through the XmlDocument
every time the variable needs to be accessed.
The FileConfig
class is used directly when dynamic variables or objects are to be loaded and saved from and to the XML file.
The FileConfig
class is built from the following classes:
FileVariable
FileItem:FileItemList:LinkedList<FileVariable>
FileSection:FileItemList:LinkedList<FileItem>
FileConfig:FileSectionDict:Dictionary<string SectionName,FileSection>
The constructor of the FileConfig
object takes the name of the XML file it should be associated with. The function LoadToFile
then loads the contents of the XML file and populates the FileSection
, FileItem
and FileVariable
collections. The FileSection
and FileItem
collections can then be accessed with FindSection
and FindItem
. A variable can be accessed with FindVariable
. Note that the FileSection
collection is a Dictionary
with its name as the key. This means that the name of the FileSection
must be unique. Since the FileItem
and FileVariable
collections are LinkedLists
they can contain FileItems
and FileVariables
with the same name.
FileSections
, FileItems
and FileVariables
can then be added or removed with the Add...
and Remove...
functions. If file change notification is wanted, it has to be enabled with the property FileChangeWatcherEnabled
and an event handler has to be added to the OnFileChanged
event. When changes have been made to the FileConfig
, the XML file can be updated with the SaveToFile
function. Note that no event will be raised if the change is a result from its own SaveToFile
function.
The demo application uses the FileConfig
to maintain a list of instances of the minimal test class Employee
where objects can be added, removed and edited. The file change notification is also enabled in the demo application so any changes made to the XML file, either from a text editor or from another instance of the demo application, is instantly reflected and updated in the application.
See the comments in the source code for all functions and properties of the FileConfig
classes. The demo application is also a useful source to see how the FileConfig
classes work and the code isn't repeated here.
The Config Classes
The Config
and its related classes are the ones to be used when you want to save and restore variables and settings that are known at compile time and whose values have to be kept persistent between the sessions of the application. Since the values in the XML file are all stored as string
s and it is only the type of the variable itself that knows how to convert back and forth between the value of that type and the string
, each Variable type has its own class. The variable types that exists in the library are:
CfgVarString
- A string
type CfgVarBool
- A boolean type CfgVarNum
- An int
type CfgVarNumLimit
- An int
type, limited between a minimum and a maximum value CfgVarEnum
- An enum
type CfgVarDouble
- A floating point double
type CfgVarXmlDoc
- An XmlDocumentType
which allows saving and loading objects formatted as XML. See the demo application for more information and example of how to use this value type.
Other types can be easily added. Just create a class for the value type and one class for the variable - See the ConfigValueTypes.cs for more information on how this is done.
There are also two combined variable types that hold several value types and various accessory functions:
CfgVarDlgWindowPos
- A variable that contains values to save a Form
position and visible state. This is used in the DlgPosSave
class which is a base class to a Form
that is to be used as a modeless dialog with fixed size, such as a tool window or a window that can show background processing when visible. CfgVarWindowPos
- A variable that contains values to save a Form
position, size and maximized/normal state. This is used in the FormPosSave
class which is a base class to a Form
whose size, position and state is going to be remembered between creations. This is used as the base class for the MainForm
in the demo application.
Combined types can also be created where all of its members are converted to a single string
in the XML file (or even a string
containing XML nodes).
The Config
class is built from the following classes:
CfgValueType...:CfgValueType<T>
CfgVar<CfgValueTypeT,T>:CfgVarNode
Item:VariableDict<string VariableName,CfgVarNode>
Section:ItemDict:Dictionary<string ItemName,Item>
SectionDict:Dictionary<string SectionName,Section>
Config
(which has a SectionDict
as a member)
The Config
class has a set of static
members to access a collection of Config
objects added to the collection with AddInstance
. The collection is a Dictionary
and the key is the name of the Config
object. There are two instances of the Config
class created by default, one is for application variables and one is for properties. The name for these instances are APP_VARIABLES
and APP_PROPERTIES
and they can be accessed through the AppPropIns
t and AppVarInst static
properties of the Config
class. The name for the actual XML file for these Config
objects is determined with the Init
function. This is normally done in the Program.cs file
static class Program
{
[STAThread]
static void Main ()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Config.Init("ConfigDemoProp.xml", "ConfigDemoVar.xml");
try {
Application.Run(new MainForm());
} finally {
Config.AppVarInst.SaveIfDirty();
Config.AppPropInst.SaveIfDirty();
Config.SaveIfNewVariables();
}
}
}
Once this is done, any class can access the Config
objects for variables and properties just by doing:
Config cProp=Config.AppPropInst;
Config cVar=Config.AppVarInst;
In order to add a variable that is connected to the XML file, use the following syntax;
CfgVarNum NumberProp=
new CfgVarNum(Config.AppPropInst,"MainForm","Properties","Number",10);
Replace the "MainSection
", "Properties
" and "Number
" with relevant names for the Section, Item and Variable. When the variable is created, it is automatically loaded with the value in the file or, if the variable or the file doesn't exist, it gets the default value (10 in the example above). From now on, the variable value can be accessed with the value property CfgData
:
NumberProp.CfgData=100;
int i=NumberProp.CfgData;
Since the value type is a CfgValueTypeNum:CfgValueType<int>
the CfgData
is an int
.
If only this variable exists in the XML file, the file will have the following content:
="1.0"
<Config>
<Section Name="MainForm">
<Item Name="Properties">
<Variable Name="Number">
<Value>100</Value>
</Variable>
</Item>
</Section>
</Config>
If now another CfgVarNum
with the exact same signature is created, say NumberProp2
, it will share the same value type. This is independent of where NumberProp2
is created as long as it uses the same Config
object. An event handler can now be connected to the OnValueChanged
event for one (or both) of the variables and if the value is changed by the other variable, the event is raised.
NumberProp2.OnValueChanged+=new(VariableValueChanged(HandlerForNumberProp2));
NumberProp.CfgData=11;
A value that is changed in a variable is not directly updated to the file. It is not even updated to the underlying FileConfig
object for that file. A single variable can be read and written from and to the FileConfig
object with FromFileConfig
and ToFileConfig
. To save an entire section to the FileConfig
use Save
. SaveAll
both updates the FileConfig
and saves it to the XML file. Use Flush
to save the current state of the FileConfig
to the XML file.
There is one internal flag that keeps track of when the FileConfig
has been changed. The function SaveIfDirty
uses this flag and only updates the XML file if needed.
There is another internal flag that keeps track of when a new variable has been added to the FileConfig
. This doesn't set the dirty flag since the variable then gets the default value and no saving needs to be done. If you want to update the XML file with new, unchanged variables anyway, use SaveIfNewVariables
function. This saves new variables for all Config
instances.
How to Generally Handle Saving of Application Variables
Update the FileConfig
when the value is changed with Save
(a complete section) or ToFileConfig
(a single variable). This can be done either directly when the variable is changed or when the application exits (or when the object that owns the variable is disposed). When the application exits, do Flush
or SaveIfDirty
perhaps together with SaveIfNewVariables
. This way the application variable XML file is only written to when the application closes. Alternatively update the FileConfig
and do a SaveIfDirty
now and then when the application is idle.
How to Generally Handle Saving of Application Properties
Do not update the FileConfig
when the value of the property or option is changed in the properties or options user interface. This way the old value can be restored from the FileConfig
if the user cancels the operation without rereading the XML file. When the user is done changing the properties or options, update the FileConfig
with SaveAll
. Now the XML file is updated. Do not save again when the application exits. This way the XML file for the properties is not written to unless it is needed. This can be especially important for battery operated computers that can shut down when the application is in the process of terminating (all applications and all processes starts to write to disks at the same time which takes more current and immediately shuts down the computer) which could result in a corrupt properties file. This may not be the case for laptops that fairly well can monitor its battery status but it could happen (and has happened) for computers in vehicles that have been left on and drained the battery when the vehicle engine is off.
File Change Notification for Property Variables
The Config
class has mechanisms to watch the underlying FileConfig
object and its XML file for changes and can notify the application when a change has been made to a Section, Item or Variable.
Add an event handler for the Section, Item or Variable to watch and set the FileWatchEnabled
property of the Config
object to true
:
NumProp.OnConfigFileVariableChanged+=
new ConfigFileVariableChanged(NumProp_OnConfigFileVariableChanged);
NumProp.Section.OnConfigFileSectionChanged+=
new ConfigFileSectionChanged(Section_OnConfigFileSectionChanged);
NumProp.Item.OnConfigFileItemChanged+=
new ConfigFileItemChanged(Item_OnConfigFileItemChanged);
NumProp.cfg.FileWatchEnabled=true;
Normally only one of the above eventhandlers is needed though.
Whenever the XML file is changed, the appropriate event will be raised. Note that the event is only risen if the Variable, Item or Section is really changed. In the above example, the NumProp_OnConfigFileVariableChanged
will be called first, followed by Item_OnConfigFileItemChanged
and last Section_OnConfigFileSectionChanged
. Note also that the events are not called from the GUI thread so if any GUI objects are to be manipulated, a Control.Invoke
has to be called for that Control
or Form
.
When the event handler is called, the FileConfig
is already updated according to the changed XML file. The Config
variable value is not changed though. It has to be changed with FromFileConfig
(or ToFileConfig
if the change is to be rejected). Otherwise the handler will be called again the next time the file is changed even if that variable in the file isn't changed the second time.
File change notification is mostly useful for properties since these are normally always synched with both the FileVariables
and the XML file itself. Application variables on the other hand might not be synched which may result in false notifications.
There is also an event that is raised whenever the file is changed. This event is called OnConfigFileChanged
and is raised even if no variable is actually changed.
The demo application has file notifications enabled for application property variables. Open the ConfigDemoProp.xml file in NotePad or any other text editor, change a variable value and save the file. The change will be reflected directly in the application. Another way to demonstrate this is to start two instances of the demo application. Change a property variable (in the Config
variables demo group box) in one of the running instances of the application and the control will also be updated in the other instance.
For more information about the FileConfig
and Config
classes, see the comments in the source code for all functions and properties of these classes. The demo application is also a useful source to see how the classes interact with the XML file and the application.
Points of Interest
One thing I noticed the hard way when working with these classes is that the file change events are raised several times by the system and that when the event is raised, the file may not be closed. I did a workaround for this by first trying to open the file with read access until it could be opened or a timeout of one second has expired:
FileStream fs=null;
int tries = 0;
while (fs == null & (tries < 10)) {
try {
fs = File.Open(e.FullPath, FileMode.Open, FileAccess.Read, FileShare.None);
} catch {
}
if (fs == null) {
System.Threading.Thread.Sleep(100);
}
tries++;
}
if (fs != null) {
fs.Close();
If the file isn't accessible within one second, the file change notification is just skipped for now.
History
- 2008-01-11 Version 1.3
- Changed the loading and saving of the XML file in
FileConfig
to be able to save nested XML elements as values - Uses
InnerXml
instead of InnerText
when a valid XML element is found as the value - Added the
XmlDocument
and Double
value type variables
- 2008-01-09 Version 1.2
- First released version of the
RJConfig
library