.Net 自定義應用程式配置
引言
幾乎所有的應用程式都離不開配置,有時候我們會将配置資訊存在資料庫中(例如大家可能常會見到名為Config這樣的表);更多時候,我們會将配置寫在Web.config或者App.Config中。通過将參數寫在配置檔案(表)中,我們的程式将變得更加靈活,隻要對參數進行修改,再由程式中的某段代碼去讀取相應的值就可以了。而如果直接将配置值寫在程式中,當配置需要改變時,則隻能通過修改代碼來完成,此時往往需要重新編譯程式集。
本文不是講述.Net Framework中諸多的内置結點如何設定,比如httpHandler、httpModule、membership、roleManager 等。而是講述.Net中配置的實作方式,以及如何定義、使用我們自定義的結點。
.Net 中的程式配置介紹
我們首先了解下.Net 中的配置檔案是如何工作的。我們看下這段代碼:
// Web.Config
<appSettings>
<add key="SiteName" value="TraceFact.Net"/>
</appSettings>
// Default.aspx.cs
protected void Page_Load(object sender, EventArgs e) {
Literal siteName = new Literal();
siteName.Text = ConfigurationManager.AppSettings["SiteName"];
form1.Controls.Add(siteName);
}
上面這段代碼大家應該再熟悉不過了,我們在appSettings結點中添加了一個add子結點,給key和value屬性賦了值,然後在程式中讀取了值。但是為什麼可以這麼做?如果我們想自定義一個配置系統,我們該怎麼做呢?
我們先抛開.Net的機制不談,來看看如果自己實作一個應用程式的配置方法該如何做,我想可以是這樣的:
- 首先建立一個XML檔案,在這個檔案中建立我們需要的結點(或者結點樹),在結點的屬性或者文本(innerText)中存儲配置值。
- 建立一個類,這個類的字段和屬性映射XML中的某個結點下的屬性和文本,以提供強類型的通路。
- 建立一個配置檔案Xml的通路類,在下面添加一個方法,比如叫GetSection(string nodeName),參數nodeName是結點(或者結點樹的根節點)的名稱。在方法内部,先建立第二步的類型執行個體,然後使用System.Xml命名空間下的方法對結點進行處理,對執行個體的屬性進行指派,最後傳回這個執行個體。
- 在程式中通過這個執行個體來通路配置的結點值。
上面的思路應該是很清晰的,可是存在一個問題:
我們的XML檔案中可能會包含多個結點,而每個結點的結構可能都不相同。比如說:每個結點下的子結點可能不相同,每個結點的屬性可能不相同。這樣的話,我們的GetSection()方法實際上隻能是針對某個特定的結點進行。那麼該怎麼辦呢?
我們隻有為不同的結點指定不同的GetSection()方法了。而如何進行指定呢?我們可以寫一大串的GetSectionA()、GetSectionB、GetSectionC()讓它們分别去對應SectionA、SectionB、SectionC。但是我們還有更好的方法,我們可以将調用GetSection()時的結點處理邏輯委托給其他的類型去處理,而在哪裡指定某個結點由某個委托程式去處理呢?自然最好還是寫在配置檔案中。比如,我們的XML檔案是這樣的:
<?xml version="1.0"?>
<configuration>
<forum name="TraceFact.Net Community">
<root url="http://forum.tracefact.net" />
<replyCount>20</replyCount>
<pageSize>30</pageSize>
<offlineTime>20</offlineTime>
</forum>
<blog name="Jimmy Zhang's Personal Space">
<root url="http://blog.tracefact.net" />
<urlMappings>
<rewriteRule>
<request>~/(\d{4})/Default\.aspx</request>
<sendTo>~/BlogDetail.aspx?year=$1</sendTo>
</rewriteRule>
</urlMappings>
</blog>
</configuration>
那麼,我們要定義對于 forum結點和 blog結點使用不同的GetSection()方法,我們就可以這麼寫:
<configSections>
<section name="forum" type="forumSectionHandler" />
<section name="blog" type="blogSectionHandler" />
</configSections>
<!--以下為 forum 和 blog 結點,省略-->
其中,configSections下的兩個section結點分别用于定義對于forum和blog結點使用哪種處理方式。section結點的name屬性說明是對于哪個結點,type屬性說明對于該結點用什麼程式來處理(當調用GetSection()方法時,會交給type所指定的類型去處理)。
看到這裡你應該已經明白了,上面講述的其實正是.Net中的配置處理方法:
在.Net中,配置檔案實際分為了兩部分,一部分是配置的實際内容,比如appSettings以及上例中的blog和forum結點;另一部分指定結點的處理程式,這些結點位于 configSections 結點下面。當你打開站點下的web.config檔案,你可能看不到太多的configSections下的結點,這是因為諸如AppSettings這樣的結點屬于内置結點,對于它們的設定全部位于C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\CONFIG 下的Machine.Config檔案中,以提供全局服務。(作業系統以及.Net Framework版本不同此目錄的位址也不盡相同)。
.Net 應用程式配置方法
使用 .Net内置結點 和 .Net内置處理程式
下面我們來一步步地實作.Net中的應用程式配置,首先看下對于.Net中内置的結點如何進行配置以及在程式中進行讀取。
建立檔案夾GeneralConfig,在檔案夾下建立一個站點WebSite,修改Web.Config,删除原有内容,添加如下代碼(為了美觀,我添加了Theme,進行了簡單的樣式設定,可以從文章所附的代碼中進行下載下傳):
<!-- Basic.aspx,使用.Net内置的結點和處理程式 -->
<appSettings>
<add key="SiteName" value="TraceFact.Net"/>
<add key="Version" value="v1.0.08040301" />
<add key="GreetingLanguage" value="Chinese" />
</appSettings>
<connectionStrings>
<remove name="LocalSqlServer"/>
<add name="LocalServer" connectionString="User ID=sa;Password=password;Initial Catalog=pubs;Data Source=." providerName="System.Data.SqlClient"/>
<add name="PassportCenter" connectionString="User ID=sa;Password=password;Initial Catalog=pubs;Data Source=www.remotesite.com" providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- 以下配置應用于本範例程式,但不是文章所讨論的範圍 -->
<system.web>
<compilation debug="true"/>
<pages theme="Default" />
</system.web>
本節我們示範如何讀取appSettings以及ConnectionStrings下的配置資料。注意到Web.Config中沒有configSection結點的設定,也就是并沒有定義appSettings結點該如何處理。如上節所說,這是因為它們的結點處理程式定義在了machine.config中,打開machine.config,我們可以看到這樣的設定:
<?xml version="1.0" encoding="UTF-8"?>
<configSections>
<section name="appSettings" type="System.Configuration.AppSettingsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false"/>
<section name="connectionStrings" type="System.Configuration.ConnectionStringsSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" requirePermission="false"/>
<!-- 其餘略 -->
可以看到,對于appSettings實際上是由System.Configuration.AppSettingsSection處理,而對于connectionStrings 實際上是由System.Configuration.ConnectionStringsSection 處理。另外再觀察一下machine.config就會發現,處理程式分成了兩種類型:一種是以Section結尾的,比如上面的這兩個;還有一種是以Handler結尾的,比如:
<section name="system.data.dataset" type="System.Configuration.NameValueFileSectionHandler, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" restartOnExternalChanges="false"/>
可以看到type 屬性System.Configuration.NameValueFileSectionHandler是以Handler結尾的。
之是以會有這樣的差別,是因為.Net中對于結點有兩種處理方式,一種是定義一個繼承自System.Configuration.ConfigurationSection 的類,這也就是以Section結尾的類型;一種是實作System.Configuration.IConfigurationSectionHandler 接口,也就是以Handler結尾的類型。.Net Framework 2.0以後版本推薦采用繼承ConfigurationSection類的方式,在本文,範例大多數使用實作IConfigurationSectionHandler接口的方式,下面也會提供一個繼承ConfigurationSection類的方式作為對比。
NOTE:使用私有程式集時 type通常由兩部分組成,由逗号“,”分隔,前半部分是類型名稱,後半部分是程式集名稱。如果是公有程式集(GAC),則需要提供publicKey。
好了,現在我們看一下如何在程式中讀取它們。添加一個檔案Basic.aspx,修改它的代碼如下(隻含主要代碼,下同):
// Basic.aspx
<h2>Website Information:</h2>
<hr />
<b>Website Name</b> :<asp:Literal ID="ltrSiteName" runat="server"></asp:Literal><br />
<b>Version</b>:<asp:Literal ID="ltrVersion" runat="server"></asp:Literal>
<br /><br />
<b>LocalServer Connection</b>:
<asp:Literal ID="ltrLocalConnection" runat="server"></asp:Literal>
// Basic.aspx.cs
protected void Page_Load(object sender, EventArgs e)
{
string siteName = ConfigurationManager.AppSettings["SiteName"];
string version = ConfigurationManager.AppSettings["Version"];
string localConnection = ConfigurationManager.ConnectionStrings["LocalServer"].ConnectionString;
ltrSiteName.Text = siteName;
ltrVersion.Text = version;
ltrLocalConnection.Text = localConnection;
在浏覽器中浏覽,應該可以看到這樣的界面:

使用 自定義結點 和 .Net内置處理程式
在上一節,我們使用了.Net内置的結點 appSettings 和 connectionStrings結點,并使用了 .Net 内置的處理程式。.Net 内置的處理程式定義于 machine.config中,提供全局服務,是以我們無需進行任何額外工作就可以直接使用。但是使用内置結點在很多情況下不一定友善,比如說,我們希望儲存站點使用的郵件伺服器的位址、使用者名和密碼,那麼按照上面的做法,我想應該是這樣的:
<add key="MailServer" value="mail.tracefact.net" />
<add key="MailUser" value="jimmyzhang" />
<add key="MailPassword" value="123456"/>
這樣的話配置及使用并不友善:首先,很明顯這三個add是一組資料,但是除了憑自己的經驗判斷,再沒有任何辦法進行區分;其次,如果我們有多組伺服器或者很多配置,我們需要寫很長的add結點。如果我們可以自定一個結點,情況就會好很多,比如我們在Web.Config中添加一個結點:
<mailServer address="mail.tracefact.net" userName="jimmyzhang" password="123456" />
這樣看起來就好了很多,mailServer表示這是一個關于郵件伺服器配置的結點,它的屬性/值 分别代表存儲的相應的值。以後我們在程式中進行發送郵件時可以根據這裡的值來對發送郵件的對象進行參數設定。本節我們就來看下如何在web.Config中使用我們自定義的結點,但使用.Net内置的處理程式。
在web.config中,結點以及屬性的命名遵循Camel命名方式,也就是首字母小寫,其後的每個單詞首字母大些的方式。
接着在站點中添加一個 Simple.aspx 檔案,打開它。此時編譯器會報錯,提示:“分析器錯誤資訊: 無法識别的配置節 mailServer”。 .Net已經提供了很多内置的處理程式,為了避免發生這個錯誤,我們必須在configSection中指定對mailServer結點的處理程式。有時候我們希望繞過.Net的機制,直接使用System.Xml命名空間下的類來對配置檔案(web.config也是标準的Xml檔案)進行操作,但是因為這裡會報錯,是以有的人幹脆就另建一個xml檔案了事,然後對建立的xml檔案進行操作。實際上,可以通過指定IgnoreSectionHandler 或者 IgnoreSection 處理程式的方式來進行處理,如同它們的名稱所暗示的,這兩個處理程式什麼都不做,僅僅是讓.Net 忽略我們的自定義結點。修改 web.Config ,在根節點configuration下建立configSections結點,然後再添加一個section結點,指定它的name屬性值為mailServer,意為指定mailServer結點的處理程式,然後指定type為System.Configuration.IgnoreSection:
<!-- 使用IgnoreSection,可以将指定的XMl結點忽視掉 -->
<section name="mailServer" type="System.Configuration.IgnoreSection, System.Configuration, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" allowLocation="false" restartOnExternalChanges="true" />
<!-- 自定義結點 mailServer -->
此時再次打開 Simple.aspx,編譯器不再報錯,由于我們什麼内容也沒有添加,此時會顯示一個空白頁面。現在,你可以采用“老辦法”,編寫程式去處理這個結點了,但是本文要講述的,是一種更優雅、更.Net的方式。
在本節,我們暫且不自定義處理程式,看看.Net中除了這個IgnoreSectionHandler還有什麼可以利用的處理程式。在.Net中,還有一個較為常用的處理程式,就是System.Configuration.SingleTagSectionHandler,它會以Hashtable的形式傳回結點的所有屬性。
現在我們将上面定義的configSections結點下name屬性為mailServer的section結點的type屬性改為System.Configuration.SingleTagSectionHandler:
<!-- 對自定義結點 mailServer 定義處理程式,使用.Net 内置的處理程式 -->
<section name="mailServer" type="System.Configuration.SingleTagSectionHandler, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
然後,修改simple.aspx檔案,代碼如下所示:
// Simple.aspx
<div>
<h1>使用 自定義結點 和 .Net内置處理程式</h1>
<h2>MailServer Information:</h2><hr />
<b>Address</b>: <asp:Literal ID="ltrAddress" runat="server"></asp:Literal><br />
<b>UserName</b>: <asp:Literal ID="ltrUserName" runat="server"></asp:Literal><br />
<b>Password</b>: <asp:Literal ID="ltrPassword" runat="server"></asp:Literal>
</div>
// Simple.aspx.cs
// 傳回一個Hashtable
Hashtable mailServer = (Hashtable)ConfigurationManager.GetSection("mailServer");
ltrAddress.Text = mailServer["address"].ToString();
ltrUserName.Text = mailServer["userName"].ToString();
ltrPassword.Text = mailServer["password"].ToString();
我們打開頁面,應該會看到諸如下面這樣的界面:
可以看到,當我們使用System.Configuration.SingleTagSectionHandler時,調用GetSection()方法會傳回一個Hashtable(當調用GetSection()方法時,會潛在地執行configSection中相應結點所指定的type類型的方法),Hashtable的key為屬性的名稱,Hashtable的value為屬性的值。
使用 自定義結點 和 自定義處理程式
上面的方法雖然可行,但還存在着問題:
- 采用Hashtable的方法,由于key是字元串類型,除非你将結點的屬性全部背過了,不然我們不得不去檢視web.config檔案,找到屬性,然後才能書寫代碼去擷取(比如 mailServer["address"])。而不能使用Vs提供的自動提示功能,也就是強類型通路的能力,這樣的話使用起來很不友善。
- 假如我們的站點大了一些,隻使用一個郵件伺服器可能壓力太大,我們需要設定多個郵件伺服器,對于子域名 forum.tracefact.net 使用一個郵件伺服器;對于 blog.tracefact.net使用另一個郵件伺服器,這時我們要如何設定Web.Config呢?
此時,我們可能會需要下面這樣結構的配置:
<configuration>
<!-- SimpleCustom.aspx, 使用自定義結點和自定義處理程式 -->
<mailServerGroup provider="www.edong.com">
<mailServer client="forum.tracefact.net">
<address>mail1.tracefact.net</address>
<userName>jimmyzhang</userName>
<password>123456</password>
</mailServer>
<mailServer client="blog.tracefact.com">
<address>mail2.tracefact.net</address>
<userName>webmaster</userName>
<password>456789</password>
</mailServerGroup>
mailServerGroup 結點包含了所有關于郵件伺服器的資訊。它的provider屬性說明郵件伺服器是由哪個ISP(Internet Service Provider 網際網路服務供應商)提供的,這裡是中國易動網(www.edong.com)。其下的結點mailServer是指具體的郵件伺服器,client說明此郵件伺服器為哪個域名提供服務,address說明郵件伺服器的位址,userName和password分别為使用者名和密碼。
此時,如果我們使用上一節的辦法,将無法實作,因為它隻能對單個結點進行操作,結點下不能包含子結點(文本節點也不行)。這個時候,我們最好自定義一個結點處理程式來完成。如同上面所說,此時有兩種方法,一種是實作IConfigurationSectionHandler 接口,一種是繼承ConfigurationSection類。我們先看如何通過實作IConfigurationSectionHandler接口的方式完成。
自定義結點處理程式 – 實作IConfigurationSectionHandler接口
IConfigurationSectionHandler 接口的定義如下:
namespace System.Configuration {
public interface IConfigurationSectionHandler {
object Create(object parent, object configContext, XmlNode section);
}
它隻要求實作一個方法:Create(),當我們在ConfigurationManager對象上調用GetSection("sectionName")方法的時候,實際上會委托給這個Create()方法進行處理。這個方法最重要的一個參數是類型為XmlNode的section,它代表着名為“sectionName”的結點。它傳回一個object類型的對象,這個對象通常是我們自定義的一個關于這個結點的配置對象,對象的字段和屬性映射結點的屬性和文本值,來提供強類型的通路(你也可以傳回一個Hashtable,這樣就無需自定義類型)。
結點在在傳遞時有一個轉換,調用GetSection()時,傳遞的是String類型的結點名稱;而在Create()方法中,傳遞的是該名稱的XmlNode類型的結點。
我們在解決方案下面新添加一個類庫項目,CustomConfig。然後在下面添加一個類檔案,MailServerConfigHandler.cs。現在我們來編寫代碼,首先建立一個類,MailServer,這個類用于映射Web.Config中mailServerGroup結點下的mailServer子結點:
public class MailServer {
// 存儲mailServer的子結點(Address,UserName,Password)的innerText值
// 以及屬性 Client 的值
private Hashtable serverNode;
public MailServer() {
serverNode = new Hashtable();
public Hashtable ServerNode {
get { return serverNode; }
public string Client {
get {
return serverNode["client"] as string;
}
public string Address {
return serverNode["address"] as string;
public string UserName {
return serverNode["userName"] as string;
public string Password {
return serverNode["password"] as string;
注意,在MailServer類的内部,我們維護了一個Hashtable,這是因為在處理mailServerGroup結點時,我們對于它的子結點mailServer的所有屬性值以及innerText都會存儲在一個Hashtable中,在後面将會看到。
然後我們再建一個類 MailServerConfig,讓這個類繼承自List<MailServer>,它用于映射mailServerGroup結點,我們對于mailServerGroup結點的屬性以及子結點的通路,實際上均通過這個類來進行:
public class MailServerConfig : List<MailServer> {
private string provider; // 對應 mailServerGroup 結點的provider 屬性
public string Provider {
get { return provider; }
set { provider = value; }
最後,到了我們對于結點的實際處理工作,建立類 MailServerConfigurationHandler,讓它實作IConfigurationSectionHandler 接口:
// 自定義配置結點 mailServerGroup 的處理程式
public class MailServerConfigurationHandler : IConfigurationSectionHandler {
// section 為 MailServerGroup 結點
public object Create(object parent, object configContext, XmlNode section) {
// 設定方法傳回的配置對象,可以是任何類型
MailServerConfig config = new MailServerConfig();
// 擷取結點的屬性資訊
config.Provider =
section.Attributes["provider"] == null ? "" : section.Attributes["provider"].Value;
// 擷取 MailServer 結點
foreach (XmlNode child in section.ChildNodes) {
MailServer server = new MailServer();
// 添加Client屬性
if (child.Attributes["client"] != null)
server.ServerNode.Add("client", child.Attributes["client"].Value);
// 擷取MailServer下的 Name,UserName,Password 結點
foreach (XmlNode grandChild in child.ChildNodes) {
// 添加文本
server.ServerNode.Add(grandChild.Name, grandChild.InnerText);
}
// 将server加入 MailServerConfig
config.Add(server);
return config;
我在這段代碼中添加了詳細的注釋,這裡就不再多說明了,需要注意的是它傳回了一個MailServerConfig對象,在程式中,我們将通過MailServerConfig來通路結點資訊。
現在我們為站點添加CustomConfig的項目引用,修改Web.Config檔案,添加下面代碼來說明對于mailServerGroup結點的處理程式:
<section name="mailServerGroup" type="CustomConfig.MailServerConfigurationHandler, CustomConfig" />
<!-- mailServerGroup 結點,此處略 -->
接下來,我們再在站點下添加一個SimpleCustom.aspx檔案,使用它來測試我們的配置處理程式:
// SimpleCustom.aspx
<h1>使用 自定義結點 和 自定義處理程式</h1>
<b>MailServerGroup Provider</b>: <asp:Literal ID="ltrServerProvider" runat="server"></asp:Literal>
<h2>Mail Server1(Client:<asp:Literal ID="ltrClient1" runat="server"></asp:Literal>) Information:</h2>
<b>Address</b>: <asp:Literal ID="ltrAddress1" runat="server"></asp:Literal> <br />
<b>UserName</b>: <asp:Literal ID="ltrUserName1" runat="server"></asp:Literal><br />
<b>Password</b>: <asp:Literal ID="ltrPassword1" runat="server"></asp:Literal>
<h2>Mail Server2(Client:<asp:Literal ID="ltrClient2" runat="server"></asp:Literal>) Information:</h2>
<b>Address</b>: <asp:Literal ID="ltrAddress2" runat="server"></asp:Literal> <br />
<b>UserName</b>: <asp:Literal ID="ltrUserName2" runat="server"></asp:Literal><br />
<b>Password</b>: <asp:Literal ID="ltrPassword2" runat="server"></asp:Literal>
// SimpleCustom.aspx.cs
// 擷取 mailServerConfig 對象
MailServerConfig mailConfig = (MailServerConfig)ConfigurationManager.GetSection("mailServerGroup");
// 擷取 MailServerGroup 結點的 Provider 屬性
ltrServerProvider.Text = mailConfig.Provider;
// 擷取第一租 MailServer 資料
ltrClient1.Text = mailConfig[0].Client;
ltrAddress1.Text = mailConfig[0].Address;
ltrUserName1.Text = mailConfig[0].UserName;
ltrPassword1.Text = mailConfig[0].Password;
// 擷取第二租 MailServer 資料
ltrClient2.Text = mailConfig[1].Client;
ltrAddress2.Text = mailConfig[1].Address;
ltrUserName2.Text = mailConfig[1].UserName;
ltrPassword2.Text = mailConfig[1].Password;
現在打開頁面,應該可以看到下面的頁面:
自定義結點處理程式 – 繼承ConfigurationSection基類
除了實作IConfigurationSectionHandler接口來自定義結點處理程式,還可以通過繼承ConfigurationSection基類的方式來完成,我們還以上面的例子來做說明。一般來說我們想要存儲的資料可以用兩種方式來存儲:一種是存儲到結點的屬性中,一種是存儲在結點的文本(InnerText)中。比如:
<node>這裡是要存儲的值</node>
<!-- 或者是下面這樣,兩種的效果是一樣的 -->
<node text="這裡是要存儲的值" />
因為一個結點可以有很多的屬性,但隻有一個InnerText,而在程式又要将這兩種形式差別處理顯然太麻煩了,是以Microsoft幹脆就隻使用屬性存儲而不使用InnerText,大家可以看一下machine.config中的配置,可曾見到過一個以InnerText來存儲配置資訊的?在ConfigurationSection中,也沒有提供對InnerText的處理,是以對于上面的例子,我們首先要進行重新格式化,僅使用屬性來存儲我們的配置值。
除此以外,我們還要将結點分組,對于入口結點,也就是mailServerGroup而言,這個結點組相當于它的一個屬性(就好像provider一樣),因為結點組包含多個結點,是以映射到面向對象的代碼中,自然就成了一個集合類。是以我們需要按照這些概念将web.config中的mailServerGroup改寫成下面這樣:
<mailServerGroup2 provider="www.edong.com">
<mailServers>
<mailServer
client="forum.tracefact.net"
address="mail1.tracefact.net"
userName="jimmyzhang"
password="123456" />
client="blog.tracefact.net"
address="mail2.tracefact.net"
userName="webmaster"
password="456789" />
</mailServers>
</mailServerGroup2>
注意,我們将mailServerGroup改成了mailServerGroup2,以免和上一節的沖突。現在我們在CustomConfig項目中添加一個檔案 MailServerSection.cs,然後添加如下代碼:
// MailServerSection 為入口
public class MailServerSection : ConfigurationSection {
[ConfigurationProperty("provider", IsKey = true)]
get { return this["provider"] as string; }
[ConfigurationProperty("mailServers", IsDefaultCollection = false)]
public MailServerCollection MailServers {
return (MailServerCollection)this["mailServers"];
set {
this["mailServers"] = value;
// MailServer 結點
public sealed class MailServerElement : ConfigurationElement {
[ConfigurationProperty("client", IsKey = true, IsRequired = true)]
get { return this["client"] as string; }
set { this["client"] = value; }
[ConfigurationProperty("address")]
get { return this["address"] as string; }
[ConfigurationProperty("userName")]
get { return this["userName"] as string; }
[ConfigurationProperty("password")]
get { return this["password"] as string; }
// MailServer 集合類
public sealed class MailServerCollection : ConfigurationElementCollection {
public override ConfigurationElementCollectionType CollectionType {
get { return ConfigurationElementCollectionType.BasicMap; }
protected override ConfigurationElement CreateNewElement() {
return new MailServerElement();
protected override Object GetElementKey(ConfigurationElement element) {
return ((MailServerElement)element).Client;
protected override string ElementName {
get { return "mailServer"; }
public new int Count {
get { return base.Count; }
public MailServerElement this[int index] {
return (MailServerElement)BaseGet(index);
if (BaseGet(index) != null) {
BaseRemoveAt(index);
BaseAdd(index, value);
new public MailServerElement this[string Name] {
return (MailServerElement)BaseGet(Name);
public int IndexOf(MailServerElement element) {
return BaseIndexOf(element);
public void Add(MailServerElement element) {
BaseAdd(element);
public void Remove(MailServerElement element) {
if (BaseIndexOf(element) >= 0)
BaseRemove(element.Client);
public void RemoveAt(int index) {
BaseRemoveAt(index);
public void Remove(string client) {
BaseRemove(client);
public void Clear() {
BaseClear();
這段代碼由三部分組成,一部分是MailServerSection類,很明顯它是配置結點的入口,它包含兩個屬性,一個是String類型的provider屬性,它映射mailServerGroup結點的provider屬性;一個是MailServerCollection類型的MailServers屬性,這個屬性映射我們新添加的mailServers結點,mailServers結點下還包含了若幹個mailServer結點。從它的名稱也可以看出來,它是一個集合類。
MailServerElement類用于映射mailServer結點的屬性,這裡是我們實際存儲資料的地方。MailServerCollection類用于映射mailServers結點,可以看出它是一個集合類,另外還包含了很多對于結點進行操作的方法,大部分的能力都繼承自ConfigurationElementCollection基類。
值得注意的是,之是以可以使用這種方式實作,使用了大量的特性标記。當你看到特性标記的時候,你應該就想到必須有地方使用反射來讀取特性的值,不然特性毫無意義。隻是這部分的内容屬于.Net Framework的底層,無需我們操心。
現在在Web.Config的configSections結點下添加我們的結點處理程式的配置:
<section name="mailServerGroup2" type="CustomConfig.MailServerSection, CustomConfig" />
然後複制SimpleCustom.aspx檔案,并将檔案名改為SimpleCustom2.aspx,然後修改SimpleCustom2.aspx,幾乎不需要做太多修改就可以了:
// 擷取 MailServerSection 對象
MailServerSection mailSection = (MailServerSection)ConfigurationManager.GetSection("mailServerGroup2");
// 擷取 MailServerGroup 結點的 Provider 屬性
ltrServerProvider.Text = mailSection.Provider;
// 擷取第一租 MailServer 資料
ltrClient1.Text = mailSection.MailServers[0].Client;
ltrAddress1.Text = mailSection.MailServers[0].Address;
ltrUserName1.Text = mailSection.MailServers[0].UserName;
ltrPassword1.Text = mailSection.MailServers[0].Password;
// 擷取第二租 MailServer 資料
ltrClient2.Text = mailSection.MailServers[1].Client;
ltrAddress2.Text = mailSection.MailServers[1].Address;
ltrUserName2.Text = mailSection.MailServers[1].UserName;
ltrPassword2.Text = mailSection.MailServers[1].Password;
打開後可以看到和上一節完全一樣的界面。
“存儲”類型執行個體
有時候,我們可能不僅希望能在Web.Config中存儲字元串類型的值,而希望存儲一個對象。比如說我們想在程式中應用Strategy模式,我們可能希望在配置中定義Strategy模式采用哪個算法。現在我們先實作一個簡單的Strategy模式,然後再看如何進行配置。新添加一個類庫項目ClassLib,然後添加IGreetingStrategy.cs檔案,修改代碼如下:
namespace ClassLib
// 定義接口
public interface IGreetingStrategy {
string GreetingType { get; }
void SetGreetingWords(ITextControl textControl);
// 英文版問候程式
public class EnglishGreeting : IGreetingStrategy {
public string GreetingType {
get { return "English Greeting"; }
public void SetGreetingWords(ITextControl textControl) {
textControl.Text = "Hello, my reader !";
// 中文版問候程式
public class ChineseGreeting : IGreetingStrategy {
private string greetingType;
public ChineseGreeting(string greetingType) {
this.greetingType = greetingType;
public ChineseGreeting() : this("中文問候") { }
get { return greetingType; }
textControl.Text = "你好, 我的讀者 !";
public class GeneralClass {
// 這個類可能還有很多的 字段、屬性、方法,這裡省略
private IGreetingStrategy gs;
public GeneralClass(IGreetingStrategy gs) {
this.gs = gs;
public string GeneralProperty {
get {
// 做一些額外的工作,這裡省略
return "<span style='color:#008000'>" + gs.GreetingType + "</span>";
public void GeneralMethod(ITextControl textControl) {
// 做一些額外的工作,這裡省略...
gs.SetGreetingWords(textControl);
textControl.Text = "<span style='color:#008000'>" + textControl.Text + "</span>";
// 省略...
GeneralClass是一個普通類型(你可以把它想象成實際應用中的任何類型),它的内部維護一個IGreetingStrategy類型,在GeneralClass的方法和屬性中,調用了IGreetingStrategy所提供的方法,實際上會根據IGreetingStrategy的不同實作調用了不同對象的方法(英文問候或者中文問候)。
這裡不再對Strategy模式進行讨論了,可以參見我的《
奇幻RPG(角色技能 與 Strategy模式)》一文。
現在讓站點引用建立的ClassLib項目,新添一個檔案ObjectStore.aspx,修改代碼如下:
// ObjectStore.aspx
<h1>在Config中存儲類型資訊(模拟存儲對象)</h1>
<b>Greeting Type</b>: <asp:Literal ID="ltrGreetingType" runat="server" ></asp:Literal><br />
<b>Greeting Words</b>: <asp:Literal ID="ltrGreetingWrods" runat="server" ></asp:Literal>
// ObjectStore.aspx.cs
// Hard Coding,直接寫入到程式裡,不使用配置
IGreetingStrategy greetingStrategy = new ChineseGreeting();
GeneralClass generalObj = new GeneralClass(greetingStrategy);
// 以下為通用代碼
if (generalObj != null){
ltrGreetingType.Text = generalObj.GeneralProperty;
generalObj.GeneralMethod(ltrGreetingWrods);
} else{
ltrGreetingType.Text = "IGreetingStrategy Load Error.";
ltrGreetingWrods.Text = "IGreetingStrategy Load Error.";
在這裡,我們根本沒有進行任何程式配置,直接HardCoding到了代碼中,目的隻是先測試下代碼是否運作正常。此時在浏覽器中打開頁面,應該可以看到如下的畫面:
好了,現在我們有了新的需求,我希望使用英文版的問候方法,也就是使用EnglishGreeting,該如何做呢?似乎很簡單,我們隻要修改一行代碼就可以了:
// 将 ChineseGreeting 改為 EnglishGreeting
IGreetingStrategy greetingStrategy = new EnglishGreeting();
問題是:這是采用HardCoding直接修改代碼的方式,如果我們希望可以通過配置檔案來完成這樣的轉化,該如何做呢?我想有不少人大概會這樣,先在Web.Config中的appSettings下面添加一個結點,然後在程式中對這個結點的值進行判斷,根據判斷結果來決定使用ChineseGreeting還是EnglishGreeting:
修改Web.Config結點,在appSettings下添加這樣的代碼:
<add key="GreetingLanguage" value="English" />
然後修改 Object.aspx.cs檔案,将代碼改成如下這樣:
// 使用AppSettings 以及 if else(switch)語句來進行配置
string strategy = ConfigurationManager.AppSettings["GreetingLanguage"];
IGreetingStrategy greetingStrategy = null;
GeneralClass generalObj = null;
if (strategy == "Chinese")
greetingStrategy = new ChineseGreeting();
else if (strategy == "English")
greetingStrategy = new EnglishGreeting();
if (greetingStrategy != null)
generalObj = new GeneralClass(greetingStrategy);
// 以下代碼相同,略…
這樣顯然可以實作我們的要求,但是它還有不足:1、對于語言類型的判斷我們依然是Hardcoding到代碼中去的,這樣如果我們日後添加了韓文Korean或者日文Japanese,我們依然需要修改代碼;2、appSettings中以及程式中都是字元串的形式存儲,如果我們不小心輸錯一個字母,那麼if-else語句就不會通過。
那麼我們該如何存儲這個IGreetingStrategy類型的對象呢?使用Xml串行化麼?不!我們應該想想有什麼辦法可以通過一個字元串(Xml檔案中的配置結點存儲的值為字元串類型),來獲得一個對象呢?答案是使用反射!我們可以借鑒.Net的方式,将類型資訊存儲到Web.Config的結點中,然後在程式中擷取結點的值,最後再利用反射來動态地建立類型。
下面的部分代碼要求你對反射有所了解,可以參看 《
.Net 中的反射(動态建立類型執行個體)–Part.4》
有了思路,接下來我們就來一步步地實作,我們首先在Web.Config中建立一個自定義結點greetingStrategy:
<!-- ObjectStore.aspx, 配置類型資訊(存儲對象) -->
<greetingStrategy type="ClassLib.ChineseGreeting, ClassLib" />
它隻有一個屬性type,當我們指定程式使用哪個IGreetingStrategy時,隻要在這裡進行設定就可以了。type由“,”分隔為了兩部分,前部分是類型名稱,後半部分是類型所在的程式集,如果是GAC,那麼還需要添加publicKey等資訊。目前的這個結點設定為ChineseGreeting。
接下來,我們要為這個結點建立處理程式。我們在CustomConfig項目下再添加一個檔案GreetingConfigHandler.cs,添加如下代碼:
public class GreetingConfigurationHandler : IConfigurationSectionHandler {
// 這裡的section結點為 Web.Config中的greetingStrategy結點
// 擷取結點type屬性的值
Type t = Type.GetType(section.Attributes["type"].Value);
// 将要建立的類型執行個體
object obj = null;
try {
obj = Activator.CreateInstance(t);
} catch (Exception ex) {
return null;
// obj為結點 type 屬性中定義的對象,在這裡是 ClassLib.ChineseGreeting
return obj;
上面這段代碼根據greetingStrategy結點的type屬性建立并傳回了一個IGreetingStrategy類型執行個體。接下來我們在Web.Config中的configSections結點下添加對greetingStrategy結點的處理程式:
<section name="greetingStrategy" type="CustomConfig.GreetingConfigurationHandler, CustomConfig"/>
OK!現在我們再次修改ObjectStore.aspx.cs檔案,使用新的方式來擷取IGreetingStrategy對象:
IGreetingStrategy greetingStrategy =
(IGreetingStrategy)ConfigurationManager.GetSection("greetingStrategy");
打開頁面,應該會看到和上面一樣的效果,差別隻是我們采用了更加靈活的方式。
使用有參數的構造函數建立類型執行個體
注意,在這裡我們建立類型時使用的是無參數的構造函數,在Activator的CreateInstance()方法中沒有提供構造函數需要的參數。但是回頭看下我們的ChineseGreeting類型的定義,它還有一個包含一個參數的構造函數:
public class ChineseGreeting : IGreetingStrategy {
private string greetingType;
public ChineseGreeting(string greetingType) {
this.greetingType = greetingType;
// 其餘略
那麼如果我們需要通過有參數的構造函數建立一個類型執行個體,又該如何做呢?首先,我們應該修改Web.Config中的greetingStrategy結點,讓它包含參數資訊:
<greetingStrategy type="ClassLib.ChineseGreeting, ClassLib" >
<params greetingType="**中文問候**" />
</greetingStrategy>
接下來,我們要修改GreetingConfigurationHandler類型,讓它在建立類型時根據結點擷取值,然後傳遞參數greetingType進去。
此時有兩種政策:
- 在GreetingConfigurationHandler中對greetingStrategy結點進行處理,取得params結點greetingType屬性的值,然後傳遞給ChineseGreeting類型的構造函數。
- 在ChineseGreeting中新添一個構造函數,接受一個XmlNode結點,将greetingStrategy結點再次進行傳遞,然後在這個構造函數中進行處理。
我們來分别嘗試,先使用第一種,修改GreetingConfigurationHandler如下所示(注意為了使代碼簡單,我沒有做諸如param結點是否存在這樣的判斷,以下同):
// 擷取param結點的屬性greetingType
XmlAttribute attr = section.SelectSingleNode("param").Attributes["greetingType"];
object[] parameters = { attr.Value };
obj = Activator.CreateInstance(t, parameters); // 使用有參數的構造函數
// obj為結點的 type屬性中定義的對象,在這裡是 ClassLib.ChineseGreeting
return obj;
然後打開頁面,可以看到如下圖所示,可見此次使用了有參數的構造函數,并讀取了Web.Config中的值。
還有一種方法,是直接将section進行傳遞,也就是将XmlNode類型的greetingStrategy結點進行傳遞,在新的構造函數中對這個結點進行處理。
此時的GreetingConfigurationHandler除了根據type來建立對象以外,對這個結點不做任何的額外處理。我們再次修改 GreetingConfigurationHandler類型,隻修改一行代碼就可以了:
// 直接将section結點進行傳遞
object[] parameters = { section };
然後我們為ChineseGreeting再添加一個構造函數:
public ChineseGreeting(XmlNode section) {
XmlAttribute attr = section.SelectSingleNode("param").Attributes["greetingType"]; // 擷取屬性
greetingType = attr.Value; // 為字段指派
編譯ClassLib和CustomConfig項目,然後再次打開ObjectStore.aspx檔案,應該看到和上面相同的輸出結果。
統一結點配置管理
上面一節絮絮叨叨說了這麼多,那麼我究竟想告訴大家什麼呢?可以想一想,我們的應用程式可能會有非常多可以設定的地方,比如我們還可以設定 URL 位址映射、設定每頁顯示的回帖數、設定分頁大小等等,這樣我們将會建立非常多的自定義結點,而為了使用每個自定義結點,我們又會建立非常多的Handler處理程式,這樣的話光web.config中的configSections結點就需要寫一長串,有沒有辦法對這些配置進行統一管理呢?當然可以!其實上一節的第二種方法就已經實作了這種效果,你可以将params結點想象成一個結點樹,那麼greetingStrategy就相當于一個入口結點。
我知道上面這樣說你可能不好了解,那麼現在我們來看下如何實作:
首先,我們需要一個Handler處理程式,我們采用上一節的第二種方式,由于這種方式僅僅是将section結點進行傳遞,然後根據type建立對象,是以我們完全不需要對它進行任何更改。但是為了避免使這兩節的内容産生混淆,我們在CustomConfig下在新建立一個GeneralConfigHandler.cs檔案,将上面的代碼幾乎是原封不動地複制下來(改了類型名):
public class GeneralConfigurationHandler : IConfigurationSectionHandler {
// 這裡的section結點為 Web.Config中的 配置的根結點
// 直接将section進行傳遞
object[] parameters = { section };
然後,我們需要在Web.Config中定義一個程式配置的根結點,對于應用程式的所有配置,我們都将通過這個根節點進行通路,而我們上面建立的GeneralConfigHandler則用于建立對這個根結點進行映射的類型的執行個體(我們稍後會講述這個類型)。現在我們看一下這個Web.Config中該如何設定:
<configSections>
<section name="traceFact" type="CustomConfig.GeneralConfigurationHandler, CustomConfig"/>
</configSections>
<!-- General.aspx, 通用配置存儲方法 -->
<traceFact type="CustomConfig.ConfigManager, CustomConfig">
<blog name="JimmyZhang's Space">
<!-- 将上面的配置也包含進來 -->
<greetingStrategy type="ClassLib.ChineseGreeting, ClassLib" />
</traceFact>
注意上面的配置,我們将所有的配置都寫在了traceFact根結點下,對于其所有的子結點,我們都通過這個traceFact作為入口來通路。另外注意traceFact結點的type屬性,它是一個ConfigManager類型,這個類型的執行個體是由GeneralConfigurationHandler通過反射動态建立的。現在我們來看一下ConfigManager類型,它實際上隻是作為一個容器,包含其下具體的配置結點的引用:
public class ConfigManager {
private XmlNode section;
private ForumConfiguration forumConfig;
// private BlogConfiguration blogConfig;
// private MailServerConfiguration mailServerConfig;
// private IGreetingStrategy greetingStrategy;
// 以下類似,省略 ...
public ForumConfiguration ForumConfig {
get { return forumConfig; }
//public BlogConfiguration BlogConfig {
// get { return blogConfig; }
//} 以下類似,省略 ...
public ConfigManager(XmlNode section) {
this.section = section;
forumConfig = new ForumConfiguration(section.SelectSingleNode("forum"));
// blogConfig = new BlogConfiguration(section.SelectSingleNode("blog"));
// mailServerConfig = new MailServerConfiguration(section.SelectSingleNode("mailServer"));
// 以下類似,省略 ...
可以看到,這個ConfigManager僅僅是作為一個容器,包含對其下具體結點配置的引用,并通過traceFact根節點,擷取traceFact其下子結點,然後再建立用于映射具體的子結點的類型執行個體。
是以對于每一個子結點,我們都需要再建立一個自定義的類,這裡我僅建立一個 ForumConfiguration來做說明:
// 具體的子結點配置 forum 結點
public class ForumConfiguration {
private XmlNode forumNode;
// 将 forum 結點傳遞進來
public ForumConfiguration(XmlNode section){
this.forumNode = section;
public string Name{
get{ return forumNode.Attributes["name"].Value; }
public string RootUrl{
get { return forumNode.SelectSingleNode("root").Attributes["url"].Value; }
public int PageSize{
get { return int.Parse(forumNode.SelectSingleNode("pageSize").InnerText); }
public int ReplyCount{
get{ return int.Parse(forumNode.SelectSingleNode("replyCount").InnerText); }
public int OfflineTime{
get { return int.Parse(forumNode.SelectSingleNode("offlineTime").InnerText); }
接着建立一個General.aspx 檔案,添加如下代碼:
// General.aspx
<h1>統一結點配置管理</h1>
<b>Name:</b><asp:Literal ID="ltrName" runat="server"></asp:Literal><br />
<b>Root Url:</b><asp:Literal ID="ltrRootUrl" runat="server"></asp:Literal><br />
<b>Reply Count:</b><asp:Literal ID="ltrReplyCount" runat="server"></asp:Literal><br />
<b>Page Size:</b><asp:Literal ID="ltrPageSize" runat="server"></asp:Literal><br />
<b>Offline Time:</b><asp:Literal ID="ltrOfflineTime" runat="server"></asp:Literal>
// General.aspx.cs
// 擷取 ConfigManager 類型執行個體
ConfigManager config = (ConfigManager)ConfigurationManager.GetSection("traceFact");
ltrName.Text = config.ForumConfig.Name;
ltrOfflineTime.Text = config.ForumConfig.OfflineTime.ToString();
ltrPageSize.Text = config.ForumConfig.PageSize.ToString();
ltrReplyCount.Text = config.ForumConfig.ReplyCount.ToString();
ltrRootUrl.Text = config.ForumConfig.RootUrl.ToString();
然後打開頁面,應該可以看到如下的畫面:
總結
在這篇文章中,我向大家簡單地介紹了如何通過實作System.Configuration.IConfigurationSectionHandler 接口或者繼承System.Configuration.ConfigurationSection 基類來實作自定義結點。
我們先後學習了如何 使用内置.Net結點以及内置結點處理程式、使用自定義結點配合.Net内置處理程式、自定義結點及處理程式、通過在配置中儲存類型資訊然後使用反射動态建立對象來模拟存儲類型執行個體,最後我們結合反射以及配置資訊建立了自己的ConfigManager來實作對自定義配置結點的管理。
感謝閱讀,希望這片文章能對你有所幫助!