本頁內容
簡介
自動代碼生成 — 無論是數據訪問層、業務實體類還是用戶界面 — 可以大大提高開發人員的生產效率。這一生成過程可以基于許多輸入,例如數據庫、任意 XML 文件、UML 關系圖等。Microsoft?Visual Studio?.NET 隨附了對從 W3C XML 架構文件 (XSD) 進行代碼生成的內置支持,分為兩種形式:類型化數據集以及與 XmlSerializer 配合使用的自定義類。
XSD 文件描述了允許包含在 XML 文檔中以便被該文檔視為有效的內容。由于需要以類型安全的方式對數據進行處理(這些數據最終將被序列化為 XML 數據以供使用),因此產生了各種將 XSD 轉換為類的方法。我們可以回想一下,XSD 并“不是”作為一種描述對象及其關系的手段而創建的。已經存在一種更好的格式可用于該目的,它就是 UML,并且已被廣泛用來對應用程序進行建模以及根據模型生成代碼。因此,在 .NET 及其面向對象的編程 (OOP) 概念以及 XSD 的概念之間存在某些(預料的)不匹配現象。當您將 XSD 映射到類時,請記住這一點。
也就是說,可以將 CLR 類型系統視為 XSD 的子集:它支持一些無法映射到常規 OO 概念的功能。因此,如果您只是使用 XSD 來對類進行建模,而不是對文檔進行建模,您很可能找不到任何沖突。
在本文的其余部分,我們將討論類型化數據集方法,還將討論通過 xsd.exe 工具生成的自定義類如何有助于得到更好的解決方案,以及如何擴展和自定義從 XSD 到類的生成過程的輸出。
為了最深入地領會本文的內容,您需要對 CodeDom 有一些基本的了解。
類型化數據集有什么問題?
類型化數據集正在越來越多地用于表示業務實體,也就是說,用于充當應用程序各個層之間的實體數據傳送器,甚至充當 Web 服務的輸出。與“正常的”數據集不同,類型化數據集很有吸引力,因為您還可以獲得對表、行、列等的類型化訪問。然而,它們并非沒有代價和/或限制:
• |
實現開銷:數據集包含許多可能不為您的實體所需的功能,如更改跟蹤、類似于 SQL 的查詢、數據視圖、大量事件等等。 |
• |
性能:與 XML 之間的序列化速度不夠快。XmlSerializer 的性能很容易超過它。 |
• |
互操作性:對于返回類型化數據集的 Web 服務的非 .NET 客戶端而言,可能難以解決。 |
• |
XML 結構:許多分層的(而且完全有效的)文檔及其架構無法扁平化為表模型。
獲取有關類型化數據集的更多信息。 |
因此,除非數據集的其他功能普遍對您有用,否則使用類型化數據集進行數據傳遞可能不是最佳選擇。值得慶幸的是,還有另一個可以利用的選擇。
XmlSerializer 和自定義類
XmlSerializer 改善了 XML 數據處理方法。通過序列化特性,XmlSerializer 能夠根據對象的 XML 表示形式還原對象,并且能夠反序列化到 XML 形式。此外,它還能夠以非常有效的方式完成這些工作,因為它可以生成動態編譯的基于 XmlReader 的(因而也是流式的)類,該類專門用于序列化(以及反序列化)具體的類型。所以,它確實非?旖。
閱讀有關 XML 序列化特性的更多內容。
當然,猜測使用哪些特性以便符合某個 XSD 絕對不是一件好玩的事情。為了解決這個問題,.NET SDK 隨附了一個可以幫助您完成艱苦工作的實用工具:xsd.exe。它是一個命令行應用程序,能夠根據 XSD 文件生成類型化數據集和自定義類。自定義類在生成后具有相應的 XML 序列化特性,因此在進行序列化時,可以保證完全忠實于架構。
閱讀 Don Box 對 XSD 以及 CLR 映射和特性的介紹。
迄今為止,一切都很好。我們具有有效且快速的方法將 XML 轉換為對象或者將對象轉換為 XML,并且我們具有能夠為我們生成類的工具。問題在于,我們有時希望得到與所生成的內容稍有不同的內容。例如,xsd.exe 所生成的類無法數據綁定到 Windows 窗體網格,因為它查找屬性而不是公共字段來顯示。我們可能希望在許多地方添加自己的自定義特性,將數組更改為類型化集合,等等。當然,我們在做這些事情的時候,應保證在序列化時能夠與 XSD 兼容。
自定義 XSD 將明顯改變所生成的類的形式。如果您只是期望將 PascalCaseIf 變成實際的 XML 標準以便使用 camelCase,那么我建議您三思而后行。MS 的一些即將問世的產品表明它們將要使用 PascalCase 來表示 XML,以便使它們更好地支持 .NET。
如果您需要進行更多的與上述自定義類似的自定義,您的選擇是什么?人們幾乎普遍認為 xsd.exe 是不可擴展的,并且沒有辦法對其進行自定義。這是不準確的,因為 .NET XML 團隊實際上向我們提供了恰好可供該工具使用的類。您將需要自己動手使用 CodeDom 以便利用它們,但自定義程度只受到您需要的限制!
您可以在下列文章中閱讀有關 CodeDom 的內容:
Generating and Compiling Source Code Dynamically in Multiple Languages
Generate .NET Code in Any Language Using CodeDOM
基礎類
一種從 XSD 生成代碼的方法是以統一的方式對架構對象模型 (SOM) 進行簡單的迭代,并直接根據該模型編寫代碼。這正是大量為克服 xsd.exe 工具局限而創建的代碼生成器所采取的方法。不過,這需要付出相當大的努力以及編寫大量的代碼,因為我們應考慮 XSD 到 CLR 類型映射、XSD 類型繼承、XML 序列化特性等問題。掌握 SOM 也不是一件輕而易舉的事情。如果無須由我們自己來完成所有工作,而只需添加或修改 xsd.exe 工具的內置代碼生成,難道不好嗎?
正像我前面所說的,但與普遍看法不同的是,xsd.exe 用于生成輸出的類就在 System.Xml.Serialization 命名空間中并被聲明為公共類,即使 xsd.exe 工具在某種程度上不允許進行任何類型的自定義。它們中的大多數確實未進行記載,但我將在這一部分中向您說明如何使用它們。請不要被 MSDN 幫助中的以下聲明嚇。骸癧TheTopSecretClassName] 類型支持Microsoft? .NET 框架基礎結構,并且不適合直接從您的代碼中使用”。我將在不進行胡亂刪改以及不采用任何反射代碼的前提下使用它們。
一種比相當平常的 "StringBuilder.Append" 代碼生成好得多的方法是利用 System.CodeDom 命名空間中的類,而這正是內置代碼生成類(從現在開始簡稱為 codegen)所做的。通過 CodeDom 中包含的一些類,我們可以用一種與語言無關的方式,在所謂的 AST(抽象語法樹)中表示幾乎所有的編程構造。稍后,另一個類(代碼生成器)可以對其進行解釋并生成您期望的原始代碼,例如Microsoft? Visual C# 或Microsoft? Visual Basic?.NET 代碼。這就是 .NET 框架中大多數代碼生成過程的工作方式。
Codegen 方法不僅利用這一點,還通過映射過程來分離架構分析和實際的 CodeDom 生成。對于我們希望為其生成代碼的每個架構元素,都必須執行該映射。從根本上說,它將構建一個新的對象以表示分析的結果,例如它的結構(這將是要為其生成的類型名)、它的成員以及這些成員的 CLR 類型等。
為了使用這些類,我們將遵循一個基本的工作流程,如下所述:
1. |
加載架構(原則上加載一個)。 |
2. |
為每個頂級 XSD 元素派生一系列映射。 |
3. |
將這些映射導出到 System.CodeDom.CodeDomNamespace。 |
在此過程中涉及到四個類,它們都定義在 System.Xml.Serialization 命名空間中:

