[摘要]作者:青苹果工作室编译  很长一段时间以来,Visual Basic程序员都是在编写客户机/服务器程序,这些程序工作在私有网络上。但是这种在一台PC机上安装一个独立应用程序的时代正在很快地结束,客户...
作者:青苹果工作室编译
  很长一段时间以来,Visual Basic程序员都是在编写客户机/服务器程序,这些程序工作在私有网络上。但是这种在一台PC机上安装一个独立应用程序的时代正在很快地结束,客户机/服务器结构不再能满足需要,现在的大多数商用程序需要共享数据。 
为什么要建立基于Intranet和Internet的应用程序 
  编写基于Intranet和Internet的应用程序或修改C/S结构的程序为工作于Intranet和Internet上的程序的原因至少有以下几点: 
  首先,每天都在增加的远程雇员需要访问公司的数据。 
  其次,通过集中应用程序里的数据能监控对它们的访问和使用。 
  第三,使用本文谈及的技术,在应用程序启动时从一个中心位置查找全局设置信息,就能方便地维护和更新这些设置,有助于最大程度地降低对桌面应用程序的更新。 
  第四,通过Web服务器而不是从远程客户端来进行数据库操作,可以避免通过网络传递数据库的登录口令。 
  最后,如果使用Internet Explorer,通过后台查询数据,就能避免在修改部分内容时重画整个页面。 
右图是在Internet Explorer中看到的一个应用程序实例。图中,应用程序显示了Northwind数据库中的信息。左边是客户名列表和两个链接:购买历史情况和最近的购买情况。当用户点击链接时,数据在右边的Details栏里显示出来。这个应用程序使用DHTML显示Details中的数据而不用刷新整个页面,而且没有使用框架。 
实现思路 
  创建基于HTTP的VB应用程序的关键是XML和XMLHTTPRequest对象,XMLHTTPRequest对象是Microsoft的XML语法分析器 (msxml.dll)的一部分,它让你能通过HTTP向远端服务器发送GET和POST请求。运行在远端服务器上的程序(我们将要使用的是 ASP页面,但应用程序可以使用任何服务器端脚本机制)接受请求、解释内容,并向调用它的应用程序返回数据或错误消息。你可能会觉得这和对SOAP的描述差不多,实际上也是如此,但这里我们不使用SOAP,因为它会使代码变得复杂。无论如何,重要的是:要理解这里所用的技术背后的思想,它和SOAP的思想是一样的,但不像SOAP那样复杂。 
  将不停改变的客户端应用程序完全隔离起来是不可能的,但通过从中心服务器而不是从本地INI文件或Windows注册表中来加载应用程序设置,就能在很高程度上建立应用程序的独立性。例如,假定有一个流动的销售团队,他们需要访问集中管理的数据以便进行有效的电话推销。过去,这些数据是集中采集的,每天通过电子邮件提供给销售人员。然而,市场的压力和快速变化的销售情况迫使销售人员必须能访问需要的最新数据。不幸的是,IT经理们坚决拒绝允许远程用户访问数据库服务器,因为他们不希望通过公用的Internet发送用户名和口令。不过不要担心,我们将采取方法使大家都满意。 
  在标准的客户机/服务器程序中,你可能需要在应用程序启动时初始化数据库连接串,这意味着客户机必须能访问连接串信息,包括用户名和口令。但是,当不允许通过网络传送那些信息时,就需要在客户端和数据库之间不建立直接连接的情况下获取数据。 
  解决办法是在服务器上建立一个ASP页面,在示例代码中它的名字是getData.asp,它接受特定形式的POST数据,它等待一个 XML字符串,这个字符串里包含着建立ADO Command对象并运行存储过程或动态SQL语句所需的命令信息。如果信息充分,getData.asp执行存储过程并返回一个XML字符串,这个字符串里包含以XML格式表述的Recordset、一系列返回值或一个错误消息。对于返回数据的命令,客户端或者将返回的Recordset对象重新表述,或者使用XML文档对象模型(DOM)来查找返回值或错误消息。 
