Обновление пользовательского интерфейса клиента во время ожидания DataSnap

Я создал приложение MDI Delphi в Delphi XE2, которое подключается к серверу DataSnap через компонент TSQLConnection (driver = 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 приводит к тому, что приложение зависает («Не отвечает» в строке заголовка).

Я думаю, что лучшим решением будет перенести выполнение ProxyMethods в отдельный поток. Однако должно существовать существующее решение, потому что связанная с этим проблема с зависшим приложением кажется распространенной жалобой. Я просто не могу найти решение.

Любые предложения приветствуются. В противном случае я смирюсь с переносом исполнения ProxyMethod в отдельный поток.


person James L.    schedule 01.03.2012    source источник


Ответы (4)


Вы определили основную проблему. Ваш запрос выполняется в потоке пользовательского интерфейса и блокирует этот поток во время его выполнения. Не может происходить никаких обновлений пользовательского интерфейса, не могут срабатывать сообщения таймера и т. д.

Я думаю, что лучшим решением будет перенести выполнение ProxyMethods в отдельный поток. Однако должно существовать существующее решение, потому что связанная с этим проблема с зависшим приложением кажется распространенной жалобой. Я просто не могу найти решение.

Вы уже нашли единственное решение проблемы. Вы должны выполнить свой длительный запрос в потоке, отличном от потока пользовательского интерфейса.

person David Heffernan    schedule 01.03.2012
comment
Это то, о чем я думал. Вы могли бы подумать, что с учетом того, как долго существует DataSnap, что должно быть что-то встроенное для обработки состояния занятости пользовательского интерфейса или что-то в этом роде... В DataSnap также отсутствует обратный вызов, чтобы указать пользовательскому интерфейсу, сколько данных было передано, чтобы пользовательский интерфейс мог реализовать индикатор прогресса для извлечения/передачи больших данных. - person James L.; 02.03.2012
comment
(Оставьте это отмеченным как «правильный ответ», потому что решение может принимать любую форму — приведенное ниже просто возможно. Жаль, что вы не можете пометить более «одного» ответа как «правильный».) - person James L.; 05.03.2012

В случае, если кто-то хочет знать, решение было довольно простым в реализации. Теперь у нас есть рабочие часы прошедшего времени [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 выполнять...

Вы можете удивиться, почему Нить не освобождается. Причина в том, что мне не удалось устранить ошибку "Ошибка потока: дескриптор недействителен (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 для обновления пользовательского интерфейса. TTimer не работает, даже если ProxyMethod выполняется в отдельном потоке, потому что поток 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;
person James L.    schedule 02.03.2012

Используя приведенную выше идею, я сделал простое решение, которое будет работать для всех классов (автоматически). Я создал 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;

Благодаря этому теперь я могу обновлять пользовательский интерфейс с обратным вызовом сервера.

person Krassimir Koychev    schedule 04.10.2012
comment
Как вы заставили компилятор использовать ваш измененный Data.DBXCommand.pas? Каждый раз, когда я пытался изменить один из файлов DBX Framework, он всегда обходил мои изменения, даже если я помещал измененный файл непосредственно в папку проекта. - person James L.; 04.10.2012
comment
Поместив измененный Data.DBXCommand.pas в папку вашего проекта. - person Krassimir Koychev; 05.10.2012

Как вы заставили компилятор использовать ваш измененный Data.DBXCommand.pas?

Поместив измененный Data.DBXCommand.pas в папку вашего проекта.

person Krassimir Koychev    schedule 05.10.2012