Basically, what I need to do is this:
Show the user a "Please wait ..." form (lets call it waitForm
) on top of the main form, execute http methods (get and post), and close the waitForm
after I get the http response.
Since the http post method communicates with a physical device, it takes a while for the response to return (which is why I'm doing this in a worker thread, so the main thread doesn't appear to be "not responding").
I've set the http timeout to 1 minute. I've tried using an anonymous thread to execute the http methods so that the application doesn't go into "not responding", and so far its working as intended.
My problem here is that I need to use that response string further into the application after the thread is done. This is my code so far:
function TForm1.test: string;
var
ReqStream, ResStream: TStringStream;
http: TIdhttp;
command, commandName: string;
begin
TThread.CreateAnonymousThread(
procedure
begin
TThread.Synchronize(nil,
procedure
begin
waitForm.Label1.Caption := 'Please wait ...';
waitForm.BitBtn1.Enabled := False;
waitForm.FormStyle := fsStayOnTop;
waitForm.Show;
end);
try
http := TIdHTTP.Create(nil);
ReqStream := TStringStream.Create('', TEncoding.UTF8);
ResStream := TStringStream.Create('', TEncoding.UTF8);
try
command := '{"Amount":"69.00","TerminalName":"SIMULATE","omitSlipPrintingOnEFT":"1"}';
commandName := 'sale';
http.Request.ContentType := 'application/json';
http.Request.CharSet := 'utf-8';
http.Request.ContentEncoding := 'utf-8';
http.Request.Accept := 'application/json';
http.Request.Connection := 'keep-alive';
http.ConnectTimeout := 60000;
http.ReadTimeout := 60000;
ReqStream.WriteString(command);
ReqStream.Position := 0;
try
http.Post('http://localhost:5555/' + CommandName, ReqStream, ResStream);
http.Disconnect;
self.result := ResStream.DataString; // I know this can't be written like that
except
//
end;
finally
FreeAndNil(ReqStream);
FreeAndNil(http);
end;
finally
TThread.Synchronize(nil,
procedure
begin
waitForm.FormStyle := fsNormal;
waitForm.BitBtn1.Enabled := True;
waitForm.Close;
end);
end;
end).Start;
ShowMessage(result); // this is where I call next procedure to parse the response.
end;
After we get a result from the test()
function, I need to parse it and use it further in the application. fsStayOnTop
and disabling the buttons is the only solution that I have found to discourage the user from interacting with the main form since .ShowModal
is not an option because it blocks the function from continuing even with Synchronize()
(am I wrong here?).
I've also tried using ITask
and IFuture<string>
, but I can't seem to make it work as intended. Maybe I should be using anonymous functions.
If you want TForm1.test()
to work synchronously while using a worker thread internally, you will have to wait for that worker thread to finish before you can exit from your function. That will also allow your anonymous procedure to capture a local variable for it to write to, and then you can assign that variable to the function's Result
after the thread is finished.
Try something more like this:
function TForm1.test: string;
var
myThread: TThread;
response: string;
begin
waitForm.Label1.Caption := 'Please wait ...';
waitForm.BitBtn1.Enabled := False;
waitForm.FormStyle := fsStayOnTop;
waitForm.Show;
try
myThread := TThread.CreateAnonymousThread(
procedure
var
http: TIdHTTP;
ReqStream: TStringStream;
command, commandName: string;
begin
command := '{"Amount":"69.00","TerminalName":"SIMULATE","omitSlipPrintingOnEFT":"1"}';
commandName := 'sale';
http := TIdHTTP.Create(nil);
try
http.Request.ContentType := 'application/json';
http.Request.CharSet := 'utf-8';
http.Request.Accept := 'application/json';
http.Request.Connection := 'close';
http.ConnectTimeout := 60000;
http.ReadTimeout := 60000;
ReqStream := TStringStream.Create(command, TEncoding.UTF8);
try
response := http.Post('http://localhost:5555/' + CommandName, ReqStream);
finally
ReqStream.Free;
end;
finally
http.Free;
end;
end
);
try
myThread.FreeOnTerminate := False;
myThread.Start;
myThread.WaitFor;
{ alternatively...
var H: array[0..1] of THandle;
H[0] := myThread.Handle;
H[1] := Classes.SyncEvent;
repeat
case MsgWaitForMultipleObjects(2, H, False, INFINITE, QS_ALLINPUT) of
WAIT_OBJECT_0 + 0: Break;
WAIT_OBJECT_0 + 1: CheckSynchronize;
WAIT_OBJECT_0 + 2: Application.ProcessMessages;
WAIT_FAILED : RaiseLastOSError;
end;
until False;
}
finally
if myThread.FatalException <> nil then
begin
//...
end;
myThread.Free;
end;
finally
waitForm.FormStyle := fsNormal;
waitForm.BitBtn1.Enabled := True;
waitForm.Close;
end;
Result := response;
ShowMessage(Result); // this is where I call next procedure to parse the response.
end
Otherwise, you should break up your logic to make TForm1.test()
work asynchronously instead, and let it notify your code when the thread is finished and the response is available, eg:
procedure TForm1.do_test;
var
myThread: TThread:
begin
myThread := TThread.CreateAnonymousThread(
procedure
var
http: TIdHTTP;
ReqStream: TStringStream;
command, commandName, response: string;
begin
command := '{"Amount":"69.00","TerminalName":"SIMULATE","omitSlipPrintingOnEFT":"1"}';
commandName := 'sale';
http := TIdHTTP.Create(nil);
try
http.Request.ContentType := 'application/json';
http.Request.CharSet := 'utf-8';
http.Request.Accept := 'application/json';
http.Request.Connection := 'close';
http.ConnectTimeout := 60000;
http.ReadTimeout := 60000;
ReqStream := TStringStream.Create(command, TEncoding.UTF8);
try
response := http.Post('http://localhost:5555/' + CommandName, ReqStream);
finally
ReqStream.Free;
end;
finally
http.Free;
end;
TThread.Queue(nil,
procedure
begin
Self.HandleResponse(response);
end);
end;
end
);
myThread.OnTerminate := ThreadTerminated;
waitForm.Label1.Caption := 'Please wait ...';
waitForm.BitBtn1.Enabled := False;
waitForm.FormStyle := fsStayOnTop;
waitForm.Show;
try
myThread.Start;
except
ThreadTerminated(nil);
raise;
end;
end;
procedure TForm1.ThreadTerminated(Sender: TObject);
begin
waitForm.FormStyle := fsNormal;
waitForm.BitBtn1.Enabled := True;
waitForm.Close;
if (Sender <> nil) and (TThread(Sender).FatalException <> nil) then
begin
//...
end;
end;
procedure TForm1.HandleResponse(const Response: string);
begin
ShowMessage(Response); // this is where I call next procedure to parse the response.
end;
That being said...
I'm doing this in a worker thread, so the main thread doesn't appear to be "not responding"
While that is a good idea in general, I just want to point out that Indy does have a TIdAntiFreeze
component to address this exact issue. You can leave your test()
function to work synchronously without using a worker thread, letting TIdAntiFreeze
pump the main message queue while TIdHTTP
is blocking the main thread.
For example:
function TForm1.test: string;
var
http: TIdHTTP;
ReqStream: TStringStream;
command, commandName: string;
begin
command := '{"Amount":"69.00","TerminalName":"SIMULATE","omitSlipPrintingOnEFT":"1"}';
commandName := 'sale';
waitForm.Label1.Caption := 'Please wait ...';
waitForm.BitBtn1.Enabled := False;
waitForm.FormStyle := fsStayOnTop;
waitForm.Show;
// place a TIdAntiFreeze onto your Form, or
// create it dynamically here, either way will work...
try
http := TIdHTTP.Create(nil);
try
http.Request.ContentType := 'application/json';
http.Request.CharSet := 'utf-8';
http.Request.Accept := 'application/json';
http.Request.Connection := 'close';
http.ConnectTimeout := 60000;
http.ReadTimeout := 60000;
ReqStream := TStringStream.Create(command, TEncoding.UTF8);
try
Result := http.Post('http://localhost:5555/' + CommandName, ReqStream);
finally
ReqStream.Free;
end;
finally
http.Free;
end;
finally
waitForm.FormStyle := fsNormal;
waitForm.BitBtn1.Enabled := True;
waitForm.Close;
end;
ShowMessage(Result); // this is where I call next procedure to parse the response.
end;