ASP代码和所需接收的数据 
  getData.asp页面的动作有特定的次序。首先,它创建一个DOMDocument对象以保存从客户端传来的数据: 
  注释: 创建 DOMDocument 对象 
  Set xml = Server.CreateObject _ 
   ("msxml2.DOMDocument") 
  xml.async = False 
  然后,它加载POST数据,并检测格式错误的请求: 
  注释: 加载 POST 数据 
  xml.Load Request 
  If xml.parseError.errorCode <> 0 Then 
   Call responseError _ 
   ("Could not load XML message." & _ 
   "Description: " & _ 
   xml.parseError.reason & _ 
   "<br>Line: " & xml.parseError.Line) 
  End If 
  它查找元素commandtext的取值或元素returnsdata、returnsvalues的取值。因为查找这些元素取值的代码是相似的,因此这里只给出查找元素commandtext取值的代码: 
  Set N = xml.selectSingleNode _ 
   ("command/commandtext") 
  If N Is Nothing Then 
   Call responseError _ 
     ("Missing <sp_name> parameter.") 
  Else 
   sp_name = N.Text 
  End If 
  然后,页面创建一个Command对象,读取所有<param>元素,并为请求中存在的每一个元素创建一个参数,代码如下: 
   注释: create parameters, if any 
  set nodes = xml.selectNodes("command/param") 
  if nodes is nothing then 
   注释: no parameters 
  elseif nodes.length = 0 then 
   注释: no parameters 
  else 
   for each param in nodes 
     注释: Response.Write server.HTMLEncode(param.xml) & "<br>" 
     on error resume next 
     paramName = param.selectSingleNode("name").text 
     if err.number <> 0 then 
       call responseError("Parameter creation: " & _ 
       "Unable to find the name " & "tag.") 
     end if 
     paramType = param.selectSingleNode("type").text 
     paramDirection = param.selectSingleNode("direction").text 
     paramSize = param.selectSingleNode("size").text 
     paramValue = param.selectSingleNode("value").text 
     if err.number <> 0 then 
       call responseError("The parameter named 注释:" & _ 
       paramName & "注释: was " & "missing one or more " & "required fields.") 
     end if 
     cm.Parameters.Append cm.CreateParameter(paramName,paramType, _ 
     paramDirection,paramSize,paramValue) 
     if err.number <> 0 then 
       call responseError("Unable to create or " & "append the parameter " & _ 
       "named 注释:" & paramName & ".注释: " & err.description) 
       Response.end 
     end if 
   next 
   on error goto 0 
  end if 
  最后,页面打开一个链接并执行请求,对于存储过程,它使用不返回数据的adExecuteNoRecords选项。 
  set conn = 
   Server.CreateObject_ 
   ("ADODB.Connection") 
  conn.Mode=adModeReadWrite 
  conn.open _ 
   Application("ConnectionString") 
  set cm.ActiveConnection=conn 
  注释: retrieve the data 
  if not returnsData then 
   cm.Execute 
  else 
   set R = server.CreateObject _ 
     ("ADODB.Recordset") 
   R.CursorLocation = adUseClient 
   R.Open cm, ,adOpenStatic, _ 
   adLockReadOnly 
  end if 
  如果命令返回数据,变量returnsData的值将为True,页面以XML文档方式将recordset的结果返回给客户机: 
  注释: return the data, if required 
  if returnsData then 
   R.Save Response, adPersistXML 
   if err.number <> 0 then 
     call responseError _ 
     ("Recordset Save Error " & _ 
     "on command 注释:" & CommandText & _ 
     "注释:: " & Err.Description) 
     Response.end 
   end if 
  ... 
