Client Interface Update while Waiting for DataSnap

I created a Delphi MDI application in Delphi XE2 that connects to a DataSnap server through the TSQLConnection component ( driver = datasnap ). Right-clicking on TSQLConnection during development allows me to generate the DataSnap client classes (ProxyMethods).

My goal is to have the elapsed hours [0:00] on the client side, which show how long the DataSnap request for maintenance has been running, updated every 1 second. Two approaches I tried but do not work:

Method # 1

Use a TTimer at 1 second intervals, which updates expired timers during ProxyMethod. I turn on the timer just before calling ProxyMethod. While ProxyMethod is running, the OnTimer event OnTimer not fire - a breakpoint in the code never hits.

Method # 2

Same as method # 1 except for the TJvThreadTimer timer. While ProxyMethod is running, the OnTimer event OnTimer , but the OnTimer code OnTimer not OnTimer until ProxyMethod completes. This can be seen because the breakpoint in the OnEvent code gets a quick hit after the ProxyMethod completes - how all OnTimer events were queued in the main VCL.

In addition, by clicking anywhere in the client application while the slow ProxyMethod is running, the application appears to hang (the message "Do not respond" is displayed in the header).

I think the best solution is to move ProxyMethods execution to a separate thread. However, there must be an existing solution, because the related problem with the application looks like it will be a common complaint. I just can't find a solution.

Any suggestions are welcome. Otherwise, I will humble myself to move ProxyMethod execution to a separate thread.

+4
source share
4 answers

You have identified a fundamental problem. Your request is launched in the user interface thread and blocks this thread during its launch. There are no user interface updates, timer messages cannot start, etc.

I think the best solution is to move ProxyMethods execution to a separate thread. However, there must be an existing solution, because the related problem with the application looks like it will be a common complaint. I just can't find a solution.

You have already found the only solution to the problem. You must run your long-term request in a thread other than the user interface thread.

+4
source

In case someone wants to know, the solution was quite simple to implement. Now we have the working elapsed time hours [0:00] that increase anytime the client application expects the DataSnap server to serve the request. In essence, this is what we have done. (Special thanks to those who share their decisions, which helped me figure it out.)

Server-generated classes (ProxyMethods) must be created in the VCL thread, but run in a separate thread. To do this, we created the wrapper class ProxyMethods and the class of streams ProxyMehtods (all this is far-fetched for this example, but it still illustrates the stream):

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 divided the execution of ProxyMethod into two steps. The first step is to save the parameter values ​​in private variables. This allows the _Execute() method to have everything that it needs to know when it executes the actual ProxyMethods method, the result of which is stored in FResult for later retrieval.

If the ProxyMethods class has several functions, you can easily transfer each method and set an internal variable (for example, FProcID ) when you call the method to set private variables. Thus, the _Execute() method could use FProcID to find out which ProxyMethod to execute ...

You may wonder why Thread does not free itself. The reason is that I could not resolve the error "Stream error: descriptor is invalid (6)" when the stream did its own cleanup.

The code calling the shell class is as follows:

 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 pauses code execution until the ProxyMethods thread exits. This is necessary because smw.GetResult will not return the required value until the thread is executed. The key to increasing the elapsed time interval [0:00] while the proxy server thread is busy is to use TJvThreadTimer to update the user interface. A TTimer does not work even when running ProxyMethod in a separate thread, because the VCL thread is waiting for WaitFor , so TTimer.OnTimer() not executed until WaitFor is executed.

Informatively, the TJvTheadTimer.OnTimer() code looks like it updates the application 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; 
+3
source

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 to this, I can now update the server callback interface.

+3
source

How did you make the compiler use the modified Data.DBXCommand.pas?

By placing the modified Data.DBXCommand.pas in the project folder.

0
source

Source: https://habr.com/ru/post/1399256/


All Articles