Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Class Library to Automatically Maintain Persistent Variables, Properties and Data Between Sessions

4.50/5 (2 votes)
11 Jan 2008CPOL15 min read 1   594  
A class library that maintains persistent variables, properties and data between sessions in XML files. It supports default values for new variables and events when the values in the XML file are changed
rjconfig.gif

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:

  1. 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.
  2. 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.
  3. 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 strings 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 AppPropInst 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

C#
static class Program
{    
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [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:

C#
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;

C#
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:

C#
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:

XML
<?xml version="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.

C#
NumberProp2.OnValueChanged+=new(VariableValueChanged(HandlerForNumberProp2));
NumberProp.CfgData=11; 	// This will raise the event for NumberProp2 
			// (but not for NumberProp).

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:

C#
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:

C#
FileStream fs=null; // Have to wait until the file is really closed.
int tries = 0;
while (fs == null & (tries < 10)) { // Wait maximum 1000ms (theoretical...)
    try {
        fs = File.Open(e.FullPath, FileMode.Open, FileAccess.Read, FileShare.None);
    } catch {
    }
    if (fs == null) {
        System.Threading.Thread.Sleep(100); 	// This is done in a background 
					// thread so it won't affect the GUI.
    }
    tries++;
}
if (fs != null) {
    fs.Close();
    // Load the xml file here and raise the event here

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)