在等待DataSnap时更新客户端UI

时间:2012-03-01 20:30:21

标签: delphi delphi-xe2 datasnap hung

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

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

方法#1

  

使用 TTimer ,间隔为1秒,在执行ProxyMethod时更新已用时间时钟。我在调用ProxyMethod之前启用了计时器。当ProxyMethod正在运行时, OnTimer 事件不会触发 - 代码中的断点永远不会被触发。

方法#2

  

与方法#1相同,但计时器为 TJvThreadTimer 。在ProxyMethod运行时, OnTimer 事件会触发,但 OnTimer 代码在ProxyMethod完成之后才会执行。这很明显是因为 OnEvent 代码中的断点在ProxyMethod完成后会快速连续命中 - 就像 OnTimer 事件一样在主VCL主题中排队。

此外,在慢速ProxyMethod运行时单击客户端应用程序上的任意位置会使应用程序显示为挂起(标题栏中显示“Not Responding”)。

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

任何建议都表示赞赏。否则,我将辞职,将ProxyMethod执行移动到一个单独的线程中。

4 个答案:

答案 0 :(得分:4)

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

  

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

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

答案 1 :(得分:3)

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

必须在VCL线程中创建服务器生成的类(ProxyMethods),但必须在单独的线程中执行。为此,我们创建了一个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 ...

你可能想知道为什么线程不会释放自己。原因是因为当线程自行清理时,我无法消除错误“线程错误:句柄无效(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,因此TTimer.OnTimer()WaitFor之前不会执行完成了。

从信息上讲,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;

答案 2 :(得分: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。

答案 3 :(得分:0)

您是如何强制编译器使用您修改的Data.DBCommand.pass>的

将修改后的Data.DBCommand.pas放入项目文件夹中。