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






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 日 -- 原始版本。


