65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (80投票s)

2009年1月13日

CPOL

11分钟阅读

viewsIcon

292530

downloadIcon

5538

在不深入理解的情况下使用自定义配置节……尽可能地。

引言

本文介绍了一些我在尝试使用自定义配置节时找到的解决方案,并解释了如何简化这一过程,使其对那些没时间彻底学习它们的初学者更加友好。我非常感谢许多作者的文章,它们关于配置节的内容帮助我最终理解了 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 元素包含其自身的 SizeMultipleEnable 属性,加上两个 AShapeSetting 元素,它们具有自己的属性 ShapeSize。这两个 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 日 -- 原始版本。
© . All rights reserved.