圖 1. 用于獲得 CodeDom 樹的類
可以按以下方式,使用這些類來獲得 CodeDom 樹:
namespace XsdGenerator
{
public sealed class Processor
{
public static CodeNamespace Process( string xsdFile,
string targetNamespace )
{
// Load the XmlSchema and its collection.
XmlSchema xsd;
using ( FileStream fs = new FileStream( xsdFile, FileMode.Open ) )
{
xsd = XmlSchema.Read( fs, null );
xsd.Compile( null );
}
XmlSchemas schemas = new XmlSchemas();
schemas.Add( xsd );
// Create the importer for these schemas.
XmlSchemaImporter importer = new XmlSchemaImporter( schemas );
// System.CodeDom namespace for the XmlCodeExporter to put classes in.
CodeNamespace ns = new CodeNamespace( targetNamespace );
XmlCodeExporter exporter = new XmlCodeExporter( ns );
// Iterate schema top-level elements and export code for each.
foreach ( XmlSchemaElement element in xsd.Elements.Values )
{
// Import the mapping first.
XmlTypeMapping mapping = importer.ImportTypeMapping(
element.QualifiedName );
// Export the code finally.
exporter.ExportTypeMapping( mapping );
}
return ns;
}
}
}
這些代碼非常簡單,盡管您可能希望在其中添加異常管理代碼。需要注意的一件事情是 XmlSchemaImporter 通過使用類型的限定名來導入類型,然后將其放在相應的 XmlSchema 中。因此,必須將架構中的所有全局元素傳遞給它,然后使用 XmlSchema.Elements 集合進行迭代。該集合像 XmlSchemaElement.QualifiedName 一樣,也是在架構編譯之后被填充的所謂的 Post Schema Compilation Infoset(即 PSCI,請參閱 MSDN 幫助)的成員。它具有在解析引用、架構類型、繼承、包含等之后填充和組織架構信息的作用。其功能類似于 DOM Post Validation Infoset(即 PSVI,請參閱 Dare Obasanjo 的 MSDN 文章和 XSD 規范)。
您可能已經注意到 XmlSchemaImporter 工作方式的一個副作用(實際上是一個缺陷):您只能檢索(導入)全局定義的元素的映射。在架構中的任何位置局部定義的任何其他元素將無法通過該機制訪問。這具有我將在后面討論的一些后果,它們可能會限制您可以應用的自定義,或者影響我們的架構設計。
XmlCodeExporter 類根據所導入的映射,用類型定義來填充傳遞給其構造函數的 CodeDomNamespace,從而生成所謂的 CodeDom 樹。通過上述方法得到的 CodeDom 就是 xsd.exe 工具在內部生成的東西。有了該樹以后,就可以直接將其編譯為程序集,或者生成源代碼。
如果我希望擺脫 xsd.exe 工具,可以輕松地生成使用該類的控制臺應用程序。為達到該目的,我需要根據收到的 CodeDom 樹生成一個源代碼文件。我通過創建一個適用于用戶所選的目標語言的 CodeDomProvider 來做到這一點: static void Main( string[] args )
{
if ( args.Length != 4 )
{
Console.WriteLine(
"Usage: XsdGenerator xsdfile namespace outputfile [cs|vb]" );
return;
}
// Get the namespace for the schema.
CodeNamespace ns = Processor.Process( args[0], args[1] );
// Create the appropriate generator for the language.
CodeDomProvider provider;
if ( args[3] == "cs" )
provider = new Microsoft.CSharp.CSharpCodeProvider();
else if ( args[3] == "vb" )
provider = new Microsoft.VisualBasic.VBCodeProvider();
else
throw new ArgumentException( "Invalid language", args[3] );
// Write the code to the output file.
using ( StreamWriter sw = new StreamWriter( args[2], false ) )
{
provider.CreateGenerator().GenerateCodeFromNamespace(
ns, sw, new CodeGeneratorOptions() );
}
Console.WriteLine( "Finished" );
Console.Read();
}
|
我可以使用生成器所收到的 CodeGeneratorOptions 實例的屬性,進一步自定義生成的代碼格式和其他選項。有關可用的選項,請參閱 MSDN 文檔。
在編譯該控制臺應用程序后,我可以生成與 xsd.exe 工具所生成的完全相同的代碼。有了這一功能,使我完全不必再依賴該工具,并且我不再需要知道該工具是否已安裝或者位于何處,也不再需要為它啟動新的進程,等等。然而,每當我修改架構以后,都需要一遍遍地從命令行運行它,這是很不理想的。Microsoft?Visual Studio?.NET 使開發人員可以通過所謂的自定義工具來利用設計時代碼生成。其中一個例子是類型化數據集,當您使用它時(盡管不必具體指定),都會有一個自定義工具在您每次保存數據集 XSD 文件時對其進行處理,并自動生成相應的“代碼隱藏”類。
有關構建自定義工具的內容超出了本文的范圍,但您可以閱讀更多有關將我迄今為止所編寫的代碼轉換為該網絡日記張貼中的自定義工具的內容。該工具的代碼包含在本文的下載內容中,您可以通過將“XsdCodeGen”自定義工具名稱指定給 XSD 文件屬性來簡單地使用它。注冊方法在隨附的自述文件中進行了說明。
即使我能夠找到更容易使用的自定義工具,但是將 xsd.exe 工具替換為另一個執行完全相同任務的工具并沒有太大意義,不是嗎?畢竟,我們完成這些工作的原因就是為了改變這種做法!因此,讓我們從這一底線開始對其進行自定義。
返回頁首
擴展 XSD 處理
為了自定義處理過程,我需要將信息傳遞給該工具,以便它知道要更改或處理的內容。此時有兩種主要選擇:
第一種方法最初可能很有吸引力,因為它非常簡單。我只需添加一個特性,然后相應地修改處理器以檢查該特性:
架構:
<xs:schema elementFormDefault="qualified" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:code="http://weblogs.asp.net/cazzu" code:fieldsToProperties="true"> |
代碼:
XmlSchema xsd; // Load the XmlSchema. ... foreach (XmlAttribute attr in xsd.UnhandledAttributes) { if (attr.NamespaceURI == "http://weblogs.asp.net/cazzu") { switch (attr.LocalName) { case "fieldsToProperties": if (bool.Parse(attr.Value)) ConvertFieldsToProperties(ns); break; ... } } } |
這正是您通常會在其他從 xsd 到類的生成器中看到的方法(您可以在 Code Generation Network 中找到大量類似的生成器)。遺憾的是,該方法將導致長長的 switch 語句、無盡的特性,并最終導致代碼難以維護并缺乏可擴展性。
第二種方法更為健壯,因為它從一開始就考慮了可擴展性。XSD 通過 元素提供此類擴展工具,該元素可以是架構中幾乎所有項目的子元素。我將利用該元素及其 子元素,以便使開發人員可以指定運行哪些(任意)擴展以及按什么順序運行。這樣的擴展架構將如下所示:
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:annotation> <xs:appinfo> <Code xmlns="http://weblogs.asp.net/cazzu"> <Extension Type="XsdGenerator.Extensions.FieldsToPropertiesExtension, XsdGenerator.CustomTool" /> </Code> </xs:appinfo> </xs:annotation> |
當然,每個擴展都將需要實現一個公共接口,以便自定義工具可以輕松地執行各個擴展:
public interface ICodeExtension { void Process( System.CodeDom.CodeNamespace code, System.Xml.Schema.XmlSchema schema ); } |
通過預先提供此類可擴展性,當產生新的自定義需要時,就可以很容易地進行其他自定義。甚至還可以從一開始就將最基本的代碼實現為擴展。
可擴展的代碼生成工具
我將修改 Processor 類以添加這種新功能,并且將簡單地從架構中檢索各個 元素。盡管如此,這里還需要提出一個警告:與那些為元素、特性、類型等公開的 Post Schema Compilation Infoset 屬性不同,在架構級別沒有針對注釋的類型化屬性。也就是說,沒有 XmlSchema.Annotations 屬性。因此,需要對 XmlSchema.Items 的一般性預編譯屬性進行迭代,以便查找注釋。而且,在檢測到 XmlSchemaAnnotation 項目之后,再次需要對其自己的 Items 一般性集合進行迭代,這是因為除了 子元素以外,還可能有 子元素,而它也缺少類型化屬性。當最終通過 XmlSchemaAppInfo.Markup 屬性獲得 appinfo 的內容之后,我們所得到的全部內容是一個 XmlNode 對象數組。您可以想像如何進行后續處理:對節點進行迭代,再對其子元素進行迭代,等等。這將產生非常丑陋的代碼。
值得慶幸的是,XSD 文件只是一個 XML 文件,因此可以使用 XPath 來對其進行查詢。
為了提高執行速度,我將在 Processor 類中保留 XPath 的靜態編譯表達式,它將在其靜態構造函數中進行初始化:
public sealed class Processor { public const string ExtensionNamespace = "http://weblogs.asp.net/cazzu"; private static XPathExpression Extensions; static Processor() { XPathNavigator nav = new XmlDocument().CreateNavigator(); // Select all extension types. Extensions = nav.Compile ("/xs:schema/xs:annotation/xs:appinfo/kzu:Code/kzu:Extension/@Type"); // Create and set namespace resolution context. XmlNamespaceManager nsmgr = new XmlNamespaceManager(nav.NameTable); nsmgr.AddNamespace("xs", XmlSchema.Namespace); nsmgr.AddNamespace("kzu", ExtensionNamespace); Extensions.SetContext(nsmgr); } |
注 有關 XPath 預編譯和執行的優點、細節和高級應用的更多信息,請參閱 Performant XML (I): Dynamic XPath expressions compilation 和 Performant XML (II): XPath execution tips。
Process() 方法需要在將 CodeNamespace 返回給調用方之前,執行該查詢并執行它找到的每個 ICodeExtension 類型:
XPathNavigator nav; using ( FileStream fs = new FileStream( xsdFile, FileMode.Open ) ) { nav = new XPathDocument( fs ).CreateNavigator(); } XPathNodeIterator it = nav.Select( Extensions ); while ( it.MoveNext() ) { Type t = Type.GetType( it.Current.Value, true ); // Is the type an ICodeExtension? Type iface = t.GetInterface( typeof( ICodeExtension ).Name ); if (iface == null) throw new ArgumentException( "Invalid extension type '" + it.Current.Value + "'." ); ICodeExtension ext = ( ICodeExtension ) Activator.CreateInstance( t ); // Run it! ext.Process( ns, xsd ); } return ns; |
我使用 Type.GetInterface() 而不是 Type.IsAssignableFrom() 來測試接口實現情況,因為它能夠快速跳到非托管代碼,所以需要的開銷較少。它們的效果是相同的,然而,使用后者將返回一個布爾值,而不是一個“類型”(如果未找到接口,則返回空值)。 返回頁首
XmlSerializer 的內部原理
有了 CodeDom 以后,可以為追求自定義的開發人員帶來大量能力和靈活性,但同時也帶來了更大的責任。以這種方式修改代碼會有危險,因為這會使代碼不再按與架構兼容的方式進行序列化,或者 XmlSerializer 功能被完全破壞,并針對意外的節點和特性引發異常,從而無法檢索值,等等。
因此,在處理生成的代碼之前,絕對需要了解 XmlSerializer 的內部原理,當然也就需要一種了解其內部原理的方法。
當對象即將進行 XML 序列化時,將通過反射您傳遞給 XmlSerializer 構造函數的類型來創建一個臨時程序集(這就是您需要那么做的原因)。請等一下!不要因為“反射”一詞而感到害怕!這對于每個類型只執行一次,并且在 AppDomain 生命期內,將創建一對極為有效的 Reader 和 Writer 類來處理序列化和反序列化。
這些類繼承了 System.Xml.Serialization 命名空間中的 XmlSerializationReader 和 XmlSerializationWriter 公共類。它們還是 [TheTopSecretClassName]。如果您希望看一下這些動態生成的類,您只需向應用程序配置文件(對于 Web 應用程序,為 web.config)中添加以下設置: <system.diagnostics> <switches> <add name="XmlSerialization.Compilation" value="4"/> </switches> </system.diagnostics>
現在,序列化程序將不會刪除在該過程中生成的臨時文件。對于 Web 應用程序,這些文件將位于 C:\Documents and Settings\[YourMachineName]\ASPNET\Local Settings\Temp 中;或者,它們將位于當前用戶的 Local Settings\Temp 文件夾中。
您將看到的代碼就是當您希望有效地加載 .NET 中的 XML 時需要編寫的代碼:使用嵌套的 while 和 if 語句進行讀取,使用 XmlReader 方法在數據流中向下移動,等等。使用這些丑陋代碼的目的就是使該處理過程真正地快起來。
還可以通過使用 Chris Sells 的 XmlSerializerPreCompiler 工具來診斷所生成的這些類中的問題。
我們可以查看此代碼,以便分析在序列化程序所生成的類中進行更改的效果。
通過 CodeDom 自定義.
某些自定義能夠立即產生吸引力,因為它們是人們經常關心的與 xsd.exe 工具生成的類有關的問題。
將字段轉化為屬性
大多數開發人員抱怨的問題之一是,xsd.exe 工具生成的類帶有公共字段,而不是由私有字段支持的屬性。XmlSerializer 生成的類通過使用常規的 [object].[member] 注釋來讀寫該類的實例中的值。當然,從編譯和源代碼的角度來看,[member] 是字段還是屬性沒有什么區別。
因此借助于 CodeDom,可以更改 XSD 的默認類。由于自定義 codegen 工具中內置的可擴展性,需要做的所有工作只是實現一個新的 ICodeExtension。該擴展將處理 CodeDom 樹中的每個類型,而無論它是一個類還是一個結構:
public class FieldsToPropertiesExtension : ICodeExtension { #region ICodeExtension Members public void Process( System.CodeDom.CodeNamespace code, System.Xml.Schema.XmlSchema schema ) { foreach ( CodeTypeDeclaration type in code.Types ) { if ( type.IsClass || type.IsStruct ) { // Turn fields to props |
現在,我需要對該類型的每個成員(可能是字段、屬性、方法等等)進行迭代,并且只處理 CodeMemberField 成員。不過,我不能只對 type.Members 集合執行 foreach 操作,因為對于每個字段而言,我都需要向同一集合中添加屬性。這將導致發生異常,因為 foreach 結構所使用的基礎枚舉數可能會無效。因此,我需要將當前成員復制到某個數組中,然后改為對該數組進行迭代:
CodeTypeMember[] members = new CodeTypeMember[type.Members.Count]; type.Members.CopyTo( members, 0 ); foreach ( CodeTypeMember member in members ) { // Process fields only. if ( member is CodeMemberField ) { // Create property Next, I create the new property: CodeMemberProperty prop = new CodeMemberProperty(); prop.Name = member.Name; prop.Attributes = member.Attributes; prop.Type = ( ( CodeMemberField )member ).Type; // Copy attributes from field to the property. prop.CustomAttributes.AddRange( member.CustomAttributes ); member.CustomAttributes.Clear(); // Copy comments from field to the property. prop.Comments.AddRange( member.Comments ); member.Comments.Clear(); // Modify the field. member.Attributes = MemberAttributes.Private; Char[] letters = member.Name.ToCharArray(); letters[0] = Char.ToLower( letters[0] ); member.Name = String.Concat( "_", new string( letters ) ); |
請注意,我向新的屬性中復制了字段名、它的成員特性以及類型。我將注釋和自定義特性(XmlSerialization 特性)移出字段,然后移到屬性(AddRange() 和 Clear())中。最后,我將該字段變為私有字段,并將其首字母轉化為小寫,在它前面加上“_”字符,這對于由屬性支持的字段而言,是一種相當通用的命名規則。
但仍然缺少屬性中最重要的元素:屬性的 get 和 set 訪問器的實現。因為它們只是對字段值進行傳遞,所以都非常簡單:
prop.HasGet = true; prop.HasSet = true; // Add get/set statements pointing to field. Generates: // return this._fieldname; prop.GetStatements.Add( new CodeMethodReturnStatement( new CodeFieldReferenceExpression( new CodeThisReferenceExpression(), member.Name ) ) ); // Generates: // this._fieldname = value; prop.SetStatements.Add( new CodeAssignStatement( new CodeFieldReferenceExpression( new CodeThisReferenceExpression(), member.Name ), new CodeArgumentReferenceExpression( "value" ) ) ); |
最后,我們只需向該類型中添加新的屬性:
type.Members.Add( prop ); } |
好了,先前的架構通過該工具生成以下代碼:
/// <remarks/> [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)] public class Publisher { /// <remarks/> public string pub_id; |
向該架構添加相應的擴展以后:
<xs:schema elementFormDefault="qualified" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:annotation> <xs:appinfo> <Code xmlns="http://weblogs.asp.net/cazzu"> <Extension Type="XsdGenerator.Extensions.FieldsToPropertiesExtension, XsdGenerator.CustomTool" /> </Code> </xs:appinfo> </xs:annotation> ... |
該架構現在將生成:
/// [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)] public class Publisher { private string _pub_id; /// public string pub_id { get { return this._pub_id; } set { this._pub_id = value; } } |
使用集合而不是數組
對于任何比較像樣的讀寫(具有 get 和 set 屬性)對象模型而言,要使其可供程序員方便地使用,它的多值屬性都應該基于集合,而不是基于數組。這樣做會使修改值和操縱對象圖變得更為容易。通常的方法涉及到從 CollectionBase 派生一個新的類型化集合類。
在將更改提交給 CodeDom 之前,XmlSerializer 支持必須對集合進行檢查。在分析和反射要序列化的類型的類的內部深處,有一個名為 TypeScope 的內部類。TypeScope 負責確保生成序列化代碼。它包含一個有趣的方法,名為 ImportTypeDesc,該方法執行大多數檢查工作并且為支持的類型生成信息。在這里,我們找到了對 IXmlSerializable(它檢查其成員中的安全特性)、數組(必須具有等于 1 的秩)、Enums、XmlNode、XmlAttribute 和 XmlElement 等的特殊支持。
尤其是對集合而言,導入方法檢查實現 ICollection 的類型,該類型必須滿足下列規則:
- 必須具有一個 Add 方法,該方法不是由該接口定義的,因為它通常是為該集合將要容納的專用類型而創建的。
- 不得通過該集合實現 IDictionary。
- 必須具有一個默認成員(即一個索引器)并且該成員具有一個類型為 System.Int32 (C# int) 的參數。系統將在所有類型層次結構中搜索這樣的成員。
- 在 Add、Count 和索引器中不能有任何安全特性。
在驗證上述信息以后,生成的派生自 XmlSerializationWriter 的專用類在為我們的類型編寫 XML 輸出時,將使用 Count 屬性進行迭代,而不使用基于數組的屬性的 Lenth:
MyAssembly.MyCollection a = (MyAssembly.MyCollection)o.@CollectionProperty;if (a != null) { for (int ia = 0; ia < a.Count; ia++) { Write10_MyCollectionItem(@"MyCollectionItem", @"http://weblogs.asp.net/cazzu/", ((MyAssembly.MyCollectionItem)a[ia]), false, false); }}
請注意,在給定對索引器的上一次檢查之后,對集合和數組的索引訪問是相同的,所以此處沒有進行更改。
相應的派生自 XmlSerializationReader 的類使用類型化的 Add 方法來填充集合:
MyAssembly.MyCollection a_2 = (MyAssembly.MyCollection)o.@CollectionProperty; ... while (Reader.NodeType != System.Xml.XmlNodeType.EndElement) { if (Reader.NodeType == System.Xml.XmlNodeType.Element) { if (((object) Reader.LocalName == (object)id8_MyCollectionItem && (object) Reader.NamespaceURI == (object)id9_httpweblogsaspnetcazzu)) { if ((object)(a_2) == null) Reader.Skip(); else a_2.Add(Read10_MyCollectionItem(false, true)); } ... |
上面顯示的讀方法返回集合所期望的適當類型:
MyAssembly.MyCollectionItem Read1_MyCollectionItem(bool isNullable, bool checkType) |
既然已經檢驗了 XmlSerializer 能夠支持和正確處理基于集合的屬性,那么將所有數組更改為相應的強類型集合就是安全的。
可以將這一新的擴展設計為在上一個擴展之前或之后運行。其中的差別是明顯的,因為迭代將分別從字段更改到新的屬性。為了使該擴展獨立于上一個擴展,我將對其進行編碼以針對字段工作。不過,請注意,如果將其配置為在 FieldsToPropertiesExtension“之后”運行,則該代碼將是不正確的。
讓我們首先分析將生成自定義集合的方法。該集合應如下所示:
public class PublisherCollection : CollectionBase { public int Add(Publisher value) { return base.InnerList.Add(value); } public Publisher this[int idx] { get { return (Publisher) base.InnerList[idx]; } set { base.InnerList[idx] = value; } } } |
用于生成該類型化集合的代碼為:
public CodeTypeDeclaration GetCollection( CodeTypeReference forType ) { CodeTypeDeclaration col = new CodeTypeDeclaration( forType.BaseType + "Collection" ); col.BaseTypes.Add(typeof(CollectionBase)); col.Attributes = MemberAttributes.Final | MemberAttributes.Public; // Add method CodeMemberMethod add = new CodeMemberMethod(); add.Attributes = MemberAttributes.Final | MemberAttributes.Public; add.Name = "Add"; add.ReturnType = new CodeTypeReference(typeof(int)); add.Parameters.Add( new CodeParameterDeclarationExpression ( forType, "value" ) ); // Generates: return base.InnerList.Add(value); add.Statements.Add( new CodeMethodReturnStatement ( new CodeMethodInvokeExpression( new CodePropertyReferenceExpression( new CodeBaseReferenceExpression(), "InnerList"), "Add", new CodeExpression[] { new CodeArgumentReferenceExpression( "value" ) } ) ) ); // Add to type. col.Members.Add(add); // Indexer property ('this') CodeMemberProperty indexer = new CodeMemberProperty(); indexer.Attributes = MemberAttributes.Final | MemberAttributes.Public; indexer.Name = "Item"; indexer.Type = forType; indexer.Parameters.Add( new CodeParameterDeclarationExpression ( typeof( int ), "idx" ) ); indexer.HasGet = true; indexer.HasSet = true; // Generates: return (theType) base.InnerList[idx]; indexer.GetStatements.Add( new CodeMethodReturnStatement ( new CodeCastExpression( forType, new CodeIndexerExpression( new CodePropertyReferenceExpression( new CodeBaseReferenceExpression(), "InnerList"), new CodeExpression[] { new CodeArgumentReferenceExpression( "idx" ) } ) ) ) ); // Generates: base.InnerList[idx] = value; indexer.SetStatements.Add( new CodeAssignStatement( new CodeIndexerExpression( new CodePropertyReferenceExpression( new CodeBaseReferenceExpression(), "InnerList"), new CodeExpression[] { new CodeArgumentReferenceExpression("idx") }), new CodeArgumentReferenceExpression( "value" ) ) ); // Add to type. col.Members.Add(indexer); return col; } |
此時,您應該考慮一個在對 CodeDom 進行編程時有用的技巧;看到這些似乎沒完沒了的 Statements.Add 代碼行了嗎?當然,我們可以將它們拆分為多個獨立的行,每行創建一個臨時變量以容納該對象并將其傳遞給下一行。但這樣只會使它們更加無窮無盡!那好,只要您能夠習慣,那么下面的技巧會是一種將這些代碼行拆分為多個部分的好方法:
要生成 CodeDom 嵌套語句,鄰近的屬性/索引器/方法訪問通常是從右向左構建的。
實際上:要生成以下代碼行:
您應該從索引器表達式 [idx] 開始,接著是屬性訪問 InnerList,最后是對象引用基。這將生成下面的 CodeDom 嵌套語句:
CodeExpression st = new CodeIndexerExpression( new CodePropertyReferenceExpression( new CodeBaseReferenceExpression(), "InnerList" ), new CodeExpression[] { new CodeArgumentReferenceExpression( "idx" ) } ); |
請注意,我從右向左創建語句,最后才完成適當的構造函數參數。用這種方式手動縮進和拆分代碼行通常是一個好主意,這樣可以更容易地看到各個對象構造函數在哪里結束以及哪些是它的參數。
最后,ICodeExtension.Process 方法實現涉及到對類型及其字段進行迭代,以查找基于數組的字段:
public class ArraysToCollectionsExtension : ICodeExtension { public void Process( CodeNamespace code, XmlSchema schema ) { // Copy as we will be adding types. CodeTypeDeclaration[] types = new CodeTypeDeclaration[code.Types.Count]; code.Types.CopyTo( types, 0 ); foreach ( CodeTypeDeclaration type in types ) { if ( type.IsClass || type.IsStruct ) { foreach ( CodeTypeMember member in type.Members ) { // Process fields only. if ( member is CodeMemberField && ( ( CodeMemberField )member ).Type.ArrayElementType != null ) { CodeMemberField field = ( CodeMemberField ) member; CodeTypeDeclaration col = GetCollection( field.Type.ArrayElementType ); // Change field type to collection. field.Type = new CodeTypeReference( col.Name ); code.Types.Add( col ); } } } } } |
正像我在前面所做的,我復制了需要修改的集合;在此例中,是 CodeNamespace.Types。
進一步的自定義可以包括:向生成的類中添加 [Serializable],添加 DAL 方法(即 LoadById、FindByKey、Save、Delete 等),生成被序列化操作忽略但由您的代碼使用的成員(應用 XmlIgnoreAttribute),省略屬于外部導入架構的類的生成,等等。
映射技巧
如果您要更深入地研究代碼生成工具本身,或者希望更進一步地自定義架構處理,那么您或許會對下列與 codegen 類有關的高級問題感興趣。如果您只是要開發擴展和操縱 CodeDom,則它們對您不會有多大價值,您可以跳過本部分而不會有任何問題。
我已經通過檢索元素的 XmlTypeMapping 來處理這些元素;我尚未使用其任何屬性,但如果您必須要找到與元素對應的 CodeTypeDeclaration,則可能需要使用這些屬性。有關 XmlTypeMapping 屬性及其含義的簡短說明,請參閱 MSDN 文檔。但是,該類用在許多方案中,如該文檔中所示的 SoapReflectionImporter 映射導入。至于我所使用的 XmlSchemaImporter,我已經發現 XmlTypeMapping.TypeFullName 和 XmlTypeMapping.TypeName 對一個特定架構元素的設計具有不正確的行為:如果該元素在某個序列內部包含單個未綁定的子元素,則兩者都將錯誤地假定子屬性的類型。
因此,對于以下架構元素:
<xs:element name="pubs"> <xs:complexType> <xs:sequence> <xs:element name="publishers" type="Publisher" maxOccurs="unbounded" /> </xs:sequence> </xs:complexType> </xs:element> |
XmlTypeMapping.TypeFullName 和 XmlTypeMapping.TypeName 都沒有“pubs”值(這是將要生成的類型),而是具有值“Publisher[]”,這是其唯一屬性的類型。如果該序列具有一個以上的元素,則一切都可以按預期方式工作。請注意,無論元素的類型是否為命名的全局類型,或者無論該元素本身是否為引用,這一(明顯的)錯誤都適用。
除了類型映射以外,XmlSchemaImporter 還可以檢索將應用于其成員(字段)的映射。這很有用,因為 XSD/CLR 類型映射(包括 XSD 自定義派生類型)將被解析,并且您可以確信它就是由 XmlSerializer 使用的那個映射。您可以按如下方式獲得成員映射:
XmlMembersMapping mmap = importer.ImportMembersMapping( element.QualifiedName ); int count = mmap.Count; for (int i = 0; i < count; i++) { XmlMemberMapping map = mmap[i]; //You have now: // map.ElementName // map.MemberName // map.TypeFullName // map.TypeName }
|
XmlMemberMapping.TypeFullName 容納命名空間限定的 CLR 類型,盡管 XmlMemberMapping.TypeName 具有 XSD 類型名。例如,對于 XSD 類型“xs:positiveInteger”的成員,前者將是“System.String”,而后者將是“positiveInteger”。如果您沒有訪問該成員映射檢索的權限,則必須知道 XmlSerializer 所使用的所有 XSD 到 CLR 類型轉換規則。請注意,這些規則不必與用于 XSD 驗證和 DOM PSVI 的規則相同。
對于成員導入,有一個重要的警告(同樣,明顯是一個錯誤)。您不能重用 XmlSchemaImporter,否則將得到由導入代碼在 XmlMembersMapping 構建時引發的 InvalidCastException。這可以通過每次使用導入程序的新實例來加以解決。
有了這些信息,您可以徹底更改類的外觀,例如,重命名屬性以使首字母變成大寫,而不會對序列化基礎結構產生危害。
當我討論 codegen 類的基本原理時,我說過您只能為全局定義的元素檢索(導入)映射;如果您創建自己的自定義特性以修改得到的類,則將只能夠針對頂級元素檢索和分析它們,因為您將只具有這些元素的映射。例如,假設您添加了一個 code:className 特性,該特性被某個擴展用來更改生成的類名:
<xs:schema xmlns:code="http://weblogs.asp.net/cazzu" ...> <xs:element name="pubs" code:className="PublishersData"> <xs:complexType> <xs:sequence> <xs:element name="publishers" code:className="Publishers"> <xs:complexType> |
您將能夠為 pubs 元素檢索這些映射,但無法為 publishers 子元素檢索這些映射。因此,對其進行處理將是不安全的,因為 codegen 類將來可能發生更改。如果不能控制映射,您就不能簡單地假設相應的 CodeTypeDeclaration 將具有與該元素相同的名稱(以便找到和更改它)。當然,您可以自行決定是否可以接受這種危險。
小結
通過重用為 XmlSerializer 創建的內置代碼生成功能,可以確保對所生成的代碼進行少量更改不會破壞 XML 序列化。通過 CodeDom 直接操縱其輸出也可以增加靈活性。我在本文中說明了如何通過靈活處理 XML 架構來實現任意擴展以改變輸出,并且開發了一些有用的示例。
在奠定這一牢固的基礎之后,您可以繼續研究更高級的方案,例如:外部(導入的/包含的)XSD 架構及其與代碼生成的關系,操縱代碼輸出以重新使用應用程序或企業范圍的儲備庫中的類型定義(包括 XSD 和相應的生成 .NET 類型),等等。
希望本文能夠推動您使用新穎的方法來進行 XSD 存儲和管理,以及相應的代碼生成和重用。
|