如果命令以输出参数返回值,页面将返回包含这些值的XML字符串。该文档的根是一个,每一个返回值对应一个子元素(参见本文可下载例程代码)。 
  如果产生错误,页面使用responseError子程序格式化并返回一个XML字符串。参数sDescription中包含了错误文本: 
  Sub responseError(sDescription) 
   Response.Write _ 
     "<response><data>Error: " & sDescription & "</data></response>" 
   Response.end 
  End Sub 
  上面的图中显示了运行中的一个实例。应用程序在左边的<div>标记里显示一个客户名列表。每个客户旁边有两个链接:Purchase History和Recent Purchase。当用户点击其中之一时,客户端程序运行一个存储过程并在右边的<div>标记里显示结果。 
  为了显示这一方案的灵活性,虽然都使用到getData.asp,但三个返回数据的操作以互不相同的方式工作。对客户列表的查询涉及到动态SQL。对Purchase History的查询运行了一个称为CustOrderHist的存储过程,它附带在Northwind数据库中,并且返回一个recordset。对Recent Purchase的查询运行了一个称为RecentPurchaseByCustomerID的存储过程,它接受一个输入参数CustomerID并在一个输出参数ProductName中返回该客户最近购买的产品的名称。过程的定义是: 
  CREATE PROCEDURE RecentPurchaseByCustomerID 
   @CustomerID nchar(5), 
   @ProductName nchar(40) output 
  AS 
  SELECT @ProductName = 
   (SELECT top 1 ProductName _ 
   FROM Products 
   INNER JOIN ([Order Details] 
  INNER JOIN Orders ON_ 
   Orders.OrderID=[Order_ 
   Details].OrderID) 
  ON Products.ProductID = 
   [Order Details].ProductID 
  WHERE Orders.OrderDate = 
   (SELECT MAX(orders.orderdate)_ 
   FROM Orders 
   where CustomerID=@CustomerID) 
  AND Orders.CustomerID=@CustomerID) 
  GO 
  无论查询中包含的是动态SQL、返回recordset的存储过程、还是在输出参数中返回值的存储过程,设置POST消息的过程差不多都完全相同。客户页面创建XML字符串<command>、创建XMLHTTPRequest对象,用Open方法将它设置为对getData.asp 页面中的URL使用POST方法、并制定对象以异步方式工作(Open方法中的False参数)。它使用Send方法发送字符串: 
  set xhttp = createObject("msxml2.XMLHTTP") 
  xhttp.open "POST", "http://localhost/myWeb/getData.asp", False 
  xhttp.send s 
  注意:在Open方法中使用的URL必须是一个完整的URL,而不是一个相对URL。换句话说,下面的代码不能工作,因为URL是不完整的: 
  xhttp.open "POST", "getData.asp", False 
