從一個範例看XML的應用
引言
如果你已經看了
Asp.Net Ajax的兩種基本開發模式這篇文章,你可能很快會發現這樣一個問題:在那篇文章的方式2中,用戶端僅僅是發送了頁面上
一個文本框的内容到服務端,而服務端的Web服務方法也隻接收
來自用戶端的字元串類型的數值。
而很多時候,服務端的方法期望接收的是一個自定義類型,或者是多個不同類型的參數。為了能夠處理這種
由一個字元串包含多種不同類型值情況,我們可以采用XML。
這篇文章将建構一個簡單的圖書查詢頁面,通過這個程式,我們将會看到XML、XSD模式驗證、XSLT樣式轉換,以及Asp.Net腳本回調功能的一個綜合應用。
資料庫建立和資料通路
我們先看一下這個Web頁面實作的功能:頁面提供一些文本框供使用者輸入,包括書名、出版社、作者等資訊,然後将這些資訊發往伺服器,伺服器對資料庫進行查詢,然後傳回查詢結果。如果是通常的Asp.Net開發,完成這樣的功能是很基本的要求,根本用不着我花時間寫這些文字,但這裡我們希望實作Ajax方式的效果,是以就需要解決引言中提出的問題。
如果你看過我的文章,那麼應該知道我喜歡循序漸進的寫作方式,這篇也是一樣,我們先從資料庫建立開始。由于資料庫和資料通路并不是本文的重點,是以我隻簡單地描述一下步驟。在本地SQL Server或者直接在App_Data下建立一個資料庫,起名叫SiteDB,然後建一個表Book,字段的設定如下:

随後填充一些範例資料,如果你想節約點時間,那麼可以直接下載下傳本文所附帶的代碼,在App_Data檔案夾下包含有SiteDB資料庫。
接下來我們在App_Code檔案夾下添加一個SiteBLL.cs檔案,本文用到的所有代碼邏輯都包含在了SiteBLL類中,這麼做顯然是不妥的,但這裡我們主要關注的是XML的應用,而非構架與設計,是以暫且就這個樣子好了。很容易就能想到,我們要添加的第一個方法,會擁有下面這樣的簽名,它根據方法的參數查詢資料庫,然後以DataSet的形式傳回結果:
private static DataSet SearchBook
(string name, string author, string publisher, DateTime pubDate, decimal price)
如果要建構一個實際的查詢,那麼需要很大量的資料才能保證幾乎每次搜尋都能夠獲得到資料來提供示範,而實際上我們隻添加了5條範例資料,是以讓我們幹脆将它們全部傳回,而忽略這裡的參數,但在實際當中,當然是根據這些參數來獲得實際的傳回資料:
private static DataSet SearchBook(string name, string author,
string publisher, DateTime pubDate, decimal price)
{
string connString =
WebConfigurationManager.ConnectionStrings["SiteDBConnection"].ConnectionString;
string provider =
WebConfigurationManager.ConnectionStrings["SiteDBConnection"].ProviderName;
DbProviderFactory factory = DbProviderFactories.GetFactory(provider);
DbConnection conn = factory.CreateConnection();
conn.ConnectionString = connString;
DbDataAdapter adapter = factory.CreateDataAdapter();
DbCommand selectCmd = conn.CreateCommand();
selectCmd.CommandText = "Select * From Book";
adapter.SelectCommand = selectCmd;
DataSet ds = new DataSet("BookStore");
adapter.Fill(ds, "Book");
return ds;
}
這段代碼沒有什麼好解釋的,唯一值得注意的可能是我完全采用了面向接口(基類)的方式編寫資料通路代碼,這樣将來如果更換為Oracle或者其他任何資料庫,這裡不需要更改一行代碼,隻需要修改下Web.Config就可以了。
XML應用 -- 單一字元串包含多種不同類型值
接下來我們對頁面進行一下布局,如下所示:
控件的命名是自解釋的,是以下面看代碼應該不會遇到障礙,這裡我就不再贅述了。需要注意的是頁面上含有一個空的div标記,它用來承載我們的查詢結果:
<div id="output"></div>
另外,“搜尋”按鈕是純粹的HTML标記,不含有runat="server"屬性,輕按兩下它,會在頁面生成下面的javascript腳本段:
function btnSearch_onclick() {
// ...
接下來我們要做的就是實作這個js方法,它的任務就是将文本框中輸入的内容發往伺服器。此時我們遇到了文章開頭提出的問題,伺服器期望的是5個參數,而且有字元串、數字、日期三種類型,而在用戶端,我們隻有一種類型 -- 字元串。因為javascript和C#顯然用得不是一個類型系統,它們完全是兩個領域。同時我們隻發送一個參數,但要包含所有5個數值。對于現在以及和現在類似的情形,我将它統稱為
單一字元串包含多種不同類型的數值的情況,為了便于服務端(更寬泛點,叫程式)的處理,我們可以定義自己的XML。此處,我定義它的格式為:
<userInput>
<name>書名</name>
<author>作者</author>
<publisher>出版社</publisher>
<pubDate>出版日期</pubDate>
<price>價格</price>
</userInput>
有了這個格式定義,實作btnSearch_onclick()就非常的容易了:
var name = <%="document.getElementById(\"" + txtName.ClientID + "\").value" %>;
var author = <%="document.getElementById(\"" + txtAuthor.ClientID + "\").value" %>;
var publisher = <%="document.getElementById(\"" + txtPublisher.ClientID + "\").value" %>;
var pubDate = <%="document.getElementById(\"" + txtPubDate.ClientID + "\").value" %>;
var price = <%="document.getElementById(\"" + txtPrice.ClientID + "\").value" %>;
var inputXml = "<userInput>" +
"<name>" + name + "</name>" +
"<author>" + author + "</author>" +
"<publisher>" + publisher + "</publisher>" +
"<pubDate>" + pubDate + "</pubDate>" +
"<price>" + price + "</price>" +
"</userInput>";
var context = "Any data you want to pass !";
ClientSearchBook(inputXml, context);
這段代碼需要注意這樣幾點:
- 由于習慣問題,我給頁面拖的是Asp.Net伺服器控件,實際上,這裡使用純粹的Html Input标記就可以了,代碼會更清爽一些,但是因為已經寫好了,我偷懶了一下就沒有改過去>_<、(但是使用伺服器控件會有一個額外好處,就是可以使用驗證控件,但是這裡出于示範目的,我沒有添加驗證控件)。
- 這裡的context和 後面介紹的context作用一樣,可以用來傳遞任何資料,這個值可以從調用成功或失敗的回調方法中獲得。
- ClientSearchBook()方法并沒有實作,因為這篇文章我打算采用Asp.Net的腳本回調來實作,而不是用已經介紹過的Ajax Extension配合Web Service來實作,是以這個方法最後是由服務端生成的,這在後面會介紹到。現在隻需知道它将inputXml發往服務端就可以了。
再次與
中介紹的第二種方式類似,我們實作onCompleted()和onFailed()這兩個回調方法,它們将會在服務端生成的腳本代碼中進行注冊(後面會看到),當調用成功時調用onCompleted(),調用失敗時調用onFailed()(這裡我沒有再示範context的使用了):
function onCompleted(result, context){
output.innerHTML = result;
function onFailed(error, context){
output.innerHTML = "Search Failed : " + error;
方法的實作隻不過是将傳回結果或者錯誤資訊顯示在頁面的div标記中。
XML模式 -- 使用XSD校驗用戶端資料
我曾經聽過這樣一句Web程式設計的“諺語”――永遠不要相信用戶端發來的資料。意思就是說即便你添加了用戶端的表單驗證,仍然要在服務端對用戶端發來的資料進行驗證。在本文的例子中,我們接收的是一個XML字元串,那麼如何對它進行驗證呢?我們可以使用XML模式(XML Schema)來對它進行驗證,XML模式檔案的字尾名為xsd。對于XSD有這樣一個很好的類比:就拿資料庫的表定義來說,如果你定義的XML是表的列名,那麼XSD就規定了列的類型(int還是bit,或者varchar)。
手工編寫XML模式會很精細,但對于複雜的XML文檔來說是很費力氣的。在VS2008中,有一個内置功能,可以由XML文檔推斷出它的模式,盡管推斷出的模式往往不夠精準,但我們可以對推斷出的模式進行一些修改,在大多數情況下就可以得到我們想要的模式。具體的做法是:建立一個符合預期輸入的XML檔案,用VS2008打開這個檔案,然後在菜單欄選擇“XML”-->“Create Schema”,再對這個生成的模式進行修改,最後儲存在站點目錄下,這裡我将它儲存為了userInputSchema.xsd:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="userInput">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string" />
<xs:element name="author" type="xs:string" />
<xs:element name="publisher" type="xs:string" />
<xs:element name="pubDate" type="xs:date" />
<xs:element name="price" type="xs:decimal" />
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
詳細介紹XML模式需要花費很多的時間,是以這裡我們隻要知道它限制了name、author、publisher、pubDate、price這5個XML元素可以包含的資料類型就可以了。接下來我們就可以編寫一個方法,針對XML檔案進行驗證了,在SiteBLL下再添加一個ValidateXmlSchema()方法:
private static bool ValidateXmlSchema(string xmlString, string xsdPath) {
TextReader reader = new StringReader(xmlString);
XmlReaderSettings settings = new XmlReaderSettings();
settings.ValidationType = ValidationType.Schema;
settings.Schemas.Add(null, xsdPath);
XmlReader xmlReader = XmlReader.Create(reader, settings);
try {
while (xmlReader.Read()) { }
} catch {
return false;
}
return true;
這個方法的第一個參數是一個xml字元串,此處也就是用戶端發來的資料;第二個參數是XML模式的檔案路徑。在方法内部使用了一個XmlReader周遊了Xml文檔,由于對XmlReader設定了模式,是以在周遊時會對每一個節點進行驗證,當發現不符合模式要求的節點值時便會抛出異常,如果我們捕獲到異常,就傳回false。上面有一個很常見的應用這裡順便說一下,可以注冊XmlReaderSettings對象的ValidationEventHandler事件,注冊這個事件後發現不符合模式的節點時可以交給事件處理程式處理,而不會抛出異常。這個事件的參數包含了錯誤的詳細資訊,例如哪個節點的驗證失敗,還可以區分是一個“警告”還是一個“錯誤”。
XSLT樣式表 -- 從XML 到 XHTML
OK,處理用戶端的處理現在已經告一段落了,讓我們再次看一看服務端SearchBook()方法的簽名:
我們看到它傳回的是一個DataSet,而在用戶端,我們期望接收的是一個字元串,雖然我們可以在服務端周遊DataSet中的表,然後對其字段值進行處理,比如嵌入一些HTML代碼,然後将處理好的HTML代碼傳回。但是有一種更加“fashion”的做法,就是使用XSLT進行轉換。為了進行轉換,我們首先要獲得DataSet的XML形式的表現,這可以友善地通過在DataSet對象上調用GetXml()方法來獲得。随後,我們需要以程式設計的方式對這個XML進行XSLT轉換,将其轉換為預期的XHTML。
開始之前,我們需要知道我們在DataSet上調用GetXml()方法獲得的結果,因為我們将DataSet命名為了BookStore,将表命名為了Book,是以XML應該為類似下面的形式:
<BookStore>
<Book>
<Id>1</Id>
<Name>SQL Server 2005寶典</Name>
<Author>Paul Nielsen</Author>
<Publisher>人民郵電出版社</Publisher>
<PubDate>2006-10-01T00:00:00+08:00</PubDate>
<Price>65.50</Price>
</Book>
...
</BookStore>
接下來我們要編寫一個XSLT樣式表檔案,對類似上面的資料進行轉換,将它們轉成标準的表格:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
exclude-result-prefixes="msxsl">
<xsl:output method="html" indent="yes"/>
<xsl:template match="/">
<table class="mainTable">
<tr style="background:#f5f5f5;">
<th style="width:20%;">書名</th>
<th style="width:20%;">作者</th>
<th style="width:20%;">出版社</th>
<th style="width:20%;">出版日期</th>
<th style="width:20%;">定價</th>
</tr>
<xsl:for-each select="/BookStore/Book">
<xsl:element name="tr">
<xsl:element name="td">
<xsl:value-of select="Name" />
</xsl:element>
<xsl:value-of select="Author" />
<xsl:value-of select="Publisher" />
<xsl:value-of
select="msxsl:format-date(PubDate, 'yyyy-M-dd')" />
<xsl:value-of select="Price" />
</xsl:element>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
與XML模式類似,解釋XSLT需要很多的篇幅,本文不打算詳細對它進行解釋。現在隻要知道它可以将一個原始XML轉換成各種格式的目标文檔,其中之一是XHTML就可了。上面的XSLT将DataSet輸出的XML轉換成了一個HTML的Table标記。
有了這個XSLT樣式表,接下來我們就可以在SiteBLL中再添加一個方法:
// 使用XSLT将XML轉換為XHTML
private static string ConvertToXhtml(string xml, string xslPath) {
XmlDocument doc = new XmlDocument();
doc.LoadXml(xml);
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(xslPath);
TextWriter writer = new StringWriter();
transform.Transform(doc, null, writer);
return writer.ToString();
ConvertToXhtml()隻是進行XSLT轉換的一個最簡單的代碼,但足以滿足本文中我們的需求。實際上,我們在進行XSLT轉換的時候,還可以向XSLT樣式表傳遞伺服器端的對象和參數,以後有時間再為大家介紹。
SearchBook()重載方法
我們很快又回到了曾在
中讨論過的基本模式,服務端接受一個字元串類型,傳回一個字元串類型。隻不過這次接受的字元串類型為XML格式,而傳回的是經過XSLT格式化成XHTML的DataSet。為了便于使用,我們将所有的從XML中獲得值、XML 模式驗證、XSLT轉換包裝在一個SearchBook()的重載方法中:
public static string SearchBook(string xmlString, string xsdPath, string xslPath) {
if (ValidateXmlSchema(xmlString, xsdPath)) {
doc.LoadXml(xmlString);
XmlNode root = doc.DocumentElement;
string name = root.SelectSingleNode("name").InnerText;
string author = root.SelectSingleNode("author").InnerText;
string publisher = root.SelectSingleNode("publisher").InnerText;
DateTime pubDate =
Convert.ToDateTime(root.SelectSingleNode("pubDate").InnerText);
decimal price =
Convert.ToDecimal(root.SelectSingleNode("price").InnerText);
string xml = SearchBook(name, author, publisher, pubDate, price).GetXml();
string xhtml = ConvertToXhtml(xml, xslPath);
return xhtml;
return "Your input is invalid !";
這段代碼非常簡單,沒有什麼特别之處。需要注意的是:當模式驗證失敗的時候,傳回的是一個字元串“Your input is invalid !”。這裡的資訊顯然太少了,如同我在上面所說,你可以在驗證時,注冊XmlReaderSettings對象的ValidationEventHandler事件,然後在事件的處理方法中獲得更詳細的資訊(哪個節點驗證失敗了,什麼原因)。
啟用Asp.Net腳本回調
我們終于又回到了頁面的設定當中,但這次不是布置頁面控件,而是啟用Asp.Net的腳本回調功能。我們要做的第一步,就是讓Web頁面實作ICallbackEventHandler接口,它的實作如下:
private string userInput;
void ICallbackEventHandler.RaiseCallbackEvent(string eventArgument) {
userInput = eventArgument;
string ICallbackEventHandler.GetCallbackResult() {
string xsdPath = Server.MapPath("userInputSchema.xsd");
string xslPath = Server.MapPath("userInputXsl.xslt");
return SiteBLL.SearchBook(userInput, xsdPath, xslPath);
RaiseCallBackEvent()方法接收一個eventArgument字元串,這個字元串即為用戶端發往服務端的值,也就是我們在btnSearch_onclick()建構的inputXml字元串,我們将它儲存在一個私有變量中。GetCallbackResult()方法使用這個私有變量,并調用了我們上一小節建立的SearchBook()方法,傳回了XHTML字元串。
至此,還有一個問題沒有解決:我們沒有将用戶端onComplted()和onFailed()與Asp.Net的腳本回調關聯起來,除此以外,應該記得在btnSearch_onclick()方法中調用了一個“奇怪”的用戶端javascript方法ClientSearchBook(),而它卻并沒有在頁面中實作。實際上,這個方法是自動生成的,現在改寫頁面的Page_Load()方法:
protected void Page_Load(object sender, EventArgs e) {
if (!Request.Browser.SupportsCallback)
throw new ApplicationException("Browser doesn't support callbacks !");
string methodBody = Page.ClientScript.GetCallbackEventReference
(this, "arg", "onCompleted", "context", "onFailed", false);
string method = @"function ClientSearchBook(arg, context){" + methodBody + ";}";
Page.ClientScript.RegisterClientScriptBlock
(this.GetType(), " ClientSearchBook", method, true);
GetCallbackEventReference()方法關聯了用戶端的onCompleted和onFailed方法,分别用于成功和失敗時的回調。它的第一個參數是實作了ICallbackEventHandler的控件,此處就是目前的Page頁面了;第二個參數是用戶端發往服務端的資料;第三個參數是方法成功時的回調方法;第四個參數是我們的老熟人context,它被用于回調的onComplted()和onFailed()方法中;第五個參數是方法失敗時的回調方法;最後一個說明是否異步調用。
GetCallbackEventReference()方法傳回了一段javascript腳本,這段腳本隻是一個javascript方法的方法體。 是以,我們接着建構了一個包含完整方法的字元串。最後我們将這個方法注冊到了頁面上。是以當你打開頁面時,會發現頁面中已經生成了btnSearch_onclick()中所調用的這個ClientSearchBook()。
<script type="text/javascript">
//<![CDATA[
function ClientSearchBook(arg, context){
WebForm_DoCallback('__Page',arg,onCompleted,context,onFailed,false);
//]]>
</script>
如果你對這段代碼中的WebForm_DoCallback()方法感到奇怪,不知道它位于何處,那麼你可以找到這段代碼:
<script src="/WebSite/WebResource.axd?d=gTLcCoR1D13V4dcBYSU_JA2&t=633432946018437500" type="text/javascript"></script>
将其中的/WebSite/WebResource.axd?d=gTLcCoR1D13V4dcBYSU_JA2&t=633432946018437500 複制到浏覽器的合适位置,然後會下載下傳到一個WebResource.axd檔案,用文本編輯器打開這個檔案,可以看到許多的javascript代碼,其中就包括WebForm_DoCallback()方法,這些便是由Microsoft所實作的方法回調的底層代碼了。
效果預覽
現在,我們可以打開頁面浏覽一下效果了,我們先輸入一個不正确的日期格式,然後點選搜尋,會看到下面的結果:
然後我們将日期修改正确,再次進行輸入,可以看到下面的結果:
總結
這篇文章為大家示範了一個XML的綜合應用:使用字元串傳遞自定義數值、使用XML模式驗證XML的有效性、使用XSLT将XML轉換為XHTML标記,以及使用Asp.Net的腳本回調功能實作Ajax的效果。
通過這篇文章,可以看到XML的廣泛應用,但是也發現了實作這樣一個簡單的功能卻需要做如此繁雜的工作。是以,我個人覺得如果想要一些更巧妙的設計、更優良的性能,那麼可以采用這樣的方式。但是如果要追求更高的開發效率,我想一個UpdatePanel再加一個GridView就足以完成上面的功能了吧。
感謝閱讀,希望這篇文章能給你帶來幫助!