【问题标题】:Updating client UI while waiting for DataSnap在等待 DataSnap 时更新客户端 UI
【发布时间】:2012-03-20 09:07:57
【问题描述】:

我在 Delphi XE2 中创建了一个 MDI Delphi 应用程序,它通过 TSQLConnection 组件 (driver = datasnap) 连接到 DataSnap 服务器。在设计时右键单击 TSQLConnection 可以让我生成 DataSnap 客户端类 (ProxyMethods)。

我的目标是在客户端有一个经过时间的时钟 [0:00],它显示 DataSnap 请求需要多长时间才能提供服务,每 1 秒更新一次。我尝试过但不起作用的两种方法是:

方法#1

使用具有 1 秒间隔的 TTimer 在执行 ProxyMethod 时更新经过的时钟。我在调用 ProxyMethod 之前启用了计时器。当 ProxyMethod 运行时,OnTimer 事件不会触发——代码中的断点不会被命中。

方法#2

与方法 #1 相同,除了计时器是 TJvThreadTimer。当 ProxyMethod 运行时,OnTimer 事件会触发,但 OnTimer 代码直到 ProxyMethod 完成后才会执行。这很明显,因为 OnEvent 代码中的断点会在 ProxyMethod 完成后快速连续命中——就像 OnTimer 事件都已在主队列中排队VCL 线程。

此外,在运行缓慢的 ProxyMethod 时单击客户端应用程序上的任意位置会使应用程序看起来被挂起(标题栏中出现“未响应”)。

我认为最好的解决方案是将 ProxyMethods 的执行移至单独的线程。但是,必须有一个现有的解决方案——因为相关的hung应用问题似乎是一个常见投诉。我只是找不到解决方案。

感谢任何建议。否则,我将辞职,将 ProxyMethod 执行移到单独的线程中。