用VB和XML建立集中式应用程序(下)
(作者:青苹果工作室编译 2001年03月19日 15:41)
传送消息及接收数据 
  客户端的XML消息由一个带有几个子元素的<command>元素构成:一个包含着存储过程名称的<commandtext>元素;一个<returnsdata> 元素,它告诉服务器客户端是否期待返回数据;返回零个还是多个包含着参数信息的<param>元素。在这个最简单的没有参数的情况下,传送的字符串查询类似于: 
  <command> 
   <commandtext> 
     StoredProc or Dynamic SQL 
   </commandtext> 
   <returnsvalues>True</returnsvalues> 
  </command> 
  要添加参数,就需要为每一个参数添加一个<param>元素。每个<param>元素有五个子元素:<name>、<type>、<direction>、<size>和<value>。五个子元素的先后次序无关紧要,但都是必须的。通常,依照定义ADO Parameter对象所需的次序定义它们。例如,存储过程CustOrderHist需要一个CustomerID参数,所以创建传送到 detData.asp的XML字符串的代码就是: 
  dim s 
  s = "<?xml version=""1.0""?>" & vbcrlf 
  s = s & "<command><commandtext>" 
  s = s & "CustOrderHist" 
  s = s & "</commandtext>" 
  s = s & "<returnsdata>" & "True</returnsdata>" 
  s = s & "<param>" 
  s = s & "<name>CustomerID</name>" 
  s = s & "<type><%=adVarChar%></type>" 
  s = s & "<direction>" & "<%=adParamInput%></direction>" 
  s = s & "<size>" & len(CustomerID) & "</size>" 
  s = s & "<value>" & CustomerID & "</value>" 
  s = s & "</param>" 
  s = s & "</command>" 
  注意前面的代码是在客户端的;ADO常量没有在客户端定义,这是它们必须用<% %>标记括起来的原因。在发送应答之前,服务器用正确的值替换它们。页面getData.asp有一个Response.ContentType属性,其数值为"text/xml";因此,我们就能使用ResponseXML属性查询结果。当查询返回一个记录集合时,可以创建一个Recordset对象并像以下代码那样使用XMLHTTP-Request对象的responseXML属性来打开它: 
  Dim R 
  set R = createObject("ADODB.Recordset") 
  R.open xhttp.responseXML 
  当通过输出参数返回查询数据时,可以通过将一个变量设置为XMLHTTPRequest对象的responseXML属性来创建一个DOMDocument: 
  Dim xml 
  set xml = xhttp.responseXML 
  使用输出参数时,XML字符串包含和每个返回值相对应的一个元素。每个元素都是<values>根元素的一个子元素。例如: 
  <?xml version=""1.0"" 
   encoding=""ISO-8859-1""?> 
  <values> 
   <paramname>value</paramname> 
   <paramname>value</paramname> 
  </values> 
  如果数据包含外语字符,就有可能需要修改encoding属性。ISO-8859-1编码能很好地支持大多数西欧语言。 
  各种情况下,客户端页面都会使用返回值来格式化一个HTML字符串,这个字符串放在屏幕右侧的div标记里的。客户端页面使用div对象的innerHTML属性来进行显示: 
  document.all("details").innerHTML = 
   <some formatted html string> 
为不同类型的客户端服务 
  ASP页面让我们方便地看到应用程序到底都做了些什么,但你能用相同的技术建立使用任何技术的客户端程序。可下载代码中包含了像ASP页面一样地显示数据的Visual Basic工程文件,但是VB工程并不创建它传送到服务器上的XML字符串。相反,在启动时它通过一个称为Initialize的存储过程从服务器查询它们,这个过程简单地从叫做ClientCommands的数据表里查找这些内容。 
  数据表ClientCommands包含两个字段:command_name和command_xml字段。客户端程序接收三个特定的command_name:getCustomerList、CustOrderHist和recentPurchaseByCustomerID。这些命令的command_xml字段包含了程序发送到getData.asp页面的XML字符串,如此就集中地控制了XML字符串的格式以及存储过程的“真实”名称。在将XML字符串发送到getData.asp前,客户端程序使用XML DOM来设置存储过程的参数值。可下载的代码包含定义Initialize过程和创建并维护数据表ClientCommands的SQL代码。 
右图显示了 VB客户端程序。图中,示例程序的VB版本显示了同基于浏览器版本大致相同的信息,只不过它使用的是bound data grid控件,而不是 HTML。 
  示例应用程序演示了使用XHTTPRequest对象来兑现本文开头中所做的许诺。应用程序工作在能访问getData.asp的任何远程计算机内。我们能通过IIS或NTSF权限设置限制对ASP页面的访问,并且能在服务器而不是客户机上保存应用程序的全局设置。这样就避免了通过网络发送数据库的用户名和口令,而且在IE中应用程序按照需要显示数据而不是刷新整个页面。 
结语 
  在该应用的正式产品代码中,我们能用很多种方法使应用程序更有效。比如,我们能从ASP页面中去掉数据查询代码,并将其放到一个COM应用程序中;或者,我们还可以创建XSLT变换器以显示返回的数据。最后,通过放弃动态页面更新,并在服务器上进行XSLT变换,我们能够扩大客户端程序的应用范围。现在需要的做的,就是去尝试。  
关键词:用VB与XML创建集中式应用程序