自动维护会话之间持久变量、属性和数据的类库






4.50/5 (2投票s)
一个类库,它通过 XML 文件在会话之间维护持久变量、属性和数据。它支持新变量的默认值以及 XML 文件中值更改时的事件。

将 AppData.xml 放在演示应用程序项目的 bin/debug 和/或 bin/release 文件夹中,以便使用已创建的员工列表运行演示应用程序。
引言
每当我创建一个 Windows 桌面应用程序时,我总是需要保存应用程序使用的一些数据对象和变量,以便在会话之间保持持久性。需要保存的数据类型通常可以分为 3 类:
- 应用程序处理的数据,其本质是动态的。数据类型在编译时已知,但数量和内容未知。例如,这可以是对象列表 - 演示项目中员工的最小示例列表,例如,要监视更改的目录列表,或者可编辑组合框的动态列表,仅举几例。对于大量数据,数据库更合适,但在普通应用程序中,对少量动态数据有多种需求。数据被加载、维护,然后在更改后保存,而无需进行复杂的交叉索引或需要多字段排序或搜索。
- 配置设置、首选项、属性和选项,它们会更改或配置应用程序的各种方面和运行模式。例如,这可能是一个语言设置、难度设置、默认路径、限制以及要使用的 COM 端口。这些通常是用户通过明确定义的界面(例如选项对话框)更改或设置的设置,然后在应用程序运行时大部分时间保持不变。很多时候,这些设置一旦设定后就不会改变,这对应用程序的使用至关重要。设置通常在应用程序启动时从文件加载,并且仅在用户更改后才保存回文件。
- 由应用程序维护的后台持久变量,这些变量由用户或应用程序状态间接更改。这可能是窗体、对话框或控件的位置、大小和状态,计数器以及最近使用的文件等。这些变量和设置通常在应用程序的生命周期中频繁更改,但仅在应用程序关闭时才保存回文件。它们通常对应用程序的工作方式不那么关键。如果丢失,可以使用设置和变量的默认值。
这个库中的类,称为 RJConfig
,处理以上所有 3 类。此外,它还具有以下功能:
- 数据存储在 XML 文件中。
- 数据在 XML 文件中按 3 级层次结构组织。节包含项,项又包含变量及其值。节代表应用程序的一个区域,项代表一个子区域或一个对象,变量是设置、属性、应用程序变量或对象的成员。
- 属性和应用程序变量(上述类别 2 和 3)被声明并定义为类中的成员变量。声明包含到文件的链接、变量的类型、文件层次结构中的位置以及变量不存在于文件或文件根本不存在时使用的默认值。还可以指定其他信息,具体取决于值的类型,例如限制。
- 变量的值(变量的类型)通过一个类定义,便于添加新类型,即使是复杂类型。
- 到特定文件的链接,该文件是
Config
类的实例,通过static
成员和标准化名称访问。名称代表一个Config
对象及其关联的文件。文件名不直接使用,这与变量声明在其使用位置的事实一起,使得设计非常“面向对象友好”。使用Config
对象的类不必知道文件名,它只需要知道数据是存储为应用程序属性、持久变量还是其他预定义的命名类型。 - 一个属性或应用程序变量的多个实例可以共享文件中的相同值。只需将应用程序成员变量声明为指向相同的文件。当一个实例更改值时,可以触发事件以通知其他实例发生了更改。此机制可用作类之间通信的手段。
Config
对象的关联 XML 文件可以被监视,以检测由外部或内部源进行的更改。每当文件被修改时,都可以为已更改的节、项或单个变量触发事件。这对于允许其他应用程序在运行时更改此应用程序模式的应用程序属性或动态应用程序数据非常有用。这也可以用于共享同时运行的应用程序的多个实例的设置。在一个实例中更改设置,它将立即反映在其他实例中。
背景
这个库已经发展了好几代,最初是基于为 Visual Studio 6 下的 MFC、C++ 应用程序编写的代码,我将其发布为 CodeProject 文章 Non volatile variables and configuration settings for MFC applications。从那时起,它已经为 .NET 和 C# 完全重写,并添加了许多新功能。
Using the Code
库中有两个主要类。第一个是 FileConfig
,另一个是 Config
。
FileConfig 类
FileConfig
类是 XML 文件与应用程序或 Config
类之间的链接。它处理读写,并收集 XML 文件中找到的所有变量。技术上讲,这并不是必需的,因为 XML 类在将文件读入 XmlDocument
时已经完成了。但是,我选择自己创建文件接口,这样可以轻松地更改为另一种文件类型或存储,例如 .INI 文件、注册表或数据库。另一个原因是减少每次访问变量时通过 XmlDocument
的开销。
当需要从 XML 文件加载和保存动态变量或对象时,将直接使用 FileConfig
类。
FileConfig
类由以下类构建:
FileVariable
FileItem:FileItemList:LinkedList<FileVariable>
FileSection:FileItemList:LinkedList<FileItem>
FileConfig:FileSectionDict:Dictionary<string SectionName,FileSection>
FileConfig
对象的构造函数接受它应该关联的 XML 文件名。LoadToFile
函数然后加载 XML 文件内容并填充 FileSection
、FileItem
和 FileVariable
集合。然后可以通过 FindSection
和 FindItem
访问 FileSection
和 FileItem
集合。可以通过 FindVariable
访问变量。请注意,FileSection
集合是一个以其名称作为键的 Dictionary
。这意味着 FileSection
的名称必须是唯一的。由于 FileItem
和 FileVariable
集合是 LinkedLists
,因此它们可以包含同名的 FileItems
和 FileVariables
。
可以使用 Add...
和 Remove...
函数添加或删除 FileSections
、FileItems
和 FileVariables
。如果需要文件更改通知,则必须通过 FileChangeWatcherEnabled
属性启用,并且必须将事件处理程序添加到 OnFileChanged
事件。当 FileConfig
中进行了更改后,可以使用 SaveToFile
函数更新 XML 文件。请注意,如果更改是由其自身的 SaveToFile
函数引起的,则不会引发事件。
演示应用程序使用 FileConfig
来维护一个最小测试类 Employee
的实例列表,其中可以添加、删除和编辑对象。演示应用程序中还启用了文件更改通知,因此对 XML 文件所做的任何更改(无论是从文本编辑器还是从演示应用程序的另一个实例进行的)都会立即在应用程序中反映并更新。
有关 FileConfig
类所有函数和属性的更多信息,请参阅源代码中的注释。演示应用程序也是一个有用的资源,可以了解 FileConfig
类的工作原理,并且此处不重复代码。
Config 类
当您想要保存和恢复在编译时已知并且必须在应用程序会话之间保持持久性的变量和设置时,将使用 Config
及其相关类。由于 XML 文件中的值都存储为 string
,并且只有变量本身知道如何与该类型的值和 string
之间进行转换,因此每个变量类型都有自己的类。库中存在的变量类型有:
CfgVarString
-string
类型CfgVarBool
- 布尔类型CfgVarNum
-int
类型CfgVarNumLimit
-int
类型,限制在最小值和最大值之间CfgVarEnum
-enum
类型CfgVarDouble
- 浮点double
类型CfgVarXmlDoc
-XmlDocumentType
,允许以 XML 格式保存和加载对象。有关更多信息和使用此值类型的示例,请参阅演示应用程序。
其他类型可以轻松添加。只需为值类型创建一个类,为变量创建一个类 - 有关如何执行此操作的更多信息,请参阅 ConfigValueTypes.cs。
还有两种组合变量类型,它们包含多个值类型和各种辅助函数:
CfgVarDlgWindowPos
- 一个变量,包含保存Form
位置和可见状态的值。这在DlgPosSave
类中使用,该类是Form
的基类,用于作为无模式对话框使用,具有固定大小,例如工具窗口或显示后台处理时可见的窗口。CfgVarWindowPos
- 一个变量,包含保存Form
位置、大小以及最大化/普通状态的值。这在FormPosSave
类中使用,该类是Form
的基类,其大小、位置和状态将在创建之间被记住。这用作演示应用程序中MainForm
的基类。
也可以创建组合类型,其中所有成员都被转换为 XML 文件中的单个 string
(甚至是一个包含 XML 节点的 string
)。
Config
类由以下类构建:
CfgValueType...:CfgValueType<T>
CfgVar<CfgValueTypeT,T>:CfgVarNode
Item:VariableDict<string VariableName,CfgVarNode>
Section:ItemDict:Dictionary<string ItemName,Item>
SectionDict:Dictionary<string SectionName,Section>
Config
(它有一个SectionDict
作为成员)
Config
类有一组 static
成员来访问添加到集合中的 Config
对象集合,使用 AddInstance
添加。该集合是 Dictionary
,键是 Config
对象的名称。默认创建了两个 Config
类的实例,一个用于应用程序变量,一个用于属性。这些实例的名称是 APP_VARIABLES
和 APP_PROPERTIES
,可以通过 Config
类的 AppPropInst
和 AppVarInst
静态属性访问。这些 Config
对象的实际 XML 文件名通过 Init
函数确定。这通常在 Program.cs 文件中完成。
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();
}
}
}
完成此操作后,任何类都可以通过以下方式访问用于变量和属性的 Config
对象:
Config cProp=Config.AppPropInst;
Config cVar=Config.AppVarInst;
要添加与 XML 文件连接的变量,请使用以下语法:
CfgVarNum NumberProp=
new CfgVarNum(Config.AppPropInst,"MainForm","Properties","Number",10);
将“MainSection
”、“Properties
”和“Number
”替换为节、项和变量的相关名称。创建变量时,它会自动加载文件中的值,或者,如果变量或文件不存在,则获得默认值(如上例中的 10)。从现在开始,可以通过值属性 CfgData
访问变量值。
NumberProp.CfgData=100;
int i=NumberProp.CfgData;
由于值类型是 CfgValueTypeNum:CfgValueType<int>
,因此 CfgData
是一个 int
。
如果 XML 文件中只有此变量,则文件将具有以下内容:
<?xml version="1.0"?>
<Config>
<Section Name="MainForm">
<Item Name="Properties">
<Variable Name="Number">
<Value>100</Value>
</Variable>
</Item>
</Section>
</Config>
如果现在创建另一个具有完全相同签名的 CfgVarNum
,例如 NumberProp2
,它将共享相同的值类型。这与 NumberProp2
在何处创建无关,只要它使用相同的 Config
对象即可。现在可以为其中一个(或两个)变量连接一个事件处理程序到 OnValueChanged
事件,如果值被另一个变量更改,则会引发事件。
NumberProp2.OnValueChanged+=new(VariableValueChanged(HandlerForNumberProp2));
NumberProp.CfgData=11; // This will raise the event for NumberProp2
// (but not for NumberProp).
在变量中更改的值不会直接更新到文件。它甚至不会更新到该文件的底层 FileConfig
对象。可以使用 FromFileConfig
和 ToFileConfig
单个变量读写到 FileConfig
对象。要将整个节保存到 FileConfig
,请使用 Save
。SaveAll
会同时更新 FileConfig
并将其保存到 XML 文件。使用 Flush
将 FileConfig
的当前状态保存到 XML 文件。
有一个内部标志用于跟踪 FileConfig
何时已更改。SaveIfDirty
函数使用此标志,并且仅在需要时才更新 XML 文件。
还有一个内部标志用于跟踪何时向 FileConfig
添加了新变量。这不会设置脏标志,因为变量将获得默认值,无需保存。如果您仍想将 XML 文件更新为新的、未更改的变量,请使用 SaveIfNewVariables
函数。这会为所有 Config
实例保存新变量。
如何通用处理应用程序变量的保存
在更改值时使用 Save
(完整节)或 ToFileConfig
(单个变量)更新 FileConfig
。这可以在变量更改时直接完成,也可以在应用程序退出时(或拥有变量的对象被处置时)完成。应用程序退出时,可以执行 Flush
或 SaveIfDirty
,可能与 SaveIfNewVariables
一起使用。这样,应用程序变量 XML 文件仅在应用程序关闭时写入。或者,可以更新 FileConfig
并不时地执行 SaveIfDirty
,当应用程序空闲时。
如何通用处理应用程序属性的保存
在属性或选项用户界面中更改属性或选项的值时,不要更新 FileConfig
。这样,如果用户在不重新读取 XML 文件的情况下取消操作,旧值可以从 FileConfig
恢复。当用户完成更改属性或选项后,使用 SaveAll
更新 FileConfig
。现在 XML 文件已更新。退出应用程序时不要再次保存。这样,属性的 XML 文件只有在需要时才会被写入。这对于电池供电的计算机尤其重要,这些计算机可能会在应用程序终止过程中关闭(所有应用程序和所有进程同时开始写入磁盘,这会消耗更多电流并立即关闭计算机),这可能导致属性文件损坏。这可能不适用于笔记本电脑,它们可以很好地监控电池状态,但可能发生在车辆的计算机上,这些计算机已开启但车辆发动机关闭时耗尽了电池。
属性变量的文件更改通知
Config
类具有监视底层 FileConfig
对象及其 XML 文件更改的机制,并在对节、项或变量进行更改时通知应用程序。
为要监视的节、项或变量添加事件处理程序,并将 Config
对象的 FileWatchEnabled
属性设置为 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;
尽管通常只需要上述事件处理程序之一。
每当 XML 文件发生更改时,都会引发相应的事件。请注意,仅当变量、项或节确实发生更改时才会调用事件。在上面的示例中,将首先调用 NumProp_OnConfigFileVariableChanged
,然后是 Item_OnConfigFileItemChanged
,最后是 Section_OnConfigFileSectionChanged
。还请注意,事件不是从 GUI 线程调用的,因此如果需要操作任何 GUI 对象,则必须为该 Control
或 Form
调用 Control.Invoke
。
当调用事件处理程序时,FileConfig
已根据更改的 XML 文件进行了更新。但是,Config
变量值尚未更改。必须使用 FromFileConfig
(或 ToFileConfig
,如果更改要被拒绝)进行更改。否则,当文件再次更改时,即使该变量在文件中未更改,处理程序也会再次被调用。
文件更改通知主要对属性有用,因为这些属性通常始终与 FileVariables
和 XML 文件本身同步。另一方面,应用程序变量可能不同步,这可能导致错误通知。
还有一个事件,在文件发生更改时引发。此事件称为 OnConfigFileChanged
,即使没有实际更改任何变量也会引发。
演示应用程序为应用程序属性变量启用了文件通知。在记事本或其他文本编辑器中打开 ConfigDemoProp.xml 文件,更改变量值并保存文件。更改将直接反映在应用程序中。另一种演示此功能的方法是启动演示应用程序的两个实例。在其中一个正在运行的应用程序实例中更改属性变量(在 Config
变量演示分组框中),另一个实例中的控件也会更新。
有关 FileConfig
和 Config
类的更多信息,请参阅这些类所有函数和属性的源代码中的注释。演示应用程序也是一个了解类如何与 XML 文件和应用程序交互的有用来源。
关注点
我通过这些类工作时艰难地注意到的一件事是,系统会多次引发文件更改事件,并且在引发事件时,文件可能尚未关闭。我为此进行了变通,首先尝试以读访问模式打开文件,直到可以打开为止,或者已过一秒的超时。
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
如果文件在一秒内无法访问,则此次文件更改通知将被跳过。
历史
- 2008-01-11 版本 1.3
- 更改了
FileConfig
中 XML 文件的加载和保存,使其能够将嵌套的 XML 元素保存为值。 - 当找到有效的 XML 元素作为值时,使用
InnerXml
而不是InnerText
。 - 添加了
XmlDocument
和Double
值类型变量。 - 2008-01-09 版本 1.2
RJConfig
库的第一个发布版本。