C++ Builder 6 BizSnap/SOAP/WebService(2) - -- 通过 SOAP 传递自定义类型数据

说明:本文经过一些改动,纠正了一些问题。
不久前我收到几位朋友发来Mail说明他们在按照本文所述进行WebService应用开发时碰到的一个问题:在用ISAPI方式编写本文例子程序时发生AV错误。根据改进后的本例子程序修改了本文,请注意文中加粗部分内容。
--2002-8-17

本文将做一个略复杂的例子,实现通过 SOAP 传递自定义的数据类型。本例子的功能是在服务端通过 ADO 的数据访问控件取得数据表内容,然后将其通过 SOAP 传递到客户端再显示。

服务端:
1.New|WebServices|Soap Server Application ,如下图,与 Delphi 6 + Update 2 相比,除了左上角的图标以外,完全相同:

BCB SOAP1 IDE1

选 Web App Debugger executeable 类型, CoClass Name 为:wadSoapDemo2 ,如下图:

        <img width="317" height="254" src="/images/illustrations/bcb6_soap2ide1.jpg" alt="BCB SOAP2 IDE1"/> 
        <p>确定后将自动提示是否要新建一个接口,如下图,确定即可打开新建接口向导,如果要以后再增加接口,可以在 New|WebServices 
          中选择 SOAP Server Interface 同样可打开新建接口向导:</p>
        <img width="252" height="122" src="/images/illustrations/bcb6_soap1ide3.jpg" alt="BCB SOAP1 IDE3"/> 
        <p>2.新建接口向导如下图,输入接口名:DataTable 即可生成一个 SOAP 服务端接口:</p>
        <img width="445" height="216" src="/images/illustrations/bcb6_soap2ide2.jpg" alt="BCB SOAP2 IDE4"/> 
        <p> 关于此向导的其它说明见<a href="/root/entry.php?id=31">《C++ Builder 6 BizSnap/SOAP/WebService(1) -- 一个 Hello world! 的例子》</a>(以下简称《(1)》);<br/>
          3.<b>(注意:原文的这部分有错,现在为修改后的)</b>新建一个 DataModule ,放入四个数据库控件: ADOConnection1, 
          ADODataSet1, DataSetProvider1, ClientDataSet1 ,其各属性设置如下表:</p>
        <table width="94%" border="1">
          <tbody><tr> 
            <td width="12%" height="36">ADOConnection1</td>
            <td width="88%" height="36">ConnectionString = &quot;Provider=SQLOLEDB.1;Persist 
              Security Info=False;User ID=sa;Initial Catalog=Northwind;Data 
              Source=raptor\neutrino&quot;;<br/>
              LoginPrompt = false; </td>
          </tr>
          <tr> 
            <td width="12%" height="25">ADODataSet1</td>
            <td width="88%" height="25">Connection = ADOConnection1;<br/>
              CommandText = &quot;select FirstName, LastName from Employees&quot;; 
            </td>
          </tr>
          <tr> 
            <td width="12%" height="14">DataSetProvider1</td>
            <td width="88%" height="14">DataSet = ADODataSet1; </td>
          </tr>
          <tr> 
            <td width="12%" height="2">ClientDataSet1</td>
            <td width="88%" height="2">ProviderName = DataSetProvider1; </td>
          </tr>
        </tbody></table>
        <p>完成后的 DataModule 如下图:</p>
        <img width="383" height="236" src="/images/illustrations/bcb6_soap2ide8.jpg" alt="BCB SOAP2 IDE8"/> 
        <p> <b>(关于这部分的补充说明)</b>:原用 dbExpress 在用于 ISAPI 时会出现&ldquo;Unable load dbexpint.dll&rdquo;的错误,所以改用 
          ADO 。另,因没有 ADOClientDataSet 控件,否则若是 BDE/dbExpress/IBExpress 则只需要两个控件即可。要把数据控件放在单独的 
          DataModule ,而不能放在 WebModule 中的原因见<a href="/root/entry.php?id=57">《Web 应用的执行过程 -- 谈谈 WAD/CGI/ISAPI 的区别》</a>,本文例子仅供参考,建议在涉及数据库操作的应用中,最好用 
          SOAP Server Data Module (如<a href="/root/entry.php?id=33">《C++ Builder 6 BizSnap(3) -- DataSnap 数据库应用》</a>)。<br/>
          4.SaveAll , Unit1 命名为: MainWM , Unit2 命名为 Demo2DM , Project1 命名为: 
          Demo2 , DataTable 不改名; <br/>
          5.在接口单元的头文件(DataTable.h)中增加一个自定义的类 -- TDataSetPack ,如下: </p>
        <pre>class TDataSetPack : public TRemotable {<br/>private :<br/>    int        FCount;<br/>    AnsiString FXMLData;<br/><br/><br/>public :<br/>    __fastcall TDataSetPack( TCustomClientDataSet * aClientDataSet )<br/>        : TRemotable(),<br/>        FCount( aClientDataSet-&gt;RecordCount ),<br/>        FXMLData( aClientDataSet-&gt;XMLData )<br/>    {<br/>    }<br/><br/>__published:<br/>    __property int        Count   = { read = FCount   }; <br/>    __property AnsiString XMLData = { read = FXMLData };<br/>};<br/></pre>
        <p>自定义 SOAP 数据类型必须是从 TRemotable 类派生的,这一点与 Delphi 相同。其中 ClientDataSet 
          的 XMLData 属性是从 Delphi 6 开始新增的。其实 XMLData 中已经包含了记录数信息, Count 属性并不是必须的,这里为了演示自定义 
          SOAP 数据类型的使用,所以加入这个属性。注意:要在此头文件中加入:#include &lt;DBClient.hpp&gt;<br/>
          5.定义及实现 GetEmployeeTable 函数,其方法与《(1)》中相同,下面是在接口头文件(DataTable.h)和单元文件(DataTable.cpp)中的接口/类定义和我们加入的方法及其实现: 
        </p>
        <pre>//  DataTable.h<br/>__interface INTERFACE_UUID(&quot;{CF057C28-4130-4508-9F24-0BBD1C2CA5F0}&quot;) <br/>    IDataTable : public IInvokable<br/>{<br/>public:<br/>    virtual TDataSetPack * GetEmployeeTable( void ) = 0;  //  新增方法<br/>};<br/>typedef DelphiInterface<idatatable> _di_IDataTable;<br/><br/>//  DataTable.cpp<br/>class TDataTableImpl : public TInvokableClass, public IDataTable<br/>{<br/>public:<br/>    TDataSetPack * GetEmployeeTable( void );  //  新增方法<br/><br/>  /* IUnknown */<br/>  HRESULT STDMETHODCALLTYPE QueryInterface(const GUID&amp; IID, void **Obj)<br/>                        { return GetInterface(IID, Obj) ? S_OK : E_NOINTERFACE; }<br/>  ULONG STDMETHODCALLTYPE AddRef() { return TInterfacedObject::_AddRef();  }<br/>  ULONG STDMETHODCALLTYPE Release(){ return TInterfacedObject::_Release(); }<br/><br/>  /* Ensures that the class is not abstract */<br/>  void checkValid() { delete new TDataTableImpl(); }<br/>};<br/><br/>//  新增方法的实现:<br/>//  如果是 CGI/ISAPI 应用,则需要新建 DataModule1 ,相应修改见后面说明<br/>//  打开 ClientDataSet ,构造 TDataSetPack ,<br/>//  关闭 ClientDataSet 和数据库连接<br/>//  返回结果<br/>TDataSetPack * TDataTableImpl::GetEmployeeTable( void )<br/>{<br/>//  如果是 CGI/ISAPI 则要此句<br/>//    Application-&gt;CreateForm(__classid(TDataModule1), &amp;DataModule1);<br/>    DataModule1-&gt;ClientDataSet1-&gt;Open();<br/>    TDataSetPack * p = new TDataSetPack( DataModule1-&gt;ClientDataSet1 );<br/>    DataModule1-&gt;ClientDataSet1-&gt;Close();<br/>    DataModule1-&gt;ADOConnection1-&gt;Close( );<br/>    return p;<br/>}<br/></idatatable></pre>
        <p>除了方法的实现部分以外,其它部分与《(1)》基本上一样。这个方法的实现功能,正如程序中的注释说明的那样,用于取得数据集并转换为我们定义的数据类型后返回。</p>
        <p><strong>(Jul.22-03)注意:</strong>程序中有一个变量 p 是通过 new 符创建的,并将它作为返回值返回,但并没有显式将其删除,这会不会造成 
          Memory Leak 呢?感谢一位叫 Siney 的细心的朋友发现并向我指出这个问题。<br/>
          不过其实这个担心是没有必要的。关于这个我在这里作一个补充说明:<br/>
          因为 Borland 早就考虑到了这个问题, 在 TRemotable 的帮助中,关于析构函数的部分有如下内容:<br/>
          <br/>
          On server applications, there is typically no need to explicitly 
          free a TRemotable instance (for example, one created as a return 
          value or output parameter). By default, when a TRemotable descendant 
          is created in a method that was called remotely using an invokable 
          interface, it is added to a data context (the value of the DataContext 
          property). As long as the remotable object belongs to the data context, 
          the data context handles freeing the object. Similarly, TRemotable 
          instances that are passed in as a parameter belong to the data context.<br/>
          On client applications, TRemotable instances created and passed 
          to an invokable interface or returned as a parameter or result of 
          an invokable interface must be freed by the calling application.<br/>
          <br/>
          可见在服务端中创建 TRemotable 的派生类的实例可以不必显示删除,它会在生成 SOAP 数据后自动被删除的。但在客户端使用,则还是需要显式删除的。</p>
        <p>&nbsp;<b>(关于这部分的补充说明)</b>:注意如果是 CGI/ISAPI 应用,其中的 DataModule1 是动态创建(原因如<a href="/root/entry.php?id=57">《Web 
          应用的执行过程 -- 谈谈 WAD/CGI/ISAPI 的区别》</a>一文所述),所以相应的要把 Project|Source 
          ( 即 Demo2CGI.cpp/Demo2ISAPI.cpp )中如下代码片段那样将自动创建语句去掉。 </p>
        <pre>      Application-&gt;CreateForm(__classid(TWebModule1), &amp;WebModule1);<br/>//  如果是 CGI/ISAPI 应用则不要此句<br/>//         Application-&gt;CreateForm(__classid(TDataModule1), &amp;DataModule1);<br/>         Application-&gt;Run();<br/></pre>

6.注册接口及其实现类的部分也与《(1)》相同,就不再赘述了。
7.编译之即可产生: Demo2.exe ;

先运行一次 Demo2.exe ,完成注册的工作后启动 Web App Debugger 。打开浏览器, 输入 URL 为: http://localhost:1024/Demo2.wadSoapDemo2 即可看到一个标准的 SOAP 应用说明页面,点击进入相应链接即可看到相关的 WSDL ,在其中可以看到我们自定义的数据类型说明,如下面的 WSDL 片断所示:

  <types>
<xs:schema targetNamespace="urn:DataTable" xmlns="urn:DataTable">
<xs:complexType name="TDataSetPack">
<xs:sequence>
<xs:element name="Count" type="xs:int"/>
<xs:element name="XMLData" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:schema>
</types>

    客户端程序:
1.New|Application 新建一个一般 VCL 应用程序;
2.SaveAll , Unit1 命名为 ClnMain , Project1 命名为 Client ;
3.New|Web Services|Web Services Importer :
在下图中的URL中输入: http://localhost:1024/Demo2.wadSoapDemo2/wsdl/IDataTable,

BCB SOAP2 IDE4

如果上面用浏览器可以看到正确的 XML 文档的话,选择“Next”后将产生导入的结果,如下图:

BCB SOAP2 IDE5

其中有我们在服务端定义的数据类型 TDataSetPack 、接口 IDataTable 及其方法 GetEmployeeTable ,选择完成即可生成接口单元;
4.SaveAll, IDataTable 单元不改名保存,再在 ClnMain 中 #include IDataTable.h ;
5.在 Form 上放上一个 ClientDataSet, DataSource, DBGrid, Button, Label 等几个控件,其各属性设置如下表:

ClientDataSet1 全部默认
DataSource1 DataSet = ClientDataSet1;
DBGrid1 DataSource = DataSource1;
Button1 Caption = "Fetch data";
Label1 Caption = "Count:0";

完成后的 Form 如下图:

BCB SOAP2 IDE6

6.双击 Button1 输入下面的程序:

void __fastcall TForm2::Button1Click(TObject *Sender)
{
TDataSetPack * p = GetIDataTable()->GetEmployeeTable();

Label1->Caption = AnsiString( "Count:" ) + IntToStr( p->Count );
ClientDataSet1->XMLData = p->XMLData;
}

7.编译运行,按 Button1 , DBGrid1 中将显示服务端返回的数据集内容, Label1 中将显示记录数,如下图(说明,此图仍为原来用 InterBase 时的数据);

BCB SOAP2 IDE7

    这只是一个简单的数据库访问的例子,只能从服务端取回数据集, C++ Builder 6 中已经将 MIDAS/DataSnap 和 SOAP/WebService 结合,可以通过 SOAP/WebService 实现非常强大的数据库操作能力,这将在以后的文章中介绍。