从一个范例看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); }

这段代码需要注意这样几点:

  1. 由于习惯问题,我给页面拖的是Asp.Net服务器控件,实际上,这里使用纯粹的Html Input标记就可以了,代码会更清爽一些,但是因为已经写好了,我偷懒了一下就没有改过去>_<、(但是使用服务器控件会有一个额外好处,就是可以使用验证控件,但是这里出于演示目的,我没有添加验证控件)。
  2. 这里的context可以用来传递任何数据,这个值可以从调用成功或失败的回调方法中获得。
  3. 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&amp;t=633432946018437500" type="text/javascript"></script>

将其中的/WebSite/WebResource.axd?d=gTLcCoR1D13V4dcBYSU_JA2&amp;t=633432946018437500 复制到浏览器的合适位置,然后会下载到一个WebResource.axd文件,用文本编辑器打开这个文件,可以看到许多的javascript代码,其中就包括WebForm_DoCallback()方法,这些便是由Microsoft所实现的方法回调的底层代码了。

效果预览

现在,我们可以打开页面浏览一下效果了,我们先输入一个不正确的日期格式,然后点击搜索,会看到下面的结果:

然后我们将日期修改正确,再次进行输入,可以看到下面的结果:

总结

这篇文章为大家演示了一个XML的综合应用:使用字符串传递自定义数值、使用XML模式验证XML的有效性、使用XSLT将XML转换为XHTML标记,以及使用Asp.Net的脚本回调功能实现Ajax的效果。

通过这篇文章,可以看到XML的广泛应用,但是也发现了实现这样一个简单的功能却需要做如此繁杂的工作。所以,我个人觉得如果想要一些更巧妙的设计、更优良的性能,那么可以采用这样的方式。但是如果要追求更高的开发效率,我想一个UpdatePanel再加一个GridView就足以完成上面的功能了吧。

感谢阅读,希望这篇文章能给你带来帮助!