简体   繁体   English

在等待DataSnap时更新客户端UI

[英]Updating client UI while waiting for DataSnap

I created a MDI Delphi app in Delphi XE2 that connects to a DataSnap server via a TSQLConnection component ( driver = datasnap ). 我在Delphi XE2中创建了一个MDI Delphi应用程序,它通过TSQLConnection组件( driver = datasnap )连接到DataSnap服务器。 A right-click on the TSQLConnection at design-time lets me generate the DataSnap client classes (ProxyMethods). 在设计时右键单击TSQLConnection可以生成DataSnap客户端类(ProxyMethods)。

My goal is to have an elapsed time clock [0:00] on the client side that shows how long a DataSnap request takes to service, updated every 1 second. 我的目标是在客户端有一个经过时间的时钟[0:00],显示DataSnap请求服务的时间,每1秒更新一次。 The two approaches that I have tried, but don't work are: 我尝试过但不起作用的两种方法是:

Method #1 方法#1

Use a TTimer with a 1 second interval that updates the elapsed time clock while a ProxyMethod is being execute. 使用具有1秒间隔的TTimer ,在执行ProxyMethod时更新已用时间时钟。 I enable the timer just before calling the ProxyMethod. 我在调用ProxyMethod之前启用了计时器。 While the ProxyMethod is running, the OnTimer event doesn't fire -- a breakpoint in the code is never hit. 在ProxyMethod运行时, OnTimer事件不会触发 - 代码中的断点永远不会被触发。

Method #2 方法#2

Same as Method #1, except the timer is a TJvThreadTimer . 与方法#1相同,但计时器是TJvThreadTimer While the ProxyMethod is running, the OnTimer event fires, but the OnTimer code doesn't get execute until after the ProxyMethod completes. 在ProxyMethod运行时,会触发OnTimer事件,但是在ProxyMethod完成之后, OnTimer代码才会执行​​。 This is evident because a breakpoint in the OnEvent code gets hit in rapid succession after the ProxyMethod completes -- like the OnTimer events have all been queued in the main VCL thread. 这很明显,因为在ProxyMethod完成后, OnEvent代码中的断点会快速连续命中 - 就像OnTimer事件已经在主VCL线程中排队一样。

Furthermore, clicking anywhere on the client app while a slow ProxyMethod is running makes the app appear to be hung ("Not Responding" appears in title-bar). 此外,在慢速ProxyMethod运行时单击客户端应用程序上的任何位置会使应用程序显示为挂起(标题栏中显示“Not Responding”)。

I think the best solution is to move the execution of the ProxyMethods to a separate thread. 我认为最好的解决方案是将ProxyMethods的执行移动到一个单独的线程。 However, there must be an existing solution -- because the related hung app issue seems like it would be a common complaint. 但是, 必须有一个现有的解决方案 - 因为相关的挂起应用程序问题似乎是一个常见的抱怨。 I just can't find the solution. 我只是找不到解决方案。

Any suggestions are appreciated. 任何建议表示赞赏。 Otherwise, I will resign myself to moving the ProxyMethod execution into a separate thread. 否则,我将辞职,将ProxyMethod执行移动到一个单独的线程中。

You have identified the fundamental problem. 您已经确定了根本问题。 Your query is running in the UI thread and blocks that thread whilst it runs. 您的查询正在UI线程中运行,并在运行时阻止该线程。 No UI updates can occur, timer messages cannot fire etc. 不会发生UI更新,计时器消息无法触发等。

I think the best solution is to move the execution of the ProxyMethods to a separate thread. 我认为最好的解决方案是将ProxyMethods的执行移动到一个单独的线程。 However, there must be an existing solution -- because the related hung app issue seems like it would be a common complaint. 但是, 必须有一个现有的解决方案 - 因为相关的挂起应用程序问题似乎是一个常见的抱怨。 I just can't find the solution. 我只是找不到解决方案。

You have already found the only solution to the problem. 您已经找到了解决问题的唯一方法。 You must run your long-running query in a thread other than the UI thread. 您必须在UI线程以外的线程中运行长时间运行的查询。

Using the above idea, I made a simple solution that will work for all classes (automatically). 使用上述想法,我做了一个简单的解决方案,适用于所有类(自动)。 I created TThreadCommand and TCommandThread as follows: 我创建了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;

And then changed Data.DBXCommon.pas: 然后更改了Data.DBXCommon.pas:

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

Thanks of that, now I can do update of UI with server callback. 多亏了,现在我可以用服务器回调来更新UI。

In case anyone wants to know, the solution was rather simple to implement. 如果有人想知道,解决方案实施起来相当简单。 We now have a working elapsed time clock [0:00] that increments anytime the client app is waiting for the DataSnap server to service a request. 我们现在有一个工作的经过时间[0:00],当客户端应用程序等待DataSnap服务器为请求提供服务时,该时钟会递增。 In essence, this is what we did. 从本质上讲,这就是我们所做的。 ( A special thanks to those who share their solutions -- which helped guide my thinking. ) 特别感谢那些分享他们解决方案的人 - 这有助于指导我的思考。

The server generated classes (ProxyMethods) must be created in the VCL thread, but executed in a separate thread. 必须在VCL线程中创建服务器生成的类(ProxyMethods),但是在单独的线程中执行。 To do this, we created a ProxyMethods wrapper class and a ProxyMehtods thread class (all of which is contrived for this example, but still it illustrates the flow): 为此,我们创建了一个ProxyMethods包装类和一个ProxyMehtods线程类(所有这些都是为这个例子设计的,但它仍然说明了流程):

ProxyMethods.pas ProxyMethods.pas

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

ProxyWrapper.pas 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;

You can see that we split the execution of the ProxyMethod into two steps. 您可以看到我们将ProxyMethod的执行分为两个步骤。 The first step is to store the values of the parameters in private variables. 第一步是将参数的值存储在私有变量中。 This allows the _Execute() method to have everything it needs to know when it executes the actual ProxyMethods method, whose result is stored in FResult for later retrieval. 这允许_Execute()方法在执行实际的ProxyMethods方法时拥有它需要知道的所有内容,其结果存储在FResult以供以后检索。

If the ProxyMethods class has multiple functions, you easily wrap each method and set an internal variable (eg, FProcID ) when the method is called to set the private variables. 如果ProxyMethods类具有多个函数,则在调用方法设置私有变量时,可以轻松地包装每个方法并设置内部变量(例如, FProcID )。 This way the _Execute() method could use FProcID to know which ProxyMethod to execute... 这样_Execute()方法可以使用FProcID来知道要执行哪个ProxyMethod ...

You may wonder why the Thread doesn't free itself. 你可能想知道为什么线程不会释放自己。 The reason is because I couldn't eliminate an error " Thread Error: The handle is invalid (6) " when the thread did its own cleanup. 原因是当线程做了自己的清理时,我无法消除错误“ 线程错误:句柄无效(6) ”。

The code that calls the wrapper class looks like this: 调用包装类的代码如下所示:

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;

The WaitFor suspends code execution until the ProxyMethods thread completes. WaitFor暂停代码执行,直到ProxyMethods线程完成。 This is necessary because smw.GetResult won't return the needed value until the thread is done executing. 这是必要的,因为在线程完成执行之前, smw.GetResult不会返回所需的值。 The key to making the elapsed time clock [0:00] increment while the proxy execution thread is busy is to use a TJvThreadTimer to update the UI. 在代理执行线程繁忙时使经过时间时钟[0:00]递增的关键是使用TJvThreadTimer来更新UI。 A TTimer doesn't work even with the ProxyMethod being executed in a separate thread because the VCL thread is waiting for the WaitFor , so the TTimer.OnTimer() doesn't execute until the WaitFor is done. 即使在单独的线程中执行ProxyMethod, TTimer也不起作用,因为VCL线程正在等待WaitFor ,因此TTimer.OnTimer()WaitFor完成之前不会执行。

Informationally, the TJvTheadTimer.OnTimer() code looks like this, which updates the application's status bar: 从信息TJvTheadTimer.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;

How did you force the compiler to use your modified Data.DBXCommand.pas? 你是如何强制编译器使用你修改过的Data.DBXCommand.pas的?

By putting modified Data.DBXCommand.pas in your project folder. 通过将修改后的Data.DBXCommand.pas放入项目文件夹中。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM