從一個範例看XML的應用
2008-12-7 作者: 張子陽 分類: Asp.Net
引言
如果你已經看了《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()就非常的容易了:
function 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可以用來傳遞任何數據,這個值可以從調用成功或失敗的回調方法中獲得。
- 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()方法的簽名:
private static DataSet SearchBook (string name, string author, string publisher, DateTime pubDate, decimal price)
我們看到它返回的是一個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> <Book> ... </Book> </BookStore>
接下來我們要編寫一個XSLT樣式表文件,對類似上面的數據進行轉換,將它們轉成標準的表格:
<?xml version="1.0" encoding="utf-8"?> <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:element name="td"> <xsl:value-of select="Author" /> </xsl:element> <xsl:element name="td"> <xsl:value-of select="Publisher" /> </xsl:element> <xsl:element name="td"> <xsl:value-of select="msxsl:format-date(PubDate, 'yyyy-M-dd')" /> </xsl:element> <xsl:element name="td"> <xsl:value-of select="Price" /> </xsl:element> </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) { XmlDocument doc = new XmlDocument(); 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就足以完成上面的功能了吧。
感謝閱讀,希望這篇文章能給你帶來幫助!