第十四章 剖析几个MIDAS示范程序 MIDAS是Multi-Tier Distributed Application Services Suite的简称,为Delphi 4的一个关键技术。对于初学者来说,MIDAS具有相当的难度,因此,这一章详细剖析几个MIDAS示范程序,以帮助读者理解和掌握MIDAS技术。 与一般的数据库应用程序不同的是,只有当应用服务器正在运行的情况下,才能打开、编译和运行“瘦”客户程序的项目。 14.1 一个ActiveForm的例子 Delphi 4可以把分布式的数据库结构引申到Internet/Intranet上,把“瘦”客户程序作为ActiveForm嵌入到网页中让人们下载,然后在当地执行。 这一节剖析一个ActiveForm的示范程序,项目名称叫empeditx,它可以在C:\Program Files\Borland\Delphi4\Demos\Midas\Activefm目录中找到。它的主窗体如图14.1所示。 在打开这个项目之前,先要编译、运行位于C:\ProgramFiles\Borland\Delphi4\Demos\ Midas\Empedit目录中的Server项目,这是个应用服务器,如图14.2所示。 这里还要交代一下,在应用服务器上,用一个TQuery构件引入数据集,它的SQL语句如下: Select * From Employee 在这个ActiveForm上,有一个TDCOMConnection构件,用于以DCOM方式连接应用服务器,它的ServerName属性设为Serv.EmpServer,它的ServerGUID设为{53BC6562-5B3E-11D0-9FFC-00A0248E4B9A}。 ActiveForm用TClientDataSet构件从应用服务器引入数据集,它的RemoteServer属性设为MidasConnection即TDCOMConnection构件的名称,它的ProviderName属性设为EmpQuery即应用服务器上的TQuery构件,由它来提供IProvider接口。 ActiveForm上有几个数据控件,用于显示数据,它们都通过一个TDataSource构件获得数据。此外,ActiveForm上还有一个TDBNavigator构件,用于浏览数据集。 由于本节要介绍的示范程序是一个ActiveForm,它的大部分代码与类型库有关,我们只把其中涉及到MIDAS技术的部分“拎”出来。 当用户单击ActiveForm上的“Get Employees”按钮时,就从应用服务器检索数据。每次检索到的记录数取决于TClientDataSet构件的PacketRecords属性。 Procedure TEmpEditForm.QueryButtonClick(Sender: TObject); Begin Employees.Close; { Employees是TClientDataSet构件的名称}E mployees.Open; End; 当用户单击ActiveForm上的“Update Employees”按钮时,把用户对数据的修改写到数据集中。 Procedure TEmpEditForm.UpdateButtonClick(Sender: TObject); Begin Employees.ApplyUpdates(-1); End; 当用户单击ActiveForm上的“Undo Last Change”按钮时,取消用户对数据的修改。 Procedure TEmpEditForm.UndoButtonClick(Sender: TObject); Begin Employees.UndoLastChange(True); End; 为了测试这个ActiveForm,首先需要把它发布到Web服务器上,供下载用。为此,要使用“Project”菜单上的“Web Deployment Options”命令设置有关Web发布的选项,主要是指定ActiveForm在Web服务器上的URL。然后使用“Project”菜单上的“WebDeploy”命令把ActiveForm发布到Web服务器上。 14.2 一个动态传递SQL语句的示范程序 这一节要剖析一个动态传递SQL语句的示范程序,可以在C:\Program Files\Borland\Delphi4\Demos\Midas\Adhoc目录中找到。 这个程序分为应用服务器和客户程序两个部分。当客户程序通过IProvider接口调用DataRequest请求数据时,把用户输入的SQL语句传递给应用服务器,这样,应用服务器上的TQuery构件就能够根据用户的要求来查询数据库,这就是本示范程序的基本思路。 先来剖析应用服务器,看它的数据模块,如图14.3所示。 图14.3 数据模块 数据模块上有这么几个构件: 一个TSession构件,它的SessionName属性设为Session1_2。 一个TDatabase构件,它的SessionName属性设为Session1_2,并且定义了一个专用的别名叫ADHOC。 一个TQuery构件,它的DatabaseName属性设为ADHOC,它的SessionName属性也设为Session1_2,而它的SQL属性为空,因为SQL语句由客户程序动态地传递过来。 一个TProvider构件,它的DataSet属性设为AdHocQuery即TQuery构件的名称。 现在我们暂时不管数据模块,再来看看应用服务器的主窗体,如图14.4所示。 图14.4 应用服务器的主窗体 主窗体上显示两个计数,一个是当前连接应用服务器的客户数(Clients),另一个是已经执行的查询次数(Queries)。 用什么来判断当前的客户数,这与数据模块的实例方式有关。我们可以回到数据模块的单元,看看它的初始化代码: Initialization TComponentFactory.Create(ComServer, TAdHocQueryDemo, Class_AdHocQueryDemo, ciMultiInstance); End. 可以看出,这个数据模块的实例方式设为ciMultiInstance,表示每当有一个客户连接应用服务器,就会创建数据模块的一个新的实例。因此,数据模块的实例数就是当前的客户数。怎样统计数据模块的实例数呢?很简单,只要处理数据模块的OnCreate事件。 Procedure TAdHocQueryDemo.AdHocQueryDemoCreate(Sender: TObject); Begin MainForm.UpdateClientCount(1); End; 当一个客户退出连接时,将删除一个数据模块的实例,此时将触发数据模块的OnDestroy事件: Procedure TAdHocQueryDemo.AdHocQueryDemoDestroy(Sender: TObject); Begin MainForm.UpdateClientCount(-1); End; 其中,UpdateClientCount函数是在主窗体的单元中定义的: Procedure TMainForm.UpdateClientCount(Incr: Integer); Begin FClientCount := FClientCount + Incr; ClientCount.Caption := IntToStr(FClientCount); End; 请读者注意Incr参数的作用。怎样统计已经执行过的查询数呢?很简单,只要统计TQuery构件被激活的次数就可以了。因此,程序处理了TQuery构件的AfterOpen事件。Procedure TAdHocQueryDemo.AdHocQueryAfterOpen(DataSet: TDataSet); Begin MainForm.IncQueryCount; End; IncQueryCount是在主窗体的单元中定义的: Procedure TMainForm.IncQueryCount; Begin Inc(FQueryCount); QueryCount.Caption := IntToStr(FQueryCount); End; 在打开客户程序的项目之前,必须先编译和运行应用服务器的项目。好,现在我们打开客户程序的项目,它的主窗体如图14.5所示。 这个客户程序用一个TDCOMConnection构件连接应用服务器,它的ServerName属性设为Serv.AdHocQueryDemo。客户程序用TClientDataSet构件从应用服务器引入数据集,它的RemoteServer属性设为TDCOMConnection构件的名称,它的ProviderName属性设为AdHocQuery,这是应用服务器输出的 IProvider接口。 客户程序上有一个栅格,用于显示数据,栅格与数据集之间通过TDataSource构件连接。此外,客户程序上有一个多行文本编辑器,让用户输入SQL语句。有一个组合框用于选择要访问的数据库。我们还是先从处理OnCreate事件的句柄开始。 Procedure TForm1.FormCreate(Sender: TObject); var I: Integer;DBNames: OleVariant; Begin RemoteServer.Connected := True; DBNames := RemoteServer.AppServer.GetDatabaseNames; If VarIsArray(DBNames) then For I := 0 to VarArrayHighBound(DBNames, 1) Do DatabaseName.Items.Add(DBNames[I]); DatabaseNameClick(Self); End; 首先,把TDCOMConnection构件的Connected属性设为True,将连接应用服务器。TDCOMConnection构件的AppServer属性将返回应用服务器上数据模块的接口,通过此接口就可以调用远程数据模块的方法,例如GetDatabaseNames。GetDatabaseNames是在应用服务器的数据模块单元中定义的: Function TAdHocQueryDemo.GetDatabaseNames: OleVariant; var I: Integer; DBNames: TStrings; Begin DBNames := TStringList.Create; Try Session1.GetDatabaseNames(DBNames); Result := VarArrayCreate([0, DBNames.Count - 1], varOleStr); For I := 0 to DBNames.Count - 1 DoResult[I] := DBNames[I]; FinallyDBNames.Free; End; End; GetDatabaseNames函数的作用是返回一个数组,该数组由所有已定义的别名和BDE会话期对象专用的别名组成。现在我们回到客户程序中,调用了数据模块的GetDatabaseNames函数后,就把检索到别名加到窗体右上角的组合框中,然后调用DatabaseNameClick函数。 Procedure TForm1.DatabaseNameClick(Sender: TObject); var Password: string; Begin If DatabaseName.Text <> '''''''' then Begin ClientData.Close; Try RemoteServer.AppServer.SetDatabaseName(DatabaseName.Text, ''''''''); Except On E: Exception DoIf E.Message = ''''Password Required'''' then Begin If InputQuery(E.Message, ''''Enter password'''', Password) then RemoteServer.AppServer.SetDatabaseName(DatabaseName.Text, Password); End Else Raise; End; End; End; 调用DatabaseNameClick的目的是使应用服务器与另一个数据库连接,这就需要通过AppServer属性获得数据模块的接口,然后调用数据模块单元的SetDatabaseName。SetDatabaseName是在应用服务器的数据模块单元中定义的: Procedure TAdHocQueryDemo.SetDatabaseName(const DBName, Password: WideString); Begin Try Database1.Close; Database1.AliasName := DBName; If Password <> '''''''' then Database1.Params.Values[''''PASSWORD''''] := Password; Database1.Open; Except {如果数据库打开失败,很可能是因为该数据库需要口令} On E: EDBEngineError DoIf (Password = '''''''') then Raise Exception.Create(''''Password Required'''')Else Raise; End; End; SetDatabaseName的作用是修改TDatabase构件的AliasName属性,然后连接新的数据库,如果失败,就触发一个异常。在客户程序的DatabaseNameClick过程中,如果出现异常,就弹出一个输入框,让用户输入口令,然后再次调用数据模块的SetDatabaseName。 当用户在“Query”框中输入了SQL语句,就可以单击“Run Query”按钮执行这个查询。问题是,只有应用服务器才可以执行查询,那么客户程序是怎样把SQL语句传递给应用服务器的呢?这就是本示范程序的关键之处。 Procedure TForm1.RunButtonClick(Sender: TObject); Begin ClientData.Close; ClientData.Provider.DataRequest(SQL.Lines.Text); ClientData.Open; End; 原来,客户程序通过IProvider接口调用DataRequest把用户输入的SQL语句传递给应用服务器。客户程序通过IProvider接口调用DataRequest将在应用服务器端触发OnDataRequest事件,我们来看看应用服务器是怎样处理OnDataRequest事件的。 Function TAdHocQueryDemo.AdHocProviderDataRequest(Sender: TObject; Input: OleVariant): OleVariant; Begin AdHocQuery.SQL.Text := Input; End; 至此,一个动态传递SQL语句的示范程序剖析完毕,请读者仔细琢磨其中的编程技巧。实际上,通过IProvider接口调用DataRequest可以传递任何信息。 14.4 一个全面演示TClientDataSet功能的示范程序 这一节介绍一个演示TClientDataSet功能的示范程序,项目名称叫Alchtest,它可以在C:\Program Files\Borland\Delphi4\Demos\Midas\Alchtest目录中找到,主窗体如图14.6所示。 这个程序的总体思路是,用一个多页控件让用户修改TClientDataSet的属性或者调用它的方法,然后在下面的TAB控件中演示修改后的效果。 程序首先在处理OnCreate事件的句柄中做了一些初始化的工作。 Procedure TDBClientTest.FormCreate(Sender: TObject); var I: Integer; Begin Database1.Close; FMaxErrors := -1; FPacketRecs := -1; SetCurrentDirectory(PChar(ExtractFilePath(ParamStr(0)))); For I := 0 to StatusBar.Panels.Count - 1 do StatusBar.Panels[I].Text := ''''''''; Application.OnIdle := ShowHeapStatus; Application.OnHint := OnHint; StreamSettings(False); SetEventsVisible(ViewEvents.Checked); End; 其中,指定ShowHeapStatus作为处理应用程序的OnIdle事件的句柄,指定OnHint作为处理应用程序的OnHint事件的句柄。ShowHeapStatus是这样定义的: Procedure TDBClientTest.ShowHeapStatus(Sender: TObject; var Done: Boolean); Begin Caption := Format(''''Client DataSet Test Form - (Blocks=%d Bytes=%d)'''',[AllocMemCount, AllocMemSize]); End; ShowHeapStatus的作用是在应用程序空闲的时候,在主窗口的标题栏显示堆的状态,其中,AllocMemCount是当前分配的内存块数,AllocMemSize是当前分配的内存总长度。 OnHint是这样定义的: Procedure TDBClientTest.OnHint(Sender: TObject); Begin StatusMsg := Application.Hint; End; StatusMsg是一个自定义的属性,用于表达要在状态栏上显示的提示信息。 在处理OnCreate事件的句柄中还调用了StreamSettings函数。StreamSettings是非常有用的,当窗体关闭时,就调用StreamSettings把窗体上一些控件的状态保存到一个配置文件中。当窗体弹出时,就调用StreamSettings读取配置文件以初始化窗体上的控件。 Procedure TDBClientTest.StreamSettings(Write: Boolean); Procedure WriteStr(const OptName, Value: string); Begin FConfig.WriteString(''''Settings'''', OptName, Value); End;
Procedure WriteBool(const OptName: string; Value: Boolean); Begin FConfig.WriteBool(''''Settings'''', OptName, Value); End;
Function ReadStr(const OptName: string): string; Begin Result := FConfig.ReadString(''''Settings'''', OptName, ''''''''); End;
Function ReadBool(const OptName: string): Boolean; Begin Result := FConfig.ReadBool(''''Settings'''', OptName, False); End;
Function FindPage(const PageName: string): TTabSheet; var I: Integer; Begin For I := AreaSelector.PageCount - 1 downto 0 do Begin Result := AreaSelector.Pages[[1] [2] [3] [4] 下一页 |