Microsoft Corporation
摘要:Dare Obasanjo 著眼於可用來表示在單個進程和 AppDomain 內的組件之間共享的、基於 XML 的數據的選項,並討論了每種方法在設計上的利弊。
引言
在最近的一次設計審查之後,一名職位是項目經理的同事詢問在 API 中公開 XML 時是否存在設計准則,因為他曾經看到過許多不同的方法,但是無法確定該選擇何種方法。我告訴他,我原來相信在 MSDN 上可以找到一些准則,但是當我查閱時卻只找到一段標題為 Passing XML Data Inside the CLR(英文)的 MSDN TV,其中雖然包含此類信息,但卻不容易閱讀。因此我萌發了這樣一個念頭,即為 Don Box 的 MSDN TV 片段提供一個便於打印的版本,並提供我本人在 Microsoft 處理 XML API 方面的一些經驗。
返回頁首
准則初探
在三種主要情況下,開發人員需要考慮使用何種 API 來表示 XML。下面簡要介紹一下這些情況和准則:
• 類的字段或屬性中包含 XML:如果類的字段或屬性是 XML 文檔或片段,則該類應提供將其屬性同時作為字符串和 XMLReader 進行操作的機制。
• 方法可以接受 XML 輸入或返回 XML 作為輸出:可以接受或返回 XML 的方法應有助於返回 XmlReader 或 XPathNavigator,除非用戶希望能夠編輯 XML 數據(此時應使用 XMLDocument)。
• 將對象轉換成 XML:如果對象出於序列化目的要以 XML 來表示其自身,當其需要獲得的 XML 序列化進程控制比 XmlSerializer 所提供的更多時,則應使用 XmlWriter。如果對象要以 XML 來表示其自身,以便能夠完全以 XML 世界成員的身份參與到其中(如允許在此對象上進行 XPath 查詢或 XSLT 轉換),則此對象應實現 IXPathNavigable 接口。
在下面各部分中,我詳細介紹了上面提到的幾種情況,並解釋了我是如何獲得這些准則的。
返回頁首
常見疑點
當您決定使用方法或屬性接受或返回 XML 時,您的大腦中會出現 .NET Framework 中的許多類,它們都適用於此任務。下面列出了 .Net Framework 中最適合於表示 XML 輸入或輸出的五個類,並且給出了其正反兩方面的簡要說明。
1.
System.Xml.XmlReader(英文):XMLReader 是 .Net Framework 的拉模型 XML 分析程序。在拉模型處理過程中,XML 用戶通過根據需要從 XML 制造者那裡請求事件來控制程序流。拉模型 XML 分析程序(如 XmlReader)以只進的流方式進行操作,同時只顯示單個節點在任意給定時間的相關信息。事實上,XmlReader 不需要整個 XML 文檔加載到內存並且是只讀的,這使其成為以非 XML 數據源創建 XML 外觀的一個不錯選擇。這種以非 XML 數據源創建 XML 外觀的一個示例是 XmlCsvReader(英文)。有人可能將 XmlReader 的只進特性視為一種局限性,因為它使用戶無法通過 XML 文檔的各個部分進行多次傳遞。
2.
System.Xml.XPath.XPathNavigator(英文):XPathNavigator 是 XML 數據源上的只讀光標。XML 光標就像是一個透鏡,一次聚焦於一個 XML 節點上,但與基於拉的 API(如 XmlReader)不同,它可以在任意給定時間將光標定位在 XML 文檔的任意位置。在某種程度上,拉模型 API 是光標模型的只進版本。XPathNavigator 還是以非 XML 數據實現 XML 外觀的一個很好的候選項,因為它允許您以實時方式構造數據源的 XML 視圖,而不必將整個數據源轉換成 XML 樹。使用 XPathNavigator 創建非 XML 數據的 XML 視圖的一個示例是 ObjectXPathNavigator(英文)。事實上,XPathNavigator 是只讀的,而且缺少一些用戶友好屬性(如 InnerXml(英文)和 OuterXml(英文)),這使其在需要這些功能時不如使用 XmlDocument 或 XMLNode 那樣另人滿意。
3.
System.Xml.XmlWriter(英文):XmlWriter 提供了將 XML 文檔推入基本存儲區的一般機制。該基本存儲區可以是從文件(如果使用的是 XmlTextWriter)到 XmlDocument(如果使用的是 XmlNodeWriter(英文))的任何內容。使用 XmlWriter 作為返回 XML 的方法的參數為支持各種可能的返回類型(包括文件流、字符串和 XmlDocument 實例)提供了一種可靠方式。這就是為什麼 XmlSerializer.Serialize() method(英文)接受 XmlWriter 作為其中一個重載上的參數。正如其名稱所包含的意思,XmlWriter 只對編寫 XML 有用,不能用於讀取或處理 XML。
4.
System.Xml.XmlDocument/XmlNode(英文):XmlDocument 是 W3C Document Object Model (DOM)(英文)的實現。DOM 是由 XmlNode 對象的分層樹所構成的 XML 文檔在內存中的表示,這些對象代表 XML 文檔的邏輯組件,如元素、屬性和文本節點。DOM 是在 .Net Framework 中操作 XML 的最流行的 API,因為它提供了一種直接的方法來加載、處理和保存 XML 文檔。DOM 的主要缺陷是其設計需要將整個 XML 文檔加載到內存中。
5.
System.String(英文):XML 是一種基於文本的格式,而且比 String 類能更好地表示文本。使用字符串作為 XML 表示方法的主要優點在於字符串是最小公分母。字符串容易寫入日志文件或打印到控制台,而且如果您要使用 XML API 對 XML 進行實際處理,則可以將字符串加載到 XmlDocument 或 XPathDocument 中。使用字符串作為利用 XML 的方法或屬性的主要輸入或輸出存在幾個問題。使用字符串的第一個問題類似於 DOM,它們都需要將整個 XML 文檔加載到內存中。其次,以字符串表示 XML 會增加制造者生成 XML 字符串的負擔,在某些情況下這可能非常麻煩。生成 XML 字符串可能非常麻煩的一個示例就是從 XmlReader 或 XPathNavigator 獲得 XML 時。第三,以字符串表示 XML 可能會導致與字符編碼相關的混亂,因為無論在 XML 中放入什麼編碼聲明,.Net Framework 中的字符串始終是 UTF-16 字符編碼。最後,以字符串表示 XML 使得創建 XML 處理管道較為困難,因為管道中的每一層都必須重新分析文檔。
返回頁首
類的字段或屬性中包含 XML
在某些情況下,對象的字段或屬性可能是一個 XML 文檔或 XML 片段。下面的示例類代表一封電子郵件,其內容是 XHtml。郵件的 XML 內容是以字符串表示的,並且由該類的 Body 屬性公開:
public class Email{ private string from;public string From{ get { return from; } set {from = value; }}private string to;public string To{ get { return to; } set {to = value; }}private string subject;public string Subject{ get { return subject; } set {subject = value; }}private DateTime sent;public DateTime Sent{ get { return sent; } set {sent = value; }}private XmlDocument body = new XmlDocument();public string Body{ get { return body.OuterXML; } set { body.Load(new System.IO.StringReader(value));}} }
使用字符串作為表示 XML 文檔的字段或屬性的主要表示方式是最具用戶友好性的表示方式,因為 System.String 類是一般開發人員最熟悉的 XML“常見疑點”。但是,這將會增加使用該類的用戶的負擔,這些用戶現在可能必須應付兩次分析 XML 文檔的成本。例如,設想一種情況:在從 SqlCommand.ExecuteXmlReader() method(英文)或 XslTransform.Transform() method(英文)所得到的 XML 中設置了這樣一個屬性。在這種情況下,用戶將必須分析文檔兩次,如下例所示:
Email email= new Email();email.From = "
[email protected]";email.To= "
[email protected]";email.Subject = "Hello World";XslTransform transform = new XslTransform();transform.Load("format-body.xsl");XmlDocument body = new XmlDocument();//1. XML 由 XmlDocument.Load() 分析 body.Load(transform.Transform(new XPathDocument("body.xml"), null)); //2. 同一 XML 被 Email.Body 屬性中的 XmlDocument.Load() 再次分析email.Body = body.OuterXML;
在上例中,同一 XML 被分析了兩次,因為在將該 XML 轉換成字符串之前必須將其從 XmlReader 加載到 XmlDocument 中,然後使用該 XML 設置 Email 類的 Body 屬性,再由該屬性本身從內部將該 XML 分析為 XmlDocument。一條很有效的經驗法則就是提供對 XML 屬性的訪問(作為 XMLReader 和字符串)。因此,還應將下列方法添加到 Email 類,以使該類的用戶獲得最大的靈活性:
public void SetBody(XmlReader reader){ body.Load(reader);}public XmlReader GetBody(){ return new XMLNodeReader(body);}
這為 Email 類的用戶提供了一種方式,使這些用戶在需要時能夠以有效的方式傳遞、設置和檢索 Body 屬性中的 XML 數據。
准則 如果類的字段或屬性是 XML 文檔或片段,則該類應提供將其屬性同時作為字符串和 XMLReader 進行操作的機制。
敏銳的讀者可能會注意到,如果您直接將 XmlDocument 作為屬性公開,則應滿足該准則,還應使該屬性的用戶可以精確更改 XML。
返回頁首
方法可以接受 XML 輸入或者返回 XML 作為輸出
當設計生成或使用 XML 的方法時,開發人員有責任使這樣的方法在接受輸入時具有靈活性。當方法接受 XML 作為輸入時,您可以將這些方法分為要求在適當位置修改數據的方法,以及只需對 XML 進行只讀訪問的方法。唯一一個支持讀寫訪問的“XML 常見疑點”是 XMLDocument。下面的代碼示例顯示了這樣一個方法:
public void ApplyDiscount(XmlDocument priceList){ foreach(XMLElement price in priceList.SelectNodes("//price")){price.InnerText = (Double.Parse(price.InnerText) * 0.85).ToString(); }}
對於需要對 XML 進行只讀訪問的方法,有兩個主要選項:
• XMLReader
• XPathNavigator
XmlReader 提供了對 XML 的只進訪問,而 XPathNavigator 不但提供了對基本 XML 源的隨機訪問,還提供了對數據源執行 XPath 查詢的能力。以下代碼示例將打印下面的 XML 文檔中的 artist(藝術家)和 title(標題):
<items> <compact-disc><price>16.95</price><artist>Nelly</artist><title>Nellyville</title> </compact-disc> <compact-disc><price>17.55</price> <artist>Baby D</artist> <title>Lil Chopper Toy</title> </compact-disc></items>
XMLReader:
public static void PrintArtistAndPrice(XmlReader reader){ reader.MoveToContent(); //move from root node to document element (items) /* 保持讀取,直至獲得第一個 <artist> 元素 */ while(reader.Read()){if((reader.NodeType == XMLNodeType.Element) && reader.Name.Equals("artist")){artist = reader.ReadElementString();title= reader.ReadElementString();break; } } Console.WriteLine("Artist={0}, Title={1}", artist, title);}}
XPathNavigator:
public static void PrintArtistAndPrice(XPathNavigator nav){ XPathNodeIterator iterator = nav.Select("/items/compact-disc[1]/artist /items/compact-disc[1]/title"); iterator.MoveNext(); Console.WriteLine("Artist={0}", iterator.Current); iterator.MoveNext(); Console.WriteLine("Title={0}", iterator.Current);}
通常,返回 XML 的方法都使用類似的規則。如果希望接收方編輯 XML,則應返回 XmlDocument。否則,應根據方法是否需要提供對 XML 數據的只進流式訪問而返回 XMLReader 或 XPathNavigator。
准則 接受或返回 XML 的方法應有助於返回 XmlReader 或 XPathNavigator,除非用戶希望能夠編輯 XML 數據(此時應使用 XMLDocument)。
上述准則的意思是指返回 XML 的方法應有助於返回 XmlReader,因為它比其他任何類型都適用於更多用戶的情況。此外,當方法的調用方需要更多功能時,它們可以從返回的 XmlReader 加載 XMLDocument 或 XPathDocument。
返回頁首
將對象轉換為 XML
XML 作為信息交換的通用語言已無所不在,這使其成為要以 XML 表示自身的某些對象的顯而易見的選擇,這些對象或者是出於序列化目的,或者是為了獲得對其他 XML 技術的訪問(如使用 XPath 進行查詢或使用 XSLT 進行轉換)。
當出於序列化目的將對象轉換成 XML 時,顯然應選擇使用 XML Serialization technology in the .Net Framework(英文)。但是,在某些情況下,您對生成的 XML 所需的控制可能比 XmlSerializer 所能提供的更多。在這種情況下,工具包中的 XmlWriter 便是一個很有用的類,因為它使您不再需要該類的結構與所生成的 XML 之間存在一對一映射。下例顯示了通過使用 XmlWriter 序列化 Email 類(前面幾部分中已經提到)而生成的 XML。
public void Save(XmlWriter writer){ writer.WriteStartDocument();writer.WriteStartElement("email");writer.WriteStartElement("headers"); writer.WriteStartElement("header"); writer.WriteElementString("name", "to");writer.WriteElementString("value", this.To);writer.WriteEndElement(); //標題 writer.WriteStartElement("header"); writer.WriteElementString("name", "from");writer.WriteElementString("value", this.From);writer.WriteEndElement(); //標題 writer.WriteStartElement("header"); writer.WriteElementString("name", "subject");writer.WriteElementString("value",this.Subject);writer.WriteEndElement(); //標題writer.WriteStartElement("header"); writer.WriteElementString("name", "sent");writer.WriteElementString("value", XMLConvert.ToString(this.Sent));writer.WriteEndElement(); //標題 writer.WriteEndElement(); //標題;writer.WriteStartElement("body"); writer.WriteRaw(this.Body);writer.WriteEndDocument(); //關閉所有打開的標記}
這段代碼生成下面的 XML 文檔
<email><headers> <header><name>收件人</name><value>
[email protected]</value> </header> <header><name>發件人</name><value>
[email protected]</value> </header> <header><name>主題</name><value>Hello World</value> </header> <header><name>發送時間</name><value>2004-03-05T15:54:13.5446771-08:00</value> </header></headers><body><p>Hello World 是我最喜歡的示例應用程序。</p></body></email>
只使用 XmlSerializer 的基本功能將不可能生成上面的 XML 文檔。XmlWriter 的另一個優點是它可以從基本目標提取到數據要寫入的目標,因此它可以是從磁盤上的文件到內存中的字符串(甚至是 XmlNodeWriter(英文)的 XMLDocument 稱謂)的任何內容。
如果要求提供一種方式,使類可以更完全地參與到 XML 世界中(如與XPath 或 XSLT 等 XML 技術進行交互),則該類的最佳選擇是實現 IXPathNavigable 接口,並為該類提供 XPathNavigator。這樣做的一個示例是 ObjectXPathNavigator,它為那些使您可以在上述對象上執行 XPath 查詢或運行 XSLT 轉換的任意對象提供了 XML 視圖。
准則 如果對象出於序列化目的要以 XML 來表示其自身,當其需要獲得的 XML 序列化過程控制比 XmlSerializer 所提供的更多時,則應使用 XmlWriter。如果對象要以 XML 來表示其自身,以便能夠完全以 XML 世界成員的身份參與到其中(如允許在此對象上進行 XPath 查詢或 XSLT 轉換),則此對象應實現 IXPathNavigable 接口。
返回頁首
結論
在將來的 .NET Framework 版本中,將會更加強調基於光標的 XML API(如由 IXPathNavigable 接口公開的 XPathNavigator)。這類光標將成為與 .Net Framework 中的 XML 進行交互的主要機制。
Dare Obasanjo 是 Microsoft WebData 小組的成員,除其他事務外,該小組還開發了 .Net Framework 的 System.Xml 和 System.Data 命名空間、Microsoft XML 核心服務 (MSXML) 以及 Microsoft 數據訪問組件 (MDAC) 中的組件。
有關本文的任何問題或評論,歡迎張貼到 GotDotNet 上的 Extreme XML 留言板。