1

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.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
q1werty
  • 25
  • 7
  • Looks like you try to access waitForm from the thread which clearly isn't going to work. What happened when you debugged this? – David Heffernan Apr 07 '23 at 10:02
  • @DavidHeffernan Thank you for response David. If you are referring to finally block it does close the form, even though it is not within Synchronize procedure, I might rewrite that just in case. Keep in mind that this code I've provided is kept as simple as possible, and I showed only the important parts. Original code contains more but it is not currently relevant to what I am trying to achieve here. – q1werty Apr 07 '23 at 10:39
  • 1
    You should always access GUI from the main thread. Just because such code might seemingly work at one moment, does not mean it will not crash in the next. But, showing form is not related to the actual problem in your question so, you might want to remove that part and focus on the actual question you are asking. – Dalija Prasnikar Apr 07 '23 at 12:04
  • @DalijaPrasnikar Lets say that in finally block we are accessing main thread and closing waitForm with Synchronize. Exactly as you said, that is not what I am looking for, even tho it would be usefull to disable the interaction with main form with .ShowModal somehow. My question is how do I get return string from anonymous thread or if there is a way to do it at all without blocking main thread or putting in into not responsive state. – q1werty Apr 07 '23 at 12:12
  • In short, you cannot have a function that will return result of asynchronous operation. The code needs to be changed to use a callback instead. I don't have time to write full answer at the moment. – Dalija Prasnikar Apr 07 '23 at 12:51
  • @DalijaPrasnikar I was aware of that and I appreciate your answer. I was trying to make the function as compact and as easy to read and maintain as possible but I guess there is no workaround for that. I imagine you meant to rewrite the code by adding a separate TThread class with callback procedure? I look forward to your answer when you get the time. – q1werty Apr 07 '23 at 13:02
  • You can do a callback from the anon thread if you want. Or use an event to signal that the thread is done. – David Heffernan Apr 07 '23 at 13:13
  • @DavidHeffernan I believe I've tried that already and somehow it puts main thread into not responding state, I'm not sure why, was certainly doing something wrong. Could you use simple example of code to show me how to achieve that? Thanks in advance. – q1werty Apr 07 '23 at 13:24

2 Answers2

1

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;
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • on your async version, what is `Thread.OnTerminated := @ThreadTerminated;` ? it seems that ChatGPT is assisting you ) Btw, if the app is closed while thread is running, you will get a memory leak. – Marcodor Apr 07 '23 at 19:09
  • 1
    @Marcodor it was a typo, should have been `myThread.OnTerminate` instead. I have fixed it. And no, I do not (and would never) use ChatGPT. Do you think someone of my rep would not know that [doing so is banned](https://meta.stackoverflow.com/questions/421831/temporary-policy-chatgpt-is-banned)? – Remy Lebeau Apr 07 '23 at 19:13
  • well, it was a joke ) i know you good enough.. but even OnTerminate**d** := **@**ThreadTerminated; will not compile – Marcodor Apr 07 '23 at 19:14
  • @RemyLebeau Thank you for a well constructed answer Remy. Since the whole point is to prevent the user from interacting with main form while the http request is being executed, I can simply use TIdAntiFreeze (didn't know of this component), in order to make the code more clearer. I was trying to use worker thread just because of the "not responding" message, now I don't have to. – q1werty Apr 10 '23 at 07:40
  • @q1werty keeping the UI thread responsive (whether via thread or Antifreeze) means the user *can* interacting with the UI. So, make sure you *disable* the UI while your HTTP request is in PROGRESS, and then *re-enable* when finished. – Remy Lebeau Apr 10 '23 at 15:02
  • @RemyLebeau Of course, that was the easy part. Thank you for pointing it out. – q1werty Apr 11 '23 at 07:40
0

You can retrieve the thread execution result in a callback:

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
  private
    { Private declarations }
    Thread: TThread;
    ThreadResult: string;
    procedure OnThreadTerminate(Sender: TObject);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses
  Waits;

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
  Thread := TThread.CreateAnonymousThread(procedure
  var HttpResult: string;
  begin
    TThread.Synchronize(nil, procedure
    begin
      WaitForm.Show;
    end);
    try
      Sleep(4000); // Here do Your http request
      // raise Exception.Create('Http request error');
      HttpResult := 'Result from thread';
    except
      HttpResult := 'Something bad happened';
    end;
    TThread.Synchronize(nil, procedure
    begin
      ThreadResult := HttpResult;
      WaitForm.Close;
    end);
  end);
  Thread.OnTerminate := OnThreadTerminate;
  Thread.Start;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  Thread.Free;
end;

procedure TForm1.OnThreadTerminate(Sender: TObject);
begin
  Thread := nil;
  ShowMessage(ThreadResult);
end;

Note, there is no sense to put whole thread execution code in TThread.Synchronize. Synchronize execute the code in the main thread, so it will work like without thread.

This is just a proof of work, not sure I'd go with this design in a real scenario.

Marcodor
  • 4,578
  • 1
  • 20
  • 24
  • 1
    Just FYI, the `TThread.OnTerminate` event is fired via `TThread.Synchronize()`, so calling `Synchronize()` at the very end of the anonymous procedure is a little redundant. You could (and should) move `WaitForm.Close` into the `OnTerminate` handler instead (consider what happens if the thread raises an exception - the manual `Synchronize()` won't be called, but `OnTerminate` will still be fired). – Remy Lebeau Apr 07 '23 at 16:34
  • @RemyLebeau, I do not find it redundant, once showing the form happens in the same code block. Imagine the thread have a property TWaitFormClass. The entire life cycle is managed by the thread. On exceptions, best to handle them in context of the thread rather than closing the form in OnTerminate. – Marcodor Apr 07 '23 at 16:49