2

I'm currently trying to create a single unified unit for the purposes of threading. I've developed a web API which I intend to use with multiple applications. My intent with the thread is to get the data from the API without using the main application thread. (i.e. offloading into a thread for the purposes of good UX). The output will be JSON retrieved using a POST request to the API, and parsing will be performed from there.

I've got the unit all written up but I'm struggling to figure out how I retrieve the output in my actual application/transfer the JSON from the thread to the application.

I think what I need is to add an 'output variable' somewhere, but I'm unsure how I do that when this unit is standalone - Normally I'd write it all in one unit and of course I could output it to a global variable there.

Here's my thread unit;

unit TCAPI_ThreadLib;

interface

uses
  FMX.Types, FMX.Objects, {FMX.Dialogs,} IdHTTP, System.Classes;

type
  TTCAPI_GetJson_Thread = class(TThread)
    private
      APIHTTP : TIdHTTP;
      FApiEmail, FApiId, FApiPassword, FCommand, FCommandParams : String;
    protected
      procedure Execute; override;
      procedure Finished;
    public
      constructor Create(SEmail, SApiID, SAPIPassword : String);
      destructor Destroy; override;
    var
      FDevGUID, FDevFN, FDevPlatform, FDevModel : String;
      FApiDataResult : String;
    Const
      APIBase : String = 'http://my-web-api-url.com/api.php';
    end;

implementation

{ TTCAPI_GetJson_Thread }
constructor TTCAPI_GetJson_Thread.Create(SEmail, SApiID, SAPIPassword: String);
begin
  inherited Create(True);
  APIHTTP := TIdHTTP.Create(nil);
  FApiEmail := SEmail;
  FApiID := SApiID;
  FApiPassword := SApiPassword;
  FreeOnTerminate := True;
end;

destructor TTCAPI_GetJson_Thread.Destroy;
begin
  APIHttp.Free;
  inherited;
end;

procedure TTCAPI_GetJson_Thread.Execute;
var
  RcvStrm : TStringStream;
  TmpClass : String;
  ApiCommand : String;
  _Params : TStringList;
begin
  inherited;
  RcvStrm := TStringStream.Create;
  _Params := TStringList.Create;
  _Params.Add('API_ID='+FApiID);
  _Params.Add('API_EMAIL='+FApiEmail);
  _Params.Add('API_PASSWORD='+FApiPassword);
  APICommand := APIBase;

  try
    APIHTTP.Post(APICommand, _Params, RcvStrm);
    FApiDataResult := RcvStrm.DataString;
  finally
    RcvStrm.Free;
    _Params.Free;
  end;
  Synchronize(Finished);
end;

procedure TTCAPI_GetJson_Thread.Finished;
begin
  // ShowMessage(FApiDataResult);
end;

end.

If I add FMX.Dialogs and use a ShowMessage(FApiDataResult) call in the thread unit, it shows the JSON in a message box - I need to figure out how to get that JSON into a variable within the scope of my applications main unit/form and how to actually tell the application that the thread has finished so that I can begin parsing.

I'm sure it's simple to do, but I'd appreciate any help!

Scott P
  • 1,462
  • 1
  • 19
  • 31
  • Unrelated to your issue, but you don't free _Params – David A Jan 17 '14 at 02:01
  • You must use `TThread.Synchronize` or `TThread.Queue` to interact with the GUI. http://www.uweraabe.de/Blog/2011/01/30/synchronize-and-queue-with-parameters/ – Sir Rufo Jan 17 '14 at 02:53
  • I understand I should use Synchronize, and you can see I'm calling `Synchronize(Finished)` above which does give me the data in the context of the thread, but I'm unsure how I go about getting this data into the context of the main application thread without copying the thread unit code into the main unit itself. – Scott P Jan 17 '14 at 04:26

1 Answers1

2

Thanks to a little more research, I found a solution involving callbacks. Posting for future reference (and of course to share the knowledge for others seeking an answer);

unit TCAPI_ThreadLib;

interface

uses
  FMX.Types, FMX.Objects, IdHTTP, System.Classes;

type
  TDataCallback = Procedure(const SJson : String) of Object;

  TTCAPI_GetJson_Thread = class(TThread)
    constructor Create(SEmail, SApiID, SAPIPassword : String; CallBack : TDataCallback); overload;
    procedure Execute; override;
    destructor Destroy; override;

    private
      APIHTTP : TIdHTTP;
      FApiEmail, FApiId, FApiPassword, FCommand, FCommandParams : String;
      FApiDataResult : String;
      FDataProc : TDataCallback;
      FDevGUID, FDevFN, FDevPlatform, FDevModel : String;
      Procedure DoCallback;
    public
      Const
        APIBase : String = 'http://my-web-api-url.com/api.php';
    end;

implementation

{ TTCAPI_GetJson_Thread }
procedure TTCAPI_GetJson_Thread.DoCallback;
begin
  if Assigned(FDataProc) then FDataProc(FApiDataResult);
end;

constructor TTCAPI_GetJson_Thread.Create(SEmail, SApiID, SAPIPassword: String; CallBack : TDataCallback);
begin
  inherited Create(True);
    APIHTTP := TIdHTTP.Create(nil);
    FApiEmail := SEmail;
    FApiID := SApiID;
    FApiPassword := SApiPassword;
    FDataProc := Callback;
end;

destructor TTCAPI_GetJson_Thread.Destroy;
begin
  APIHttp.Free;
  inherited;
end;

procedure TTCAPI_GetJson_Thread.Execute;
var
  RcvStrm : TStringStream;
  TmpClass : String;
  ApiCommand : String;
  _Params : TStringList;
begin
  inherited;
  RcvStrm := TStringStream.Create;
  _Params := TStringList.Create;
  _Params.Add('API_ID='+FApiID);
  _Params.Add('API_EMAIL='+FApiEmail);
  _Params.Add('API_PASSWORD='+FApiPassword);
  APICommand := APIBase;

  try
    APIHTTP.Post(APICommand, _Params, RcvStrm);
    FApiDataResult := RcvStrm.DataString;
  finally
    RcvStrm.Free;
    _Params.Free;
  end;
  Synchronize(DoCallback);
end;

end.

Within my main application unit;

Procedure TForm1.DataCallback(Const S: String);
begin
  ShowMessage(S);
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  DataThread : TTCAPI_GetJson_Thread;
begin    
  With TTCAPI_GetJson_Thread.Create('email@example.com','apiid','password', DataCallback) do
  begin
    FreeOnTerminate := True;
    Start;
  end;    
end;

That's allowed me to keep the threading unit completely separate from my application units and it's a real strong base for me to build future units on.

Scott P
  • 1,462
  • 1
  • 19
  • 31
  • 2
    For future reuse of the unit in (potential) non-VCL applications note that Synchronize() can be omitted in console applications and other cases (for example service type applications) - see http://stackoverflow.com/questions/10689159/is-it-dangerous-to-use-synchronize-in-a-non-vcl-application – mjn Jan 17 '14 at 06:36