【问题讨论】:

    标签: delphi delphi-xe2 datasnap hung


    【解决方案1】:

    您已经确定了根本问题。您的查询在 UI 线程中运行,并在运行时阻止该线程。不能发生 UI 更新,不能触发计时器消息等。

    我认为最好的解决方案是将 ProxyMethods 的执行移至单独的线程。但是,必须有一个现有的解决方案——因为相关的hung应用问题似乎是一个常见投诉。我只是找不到解决方案。

    您已经找到了解决问题的唯一方法。您必须在 UI 线程以外的线程中运行长时间运行的查询。

    【讨论】:

    • 我就是这么想的。您会认为随着 DataSnap 已经存在了多长时间,会有一些内置的东西来处理 UI 繁忙状态或其他东西...... DataSnap 也缺少一个回调来提示 UI 已经传输了多少数据,因此 UI 可以为大型数据检索/推送实现进度指示器。
    • (将其标记为“正确答案”,因为解决方案可以采取任何形式——下面的一个只是可能性。太糟糕了,您不能将多个“一个”答案标记为“正确'。)
    【解决方案2】:

    如果有人想知道,该解决方案实施起来相当简单。我们现在有一个工作已用时间时钟 [0:00],只要客户端应用程序等待 DataSnap 服务器为请求提供服务,它就会递增。本质上,这就是我们所做的。 (特别感谢那些分享他们的解决方案的人——这有助于指导我的思考。

    服务器生成的类(ProxyMethods)必须在 VCL 线程中创建,但在单独的线程中执行。为此,我们创建了一个 ProxyMethods 包装类和一个 ProxyMehtods 线程类(所有这些都是为本示例设计的,但仍说明了流程):

    ProxyMethods.pas

    ...
    type
      TServerMethodsClient = class(TDSAdminClient)
      private
        FGetDataCommand: TDBXCommand;
      public
        ...
        function GetData(Param1: string; Param2: string): string;
        ...
      end;
    

    ProxyWrapper.pas

    ...
    type
      TServerMethodsWrapper = class(TServerMethodsClient)
      private
        FParam1: string;
        FParam2: string;
        FResult: string;
      public
        constructor Create; reintroduce;
        procedure GetData(Param1: string; Param2: string);
        procedure _Execute;
        function GetResult: string;
      end;
    
      TServerMethodsThread = class(TThread)
      private
        FServerMethodsWrapper: TServerMethodsWrapper;
      protected
        procedure Execute; override;
      public
        constructor Create(ServerMethodsWrapper: TServerMethodsWrapper);
      end;
    
    implementation
    
    constructor TServerMethodsWrapper.Create;
    begin
      inherited Create(ASQLServerConnection.DBXConnection, True);
    end;
    
    procedure TServerMethodsWrapper.GetData(Param1: string; Param2: string);
    begin
      FParam1 := Param1;
      FParam2 := Param2;
    end;
    
    procedure TServerMethodsWrapper._Execute;
    begin
      FResult := inherited GetData(FParam1, FParam2);
    end;
    
    function TServerMethodsWrapper.GetResult: string;
    begin
      Result := FResult;
    end;
    
    constructor TServerMethodsThread.Create(ServerMethodsWrapper: TServerMethodsWrapper);
    begin
      FServerMethodsWrapper := ServerMethodsWrapper;
      FreeOnTerminate := False;
      inherited Create(False);
    end;
    
    procedure TServerMethodsThread.Execute;
    begin
      FServerMethodsWrapper._Execute;
    end;
    

    可以看到我们将ProxyMethod的执行分为两步。第一步是将参数的值存储在私有变量中。这允许_Execute() 方法在执行实际的 ProxyMethods 方法时拥有它需要知道的一切,其结果存储在FResult 中以供以后检索。

    如果 ProxyMethods 类有多个函数,您可以轻松地包装每个方法并在调用该方法以设置私有变量时设置一个内部变量(例如,FProcID)。这样_Execute() 方法可以使用FProcID 来知道要执行哪个ProxyMethod...

    您可能想知道为什么线程不释放自己。原因是当线程自己清理时,我无法消除错误“Thread Error: The handle is invalid (6)”。

    调用包装类的代码如下所示:

    var
      smw: TServerMethodsWrapper;
      val: string;
    begin
      ...
      smw := TServerMethodsWrapper.Create;
      try
        smw.GetData('value1', 'value2');
        // start timer here
        with TServerMethodsThread.Create(smw) do
        begin
          WaitFor;
          Free;
        end;
        // stop / reset timer here
        val := smw.GetResult;
      finally
        FreeAndNil(smw);
      end;
      ...
    end;
    

    WaitFor 暂停代码执行,直到 ProxyMethods 线程完成。这是必要的,因为smw.GetResult 在线程完成执行之前不会返回所需的值。在代理执行线程忙时使经过时间时钟 [0:00] 递增的关键是使用TJvThreadTimer 来更新 UI。即使 ProxyMethod 在单独的线程中执行,TTimer 也不起作用,因为 VCL 线程正在等待 WaitFor,因此在 WaitFor 完成之前,TTimer.OnTimer() 不会执行。

    从信息上看,TJvTheadTimer.OnTimer() 代码如下所示,它更新了应用程序的状态栏:

    var
      sec: Integer;
    begin
      sec := DateUtils.SecondsBetween(Now, FBusyStart);
      StatusBar1.Panels[0].Text := Format('%d:%.2d', [sec div 60, sec mod 60]);
      StatusBar1.Repaint;
    end;
    

    【讨论】:

      【解决方案3】:

      使用上述想法,我制作了一个适用于所有类的简单解决方案(自动)。我创建了 TThreadCommand 和 TCommandThread 如下:

         TThreadCommand = class(TDBXMorphicCommand)
          public
            procedure ExecuteUpdate; override;
            procedure ExecuteUpdateAsync;
          end;
      
          TCommandThread = class(TThread)
             FCommand: TDBXCommand;
          protected
            procedure Execute; override;
          public
            constructor Create(cmd: TDBXCommand);
          end;
      
      
      
          { TThreadCommand }
      
          procedure TThreadCommand.ExecuteUpdate;
          begin
            with TCommandThread.Create( Self ) do
            try
              WaitFor;
            finally
              Free;
            end;
          end;
      
          procedure TThreadCommand.ExecuteUpdateAsync;
          begin
            inherited ExecuteUpdate;
          end;
      
          { TCommandThread }
      
          constructor TCommandThread.Create(cmd: TDBXCommand);
          begin
            inherited Create(True);
            FreeOnTerminate := False;
            FCommand := cmd;
            Resume;
          end;
      
          procedure TCommandThread.Execute;
          begin
            TThreadCommand(FCommand).ExecuteUpdateAsync;
          end;
      

      然后改Data.DBXCommon.pas:

      function TDBXConnection.DerivedCreateCommand: TDBXCommand; 
      begin    
         //Result:= TDBXMorphicCommand.Create (FDBXContext, Self);    
         Result:= TThreadCommand.Create (FDBXContext, Self); 
      end;
      

      谢谢,现在我可以使用服务器回调更新 UI。

      【讨论】:

      • 您是如何强制编译器使用您修改后的 Data.DBXCommand.pas 的?每次我尝试修改其中一个 DBX 框架文件时,它总是绕过我的更改——即使我将修改后的文件直接放在项目文件夹中。
      • 通过将修改后的 Data.DBXCommand.pas 放入您的项目文件夹中。
      【解决方案4】:

      你是如何强制编译器使用你修改的 Data.DBXCommand.pas?

      通过将修改后的 Data.DBXCommand.pas 放入您的项目文件夹中。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-01-27
        • 1970-01-01
        • 1970-01-01
        • 2016-06-25
        相关资源
        最近更新 更多