Updating client UI while waiting for DataSnap
-
15-11-2019 - |
Domanda
I created a MDI Delphi app in Delphi XE2 that connects to a DataSnap server via a TSQLConnection
component (driver = datasnap
). A right-click on the TSQLConnection
at design-time lets me generate the DataSnap client classes (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. The two approaches that I have tried, but don't work are:
Method #1
Use a
TTimer
with a 1 second interval that updates the elapsed time clock while a ProxyMethod is being execute. I enable the timer just before calling the ProxyMethod. While the ProxyMethod is running, theOnTimer
event doesn't fire -- a breakpoint in the code is never hit.
Method #2
Same as Method #1, except the timer is a
TJvThreadTimer
. While the ProxyMethod is running, theOnTimer
event fires, but theOnTimer
code doesn't get execute until after the ProxyMethod completes. This is evident because a breakpoint in theOnEvent
code gets hit in rapid succession after the ProxyMethod completes -- like theOnTimer
events have all been queued in the main VCL thread.
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).
I think the best solution is to move the execution of the ProxyMethods to a separate thread. 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.
Soluzione
You have identified the fundamental problem. Your query is running in the UI thread and blocks that thread whilst it runs. No UI updates can occur, timer messages cannot fire etc.
I think the best solution is to move the execution of the ProxyMethods to a separate thread. 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.
Altri suggerimenti
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. 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. 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.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;
You can see that we split the execution of the ProxyMethod into two steps. 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.
If the ProxyMethods class has multiple functions, you easily wrap each method and set an internal variable (e.g., FProcID
) when the method is called to set the private variables. This way the _Execute()
method could use FProcID
to know which ProxyMethod to execute...
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.
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. This is necessary because smw.GetResult
won't return the needed value until the thread is done executing. 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. 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.
Informationally, the TJvTheadTimer.OnTimer()
code looks like this, which updates the application's status bar:
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;
Using the above idea, I made a simple solution that will work for all classes (automatically). I created TThreadCommand and TCommandThread as follows:
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:
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.
How did you force the compiler to use your modified Data.DBXCommand.pas?
By putting modified Data.DBXCommand.pas in your project folder.