前陣子在寫LINQDouban的時候碰到關於XML序列化的場景通過Douban api添加和更新數據的時候都需要Post一個xml entry如
添加活動(xml中用%%括起來的部分是需要填寫的部分為了精簡刪除了xmlns)
view sourceprint? <?xml version= encoding=UTF?>
<entry>
<title>%title%</title>
<category scheme= term=;
<content>%Content%</content>
<db:attribute name=invite_only>%IsInviteOnly%</db:attribute>
<db:attribute name=can_invite>%CanInvite%</db:attribute>
<gd:when endTime=%DurationEnd% startTime=%DurationStart%/>
<gd:where valueString=%Where%/>
</entry>
一下子能想到的方法有兩個——通過XmlSerializer或在entity class上實現IXmlSerializable接口來實現但很快發現有幾個問題沒法解決
XmlSerializer不支持泛型集合的序列化而我在定義entity class時用了不少IList和IDictionary如db:attribute我就定義成IDictionary
XmlSerializer只能生成完整的element和attribute像上面那段xml裡<category>節點term屬性裡變化的只有後面%Category%部分這就沒法生成了
存在添加和更新的post內容不一樣的情況這就意味著同一個entity class存在多種序列化方案
douban的xml entry格式可能會更改而我不希望因此而更改代碼
想來想去最好的方法是通過XML模板+反射(簡稱XMl模板替換)來生成了就像上面的xml etnry裡面%%括起來的部分替換掉就可以了這樣可以解決上述的四個問題除了提供和XMLSerializer功能相同的序列化之外XML模板替換還要滿足下面這些要求
可以序列化實現IEnumerable的集合這是最常用的集合當然大多數的泛型集合也是應用了IEnumerable的
提供更靈活的替換XmlSerializer實現的序列化順序是A(B(c)B)A對於子對象的序列化只能是嵌套的模式而XML模板替換可以實現任何層次的替換
為每種類型的對象提供通用的序列化方式不需要任何Attribute定義不需要修改對象的定義對於給定的object和XML模板通過發射獲取屬性值進行XML替換後生成XML內容對於相同的object提供不同的XML模板就能生成不同的XML
通過修改XML模板即可修改序列化結果
下面給出一個修改過的RSS的XML模板這次CodeFun的目的是在最後實現這個模板的替換並且完成一個能夠實現上述功能的Helper class
特別的地方
<category>節點通過可訪問子對象的屬性如果你希望獲取Domain的長度可以寫成%CategoryDomainLength%
<noReplacement>節點該節點不包含任何替換信息當進行替換處理時應當忽略
<skipHours>節點SkipHours是一個List<int>集合我們希望能夠根據SkipHours的值展開多個<hour>節點
<as:scope>節點<scope>是模板定義聲明<scope>節點內包含的子節點在ChannelItems對象的作用域中所有%%(不包括%/CategoryName%)的屬性都是對Items對象的屬性訪問由於此處Items對象是List<RssItem>集合所以將循環生成多個<item>Scope的含義類似於程序域支持多個scope的嵌套Scope定義不會出現在最後生成的xml中
<channelCategory>節點<channelCategory>節點在Items的作用域中但我們可以通過/訪問外部scope的屬性類似dos文件路徑如果要訪問上上級scope則是//%/CategoryName%表示訪問Channel對象的Category屬性的Name屬性
view sourceprint? <channel>
<title>%Title%</title>
<link>%Link%</link>
<category domain=%CategoryDomain%>%CategoryName%</category>
<noRelacement>不需要替換</noRelacement>
<skipHours>
<hour>%SkinHours%</hour>
</skipHours>
<as:scope xmlns:as= name=Items type=AllSharingXmlRssRssItem>
<item>
<title>%Title%</title>
<link>%Link%</link>
<description>%Description%</description>
<channelCategory>%/CategoryName%</channelCategory>
</item>
</as:scope>
</channel>
view sourceprint?
相關class定義
view sourceprint? public class RssChannel
{
public string Title { get; set; }
public string Link { get; set; }
public RssCategory Category { get; set; }
public IList<int> SkinHours { get; set; }
public IList<RssItem> Items { get; set; }
}
public class RssItem
{
public string Title { get; set; }
public string Link { get; set; }
}
public class RssCategory
{
public string Domain { get; set; }
public string Name { get; set; }
}
下面將一步步討論如何實現XML模板的替換會給出部分代碼或偽代碼完整的代碼在文章最後會給出下載
一分析XML模板
XML模板替換最終要是要回歸到用正則表達式替換掉所有的%%但由於我們要處理的模板包括域子屬性訪問循環的信息所以這不是僅僅的RegexReplace就可以搞定的分析XML模板就是要遍歷XML模板生成一個Scope樹上面的XML模板可以生成下面的Scope樹
XML模板中包含了的三種我們需要處理的元素
包含%%的XML Attribute
包含%%的XML Element以及Element的子節點
Scope節點
從上面的Scope樹可以看出像<noReplace>這樣不需要替換的XML element或XML attribute被當作常量沒有包括在Scope樹中
在Helper Class中分析Scope樹的方法
view sourceprint? var scope = XmlTemplateScopeCompile(xmlPath entityType)
xmlPath是模板文件的路徑entityType是Scope樹用於分析的對象類型
二對給定的object生成XML
這裡用了LINQXML首先用XDocumentLoad(xmlPath)的到XML模板文件的XDocument然後根據Scope樹對XDocument上的節點進行屬性值替換節點Value替換增加節點(Repeat的節點)幸運的是XDocument比XmlDocument方便太多了實現起來非常快
在Helper Class中生成XML的方法
view sourceprint? var template = new XmlTemplate(xmlPath scope);
ConsoleWriteLine(templateTranslate(entityObj));
template是線程安全的根據需要你可以Cache起來不用每次都生成Scope樹這樣或許會減少部分性能消耗(未測試)
完整的代碼如下(包含在Demo中)
view sourceprint? var channel = new RssChannel();
channelTitle = this is channel title;
channelLink = ;
channelDescription = this is channel description;
channelCategory = new RssCategory();
channelCategoryDomain = ;
channelCategoryName = this is channel category;
channelSkipHoursAdd();
channelSkipHoursAdd();
channelSkipHoursAdd();
channelItemsAdd(new RssItem { Title=Item Link=Link Description=Des });
channelItemsAdd(new RssItem { Title = Item Link = Link Description = Des });
channelItemsAdd(new RssItem { Title = Item Link = Link Description = Des });
var path = PathCombine(AppDomainCurrentDomainBaseDirectory rssxml);
var template = new XmlTemplate(
path XmlTemplateScopeCompile(path typeof(RssChannel)));
templateTranslate(channel)Dump();
生成XML如下
view sourceprint? <channel>
<title>this is channel title</title>
<link>;/link>
<category domain=this>>this is channel category</cate
gory>
<noRelacement>不需要替換</noRelacement>
<skipHours>
<hour></hour>
<hour></hour>
<hour></hour>
</skipHours>
<item>
<title>Item</title>
<link>Link</link>
<description>Des</description>
<channelCategory>this is channel category</channelCategory>
</item>
<item>
<title>Item</title>
<link>Link</link>
<description>Des</description>
<channelCategory>this is channel category</channelCategory>
</item>
<item>
<title>Item</title>
<link>Link</link>
<description>Des</description>
<channelCategory>this is channel category</channelCategory>
</item>
</channel>
總結本文給出了另外一種XML序列化的方法通過XmlTemplate你可以更簡單更靈活的生成XML文檔這在生成RSS Atom等文檔以及開發WebAPI的client的時候都是非常方便的另外本文給出的方法同樣適用於基於XML模板生成Html文件
From:http://tw.wingwit.com/Article/program/net/201311/12717.html