为懒人设计的自定义配置节






4.93/5 (80投票s)
在不深入理解的情况下使用自定义配置节……尽可能地。
引言
本文介绍了一些我在尝试使用自定义配置节时找到的解决方案,并解释了如何简化这一过程,使其对那些没时间彻底学习它们的初学者更加友好。我非常感谢许多作者的文章,它们关于配置节的内容帮助我最终理解了 MSDN 文档中含糊不清的部分。阅读其中一两篇文章的介绍材料(例如 John Rista 的 Mysteries of Configuration 和 Alois Kraus 的 Read/Write App.Config File with .NET 2.0 是很好的资源),有助于理解下面的内容,尽管并非必需。
这里的信息呈现方式反映了我代码的演变过程,尽管比我实际经历的更有条理。每一步都增加了更多的功能和理解。当您阅读这个过程时,请随意跳到满足您需求的层面。如果您知道或找到一种更简单的方法来完成某项工作,请告诉我,以便我将其添加到我的知识库中并在本文的更新中包含。
背景
配置文件是用于存储与应用程序行为相关数据的 XML 文档。文件名是应用程序的 EXE 文件名加上“.config”扩展名,例如 *MyProg.exe.config*。当您向项目中添加一个“app.config”文件时,它的内容将成为这个“exe.config”文件的内容。
最简单,也是最无用的配置文件,只包含 XML 文档的基本结构。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>
初学者通常第一次使用配置文件时会利用 appSettings
部分。这是一个预定义的节,.NET 框架内置了一些访问工具。在此部分,您可以存储“通常”不会修改的参数,但在特殊情况下,您希望能够在不重新构建应用程序的情况下修改它们。一个简单的用法如下
<?xml version="1.0" encoding="utf-8" >
<configuration>
<appSettings>
<add key="Port" value="COM2"/>
<add key="Level" value="3"/>
</appSettings>
</configuration>
这定义了两个应用程序可以使用以下代码访问的参数
string port = Configuration.AppSettings["Port"];
int mevLevel = int.Parse(Configuration.AppSettings["Level"]);
您可以分发应用程序时在配置文件中包含一组默认值,并在特定安装时根据需要修改这些值。请注意,appSettings
部分中的参数是以字符串形式存储和返回的。在转换为其他类型时,请务必提供适当的异常处理。
如果您想允许用户修改这些值,也可以通过编程方式更新它们,尽管这不像您期望的那样直接。您需要实例化一个 Configuration
对象才能保存更改,并且属性值必须通过其他途径访问。这已经超出了我们解释的范围,但代码如下
Configuration config =
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
config.AppSettings.Settings["Port"].Value = "COM4";
config.Save(ConfigurationSaveMode.Modified);
应用程序配置文件与其他框架已知的配置文件结合在一起,构成了完整的 Configuration
对象,但对该信息感兴趣的人已经远远超出了我想要在此解决的复杂性。
“Hello, World”配置节
我涉足自定义配置节的开始,是认识到我们应用程序的配置文件中的 appSettings
部分变得杂乱无章,包含大量无序数据。这是自定义节存在的原因之一。第一个迭代创建了一个派生自 ConfigurationSection
的简单类,大致如下
public class SomeSettings : ConfigurationSection
{
private SomeSettings() { }
[ConfigurationProperty("FillColor", DefaultValue="Cyan")]
public System.Drawing.Color FillColor
{
get { return (System.Drawing.Color)this["FillColor"]; }
set { this["FillColor"] = value; }
}
[ConfigurationProperty("TextSize", DefaultValue="8.5")]
public float TextSize
{
get { return (float)this["TextSize"]; }
set { this["TextSize"] = value; }
}
[ConfigurationProperty("FillOpacity", DefaultValue="40")]
public byte FillOpacity
{
get { return (byte)this["FillOpacity"]; }
set { this["FillOpacity"] = value; }
}
}
这非常基础,允许我们使用如下所示的配置文件
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="SomeSettings" type="MyApp.SomeSettings, SomeSettings" />
</configSections>
<SomeSettings FillColor="LightBlue" TextSize="9.5" FillOpacity="50" />
</configuration>
我们定义的设置将显示为一个 XML 节点,其属性对应于我们包含的属性。还有一个名为 configSections
的新节,它向框架描述了我们的新 XML 节点。(理解 section
节点需要一些工作,我不想让您重复。暂时忽略该节,稍后我们会处理。)您现在可以通过以下方式在代码中访问这些数据
Configuration config =
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
SomeSettings gui = (SomeSettings)config.Sections["SomeSettings"];
float fSize = gui.TextSize;
请注意,即使配置节中的值是字符串,SomeSettings
类的属性也能让我们以类型安全的方式访问参数。解析工作已经为您完成。还可以将验证属性(简单和复杂的)应用于属性,以减少我们在代码中需要进行的验证工作。可以查看 MSDN 关于 IntegerValidator
的文档,作为理解这些内容的一个起点。
如果您只需要少量项,则不需要这样做,可以坚持使用内置的 appSettings
部分。我需要组织的东西更多。
添加更多数据
向 SomeSettings
类添加更多属性会向 XML 节点添加更多属性,最终使文件变得相当混乱。我们正在寻找易于阅读的组织方式。添加更多类来保存其他数据会在 configSections
下为每个类添加另一个 section
节点。如果您的数据符合该组织方式,那么这就是您想要的方法,但我希望将一组相关的子组分组到一个节中。这可以通过派生自 ConfigurationElement
类来实现。
public class SomeSettings : ConfigurationElement
{
private SomeSettings() { }
[ConfigurationProperty("FillColor", DefaultValue="Cyan")]
public System.Drawing.Color FillColor
{
get { return (System.Drawing.Color)this["FillColor"]; }
set { this["FillColor"] = value; }
}
[ConfigurationProperty("TextSize", DefaultValue="8.5")]
public float TextSize
{
get { return (float)this["TextSize"]; }
set { this["TextSize"] = value; }
}
[ConfigurationProperty("FillOpacity", DefaultValue="40")]
public byte FillOpacity
{
get { return (byte)this["FillOpacity"]; }
set { this["FillOpacity"] = value; }
}
}
请注意,ConfigurationElement
类看起来与 ConfigurationSection
类一样。我们所做的唯一更改是更改了基类。我们的收益来自于将该元素用作 ConfigurationSection
中的 ConfigurationProperty
。
public class MySection : ConfigurationSection
{
private MySection() { }
[ConfigurationProperty("Sector")]
public SomeSettings SectorConfig
{
get { return (SomeSettings)this["Sector"]; }
}
}
exe.config 文件现在可以包含如下内容
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="MySection" type="MyApp.MySettings, MySection" />
</configSections>
<MySection>
<Sector FillColor="Cyan" TextSize="8.5" FillOpacity="40" />
</MySection>
</configuration>
请注意,我现在可以在配置文件中使用节点名称“Sector”,而不是以前的“SomeSettings”。我还可以给相应的属性起一个不同的名称,例如“SectorConfig”。这足以让您使用以下代码访问配置字段
Configuration config =
ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
MySection gui = (MySection)config.Sections["MySection"];
float fSize = gui.SectorConfig.TextSize;
SomeSettings set1 = gui.SectorConfig;
在这个级别上,它看起来并没有比第一个版本好多少,但现在,我可以创建其他 ConfigurationElement
类并将它们添加到 MySection
中,而不会使文件更难理解,如下所示。
利用懒惰
如上所述,我不想费力弄清楚配置文件中的 <configSections>
部分,尤其是当我可以让系统为我处理时。我们还希望有一种更方便的方式来访问该节,而无需每次在新代码部分需要这些参数时都调用 OpenExeConfiguration
。对 MySection
类的以下修改提供了这一点,以及一些重要的灵活性
public static MySection Open()
{
System.Reflection.Assembly assy =
System.Reflection.Assembly.GetEntryAssembly();
return Open(assy.Location);
}
public static MySection Open(string path)
{
if ((object)instance == null)
{
if (path.EndsWith(".config",
StringComparison.InvariantCultureIgnoreCase))
path = path.Remove(path.Length - 7);
Configuration config =
ConfigurationManager.OpenExeConfiguration(path);
if (config.Sections["MySection"] == null)
{
instance = new MySection();
config.Sections.Add("MySection", instance);
config.Save(ConfigurationSaveMode.Modified);
}
else
instance = (MySection)config.Sections["MySection"];
}
return instance;
}
#region Fields
private static MySection instance;
#endregion Fields
现在,我们可以使用此代码访问该节
MySection gui = MySection.Open();
float fSize = gui.SectorConfig.TextSize;
SomeSettings set1 = gui.SectorConfig;
以下功能值得在此处注意
- 配置文件不必包含任何自定义数据即可正常工作。当我们第一次访问此
MySection.Open()
方法时,框架会自动将所有必要的开销添加到配置文件中。太棒了。如果您为所有属性定义了默认值,那么使用自定义节只需要这些。(而且,下面的内容会更棒。) - 配置节存储在静态变量中进行访问和保存。我们代码的其他部分可以调用相同的
MySection.Open()
方法并获取相同的数据集。在一个代码部分所做的更改可以被其他部分读取。 - 通过包含重载
MySection.Open(string path)
,我可以访问任何我选择的配置文件中的ConfigurationSection
。稍后您将看到我如何利用这一点。在这种情况下,path.EndsWith(...)
的测试非常有用。
用户自定义和保存
既然我们可以轻松访问一些组织良好的数据,如果能让用户自定义他们的设置而不必手动编辑配置文件,那就更好了。这需要我们有一种保存更改设置的方法。我在讨论 appSettings
部分时已经提到过这一点。在与多线程中访问 Section 相关的几次试错(有时应用程序需要重启才能使更改生效)之后,我最终得到了下面的 Save
方法,它能够跨线程边界工作,就像我想要的
public class MySection : ConfigurationSection
{
...
public void Save()
{
Configuration config =
ConfigurationManager.OpenExeConfiguration(spath);
MySection section = (MySection)config.Sections["MySection"];
section.SectorConfig = this.SectorConfig; //Copy the changed data
config.Save(ConfigurationSaveMode.Full);
}
[ConfigurationProperty("Sector")]
public SomeSettings SectorConfig
{
get { return (SomeSettings)this["Sector"]; }
set { this["Sector"] = value; } //***Added set accessor
}
#region Fields
private static string spath; //***Added saved file path
private static MySection instance;
#endregion Fields
}
上面未显示的是,静态 spath
字段是从 Open(string path)
方法中编辑的字符串参数填充的。Save
方法打开配置文件,将 Section 当前实例的所有属性值复制到新打开的 Section 中,然后将更改写回文件。为了能够赋值属性,我们在 SectorConfig
属性中添加了 set
访问器。(我早期版本尝试将 Configuration
对象保持为静态,但没有产生预期的结果,并且它必须是一个动态对象。)现在保存新值非常简单
MySection settings = MySection.Open();
// Make changes to the values as needed
settings.SectorConfig.FillOpacity = (byte)75;
// and then... save them
settings.Save();
由此产生的一个显著好处是 config.Save
调用中的 ConfigurationSaveMode.Full
参数。因此,当我们第一次将 Save
保存到空配置文件时,框架将为我们创建完整的节,包含我们指定的所有默认值,并且格式精美。真是太好了!我使用此功能生成行,然后将它们复制到我的 app.config 文件中。请注意,此技巧要求所有属性都有默认值。您可以将模式保留为 Full
,或者将其更改为 Modified
;我没有注意到任何明显的性能差异。
作为在 Save
方法中为 SectorConfig
属性(以及您以后可能添加的每个其他属性)赋值的替代方法,我一直在尝试以下代码,这在我在另一个应用程序中重复时需要更少的自定义编辑
public void AltSave()
{
Configuration config = ConfigurationManager.OpenExeConfiguration(spath);
MySection section = (MySection)config.Sections["MySection"];
section.LockItem = true;
foreach (ConfigurationProperty prop in section.Properties)
{
string name = prop.Name;
section.SetPropertyValue(section.Properties[name], this[name], false);
}
config.Save(ConfigurationSaveMode.Full);
}
我不能断言这总是有效的,但到目前为止,在我进行的几次测试中,即使是嵌套元素(请参阅下面的添加数据复杂性),它也是有效的。调用 section.SetPropertyValue(...)
是必需的,因为 section.Properties
集合是只读的,并且其元素不能直接修改。
防出错
如果用户现在可以更改(即弄乱)设置,您需要提供一种方法来恢复默认值。这可以通过以下添加轻松实现
public static MySection Default
{
get { return defaultInstance; }
}
private readonly static MySection defaultInstance = new MySection();
现在,可以通过 MySection.Default
的字段访问默认值。
settings.SectorConfig.FillColor = MySection.Default.SectorConfig.FillColor;
但是,请注意不要使用如下代码来将所有值重置为默认值
settings = MySection.Default;
该赋值的两边都是类实例,并且任何以后尝试修改 settings
的尝试都会导致异常,因为您将尝试更改只读字段 defaultInstance
。为了避免这种风险,我们可以单独访问默认值,或者使用以下 Copy
方法
public MySection Copy()
{
MySection copy = new MySection();
string xml = SerializeSection(this, "MySection",
ConfigurationSaveMode.Full);
System.Xml.XmlReader rdr =
new System.Xml.XmlTextReader(new System.IO.StringReader(xml));
copy.DeserializeSection(rdr);
return copy;
}
现在,可以通过此代码一次性重置默认值
settings = MySection.Default.Copy();
这不是最简单的方法,但它有一个非常明显的附带好处,即能够创建配置节的独立副本(无论多复杂),并且它还展示了 ConfigurationSection.SerializeSection
方法的用法。您可以让 Default
属性简单地返回类的 new 实例,如下所示,但这会在每次需要默认值时实例化另一个对象。
public static MySection Default
{
get { return new MySection(); }
}
添加数据复杂性
现在,我们有了一个自定义节,并且可以轻松访问它。接下来,让我们为节元素添加更复杂的组织。首先是两个新的 ConfigurationElement
定义,从一个具有两个属性的简单 ConfigurationElement
开始。
public class AShapeSetting : ConfigurationElement
{
public AShapeSetting() { }
[ConfigurationProperty("Shape", DefaultValue="Circle")]
public string Shape
{
get { return this["Shape"]; }
set { this["Shape"] = value; }
}
[ConfigurationProperty("Size", DefaultValue="12")]
public int Size
{
get { return (int)this["Size"]; }
set { this["Size"] = value; }
}
}
第二个 ConfigurationElement
也有两个简单的属性,但添加了我们新的 AShapeSetting
元素类型的两个实例。
public class ShapeSettings : ConfigurationElement
{
public ShapeSettings() { }
[ConfigurationProperty("SizeMultiple", DefaultValue="2")]
public int SizeMultiple
{
get { return (int)this["SizeMultiple"]; }
set { this["SizeMultiple"] = value; }
}
[ConfigurationProperty("Enable", DefaultValue="Yes")]
private string pEnable
{
get { return this["Enable"].ToString(); }
set { this["Enable"] = value; }
}
public bool Enable
{
get { return pEnable == "Yes"; }
set { pEnable = value ? "Yes" : "No"; }
}
[ConfigurationProperty("DevA")]
public AShapeSetting DevA
{
get { return (AShapeSetting)this["DevA"]; }
}
[ConfigurationProperty("DevB")]
public AShapeSetting DevB
{
get { return (AShapeSetting)this["DevB"]; }
}
}
我们如下将第二个元素添加到 MySection
类中
public class MySection : ConfigurationSection
{
private MySection() { }
public static MySection Open() ...
public static MySection Open(string path) ...
public MySection Copy() ...
public void Save()
{
Configuration config =
ConfigurationManager.OpenExeConfiguration(spath);
MySection section = (MySection)config.Sections["MySection"];
section.SectorConfig = this.SectorConfig; //Copy the changed data
//***Added line for the new property:
section.UnitShapeConfig = this.UnitShapeConfig; //Copy the changed data
config.Save(ConfigurationSaveMode.Full);
}
[ConfigurationProperty("Sector")]
public SomeSettings SectorConfig ...
//***Added property:
[ConfigurationProperty("UnitShapes")]
public ShapeSettings UnitShapeConfig
{
get { return (ShapeSettings)this["UnitShapes"]; }
set { this["UnitShapes"] = value; }
}
public static MySection Default ...
#region Fields
private static MySection instance;
private readonly static MySection defaultInstance = new MySection();
#endregion Fields
}
我们的配置文件节现在可以如下所示
<MySection>
<Sector FillColor="Cyan" TextSize="8.5" FillOpacity="40" />
<UnitShapes SizeMultiple="2" Enable="Yes">
<DevA Shape="Circle" Size="12" />
<DevB Shape="Star" Size="14" />
</UnitShapes>
</MySection>
这里有一些值得注意的细节
- 嵌入多层组织,易于遵循。
UnitShapes
元素包含其自身的SizeMultiple
和Enable
属性,加上两个AShapeSetting
元素,它们具有自己的属性Shape
和Size
。这两个AShapeSetting
的出现可以放在更灵活的ConfigurationElementCollection
中,但这超出了我目前的范围,目前,出现次数是固定的。 - 使用私有配置属性在布尔值 True/False 和更直观的字符串 Yes/No 之间进行转换。
- 属性名称(
UnitShapeConfig
)、ConfigurationProperty
标识符(UnitShapes
)和属性类型(ShapeSettings
)的独立性,尽管通常让其中至少两个匹配是有益的。
我必须再做一遍吗?!
随着我们在越来越多的地方开始使用这种结构,我厌倦了复制通用代码并更改名称。因此,我创建了一个代码模板来创建一个起始自定义配置节,其中包含如下所示的代码
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Text;
namespace yourApp
{
class CustomConfigSection : ConfigurationSection
{
private CustomConfigSection() { }
#region Public Methods
///<summary>
///Get this configuration set from the application's default config file
///</summary>
public static CustomConfigSection Open()
{
System.Reflection.Assembly assy =
System.Reflection.Assembly.GetEntryAssembly();
return Open(assy.Location);
}
///<summary>
///Get this configuration set from a specific config file
///</summary>
public static CustomConfigSection Open(string path)
{
if ((object)instance == null)
{
if (path.EndsWith(".config",
StringComparison.InvariantCultureIgnoreCase))
spath = path.Remove(path.Length - 7);
else
spath = path;
Configuration config = ConfigurationManager.OpenExeConfiguration(spath);
if (config.Sections["CustomConfigSection"] == null)
{
instance = new CustomConfigSection();
config.Sections.Add("CustomConfigSection", instance);
config.Save(ConfigurationSaveMode.Modified);
}
else
instance =
(CustomConfigSection)config.Sections["CustomConfigSection"];
}
return instance;
}
///<summary>
///Create a full copy of the current properties
///</summary>
public CustomConfigSection Copy()
{
CustomConfigSection copy = new CustomConfigSection();
string xml = SerializeSection(this,
"CustomConfigSection1", ConfigurationSaveMode.Full);
System.Xml.XmlReader rdr =
new System.Xml.XmlTextReader(new System.IO.StringReader(xml));
copy.DeserializeSection(rdr);
return copy;
}
///<summary>
///Save the current property values to the config file
///</summary>
public void Save()
{
// The Configuration has to be opened anew each time we want to
// update the file contents.Otherwise, the update of other custom
// configuration sections will cause an exception to occur when we
// try to save our modifications, stating that another app has
// modified the file since we opened it.
Configuration config = ConfigurationManager.OpenExeConfiguration(spath);
CustomConfigSection section =
(CustomConfigSection)config.Sections["CustomConfigSection"];
//
// TODO: Add code to copy all properties from "this" to "section"
//
section.Sample = this.Sample;
config.Save(ConfigurationSaveMode.Modified);
}
#endregion Public Methods
#region Properties
public static CustomConfigSection Default
{
get { return defaultInstance; }
}
// TODO: Add your custom properties and elements here.
// All properties should have both get and set accessors
// to implement the Save function correctly
[ConfigurationProperty("Sample", DefaultValue = "sample string property")]
public string Sample
{
get { return (string)this["Sample"]; }
set { this["Sample"] = value; }
}
#endregion Properties
#region Fields
private static string spath;
private static CustomConfigSection instance = null;
private static readonly CustomConfigSection defaultInstance =
new CustomConfigSection();
#endregion Fields
}
}
下载此模板并将 zip 文件放在您的 ItemTemplates 目录中。对于 VS 2005,此目录的默认位置是 My Documents\Visual Studio 2005\Templates\ItemTemplates。(一定有更简洁的方法……如果我能找到的话。)下次向项目“添加项”时,CustomConfigSection
将显示在“我的模板”标题下。
一个甜蜜的发现
Open
的重载在设置服务选项时非常有用。虽然这并非真正关于配置文件,但它是一个使用示例,并附带了一些附带的培训。
目标配置文件是 MyService.exe.config。我想使用另一个应用程序 MyServiceController.exe 来编辑 MySettings
节。这是使用以下(略有修改的)MyServiceController
片段完成的
cntrl = new System.ServiceProcess.ServiceController("MyService");
// Find the config file that controls the service
RegistryKey HKLM_System = Registry.LocalMachine.OpenSubKey("System");
RegistryKey HKLM_CCS = HKLM_System.OpenSubKey("CurrentControlSet");
RegistryKey HKLM_Services = HKLM_CCS.OpenSubKey("Services");
RegistryKey HKLM_Service = HKLM_Services.OpenSubKey(cntrl.ServiceName);
string path = (string)HKLM_Service.GetValue("ImagePath");
path = path.Replace("\"", ""); //Remove the quotes
// Access the configuration parameters for the service
mySettings = MySettings.Open(path);
在此实例中,使用 cntrl.ServiceName
不是必需的,但它显示了如何从 ServiceController
对象中获取它。从那里,我可以修改 mySettings
并保存修改,服务将在下次运行时读取新设置。
历史
- 2009 年 1 月 13 日 -- 原始版本。