Jump to content
gorepj

Updating UI using Invoke

Recommended Posts

How can I update the UI from a task using invoke.  In my example below the memo is updated but with the wrong values

 

procedure TMyForm.TestTask(const Task: IOmniTask);
var
  i: Integer;
begin
 for I := 0 to 4 do
   Task.Invoke(procedure
     begin
       mmoLogs.Lines.Add('['+i.ToString+']');
     end
   );

end;

 

result is 

[4]

[4]

[4]

[4]

[4]

 

I need it to be 

[0]

[1]

[2]

[3]

[4]

 

How can I achieve this?

 

  • Thanks 1

Share this post


Link to post

Thank you for the reply but unfortunately this does not work either

 

procedure TMyFormTestTask(const Task: IOmniTask);
type
  TMyProc = reference to procedure;

 

function CaptureValue(Value: Integer): TMyProc;
  begin
    Result := procedure begin mmoLogs.Lines.add(Value.ToString); end;
  end;

var
  i: Integer;
  Proc: TMyProc;


begin

  for I := 0 to 9 do
    begin
      Proc := CaptureValue(i);
      Task.Invoke(procedure
      begin
        Proc();
      end
    );

 

//

Gives a result of
10
10
10
10
10
10
10
10
10
10

Share this post


Link to post

Works fine here.  I wrote a version with the threading removed to make it simpler to understand.

 

{$APPTYPE CONSOLE}

uses
  System.SysUtils;

function CaptureValue(Value: Integer): TProc;
begin
  Result :=
    procedure
    begin
      Writeln(Value);
    end;
end;

procedure Main;
var
  i, j: Integer;
  Procs: TArray<TProc>;
begin
  SetLength(Procs, 10);

  for i := 0 to 9 do
    Procs[i] :=
      procedure
      begin
        Writeln(i);
      end;
  for j := 0 to 9 do
    Procs[j]();


  for i := 0 to 9 do
    Procs[i] := CaptureValue(i);
  for j := 0 to 9 do
    Procs[j]();
end;

begin
  Main;
  Readln;
end.

Output is

10
10
10
10
10
10
10
10
10
10
0
1
2
3
4
5
6
7
8
9

 

Share this post


Link to post

Thank you very much.  Your code indeed works but not in the context of an omnithread invoke

  for I := 0 to 9 do
    begin
      SetLength(Procs,Length(Procs)+1);
      Procs := CaptureValue(i);
      Procs();  //This line works
      Task.invoke(procedure begin Procs(); end ); //This line Access Violation
    end;

 

 

Share this post


Link to post

Works fine in any context. If you notice though, I supplied a complete program. You only supplied snippets, the latest of which does not compile. Perhaps if you want help finding the bug in your code you could supply a complete but minimal reproduction, just as I did. Then it would be simple for us to find the mistake. 

Edited by David Heffernan

Share this post


Link to post

Thank you for your answers again.  The scenario doesn't lend itself to a Consol app.  So here is a test case using a form with a button and a memo.

I tried also using the array of TMyProc and that also did not work.  I kept getting access violations.

 

My goal actually is to learn the best practice simple approach in updating the GUI from within a running IOmniTask thats all. Just like Synchronize from a TThread but the Omnithread way.

I thought it may be using invoke but maybe its perhaps using Comm.Send to an omnithreadMonitor.

 

unit OTLInvokeMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, OtlTask, OtlTaskControl, Vcl.StdCtrls;

type
  TMyForm = class(TForm)
    Memo: TMemo;
    Button: TButton;
    procedure ButtonClick(Sender: TObject);
  private
    { Private declarations }
  procedure TestTask(const Task: IOmniTask);
  public
    { Public declarations }
  end;

var
  MyForm: TMyForm;

implementation

{$R *.dfm}

procedure TMyForm.ButtonClick(Sender: TObject);
begin
  createTask(TestTask).
  Run;
end;

procedure TMyForm.TestTask(const Task: IOmniTask);
type
  TMyProc = reference to procedure;

function CaptureValue(Value: Integer): TMyProc;
  begin
    Result := procedure begin Memo.Lines.add(Value.ToString); end;
  end;

var
  i: Integer;
  Proc: TMyProc;
begin
  Task.Invoke(procedure begin Memo.Lines.Add('Test 1 - Using Invoke (Outputs same value each time)'); end );
  for I := 0 to 2 do
    Task.Invoke(procedure begin Memo.Lines.Add(i.tostring); end );

  Task.Invoke(procedure begin Memo.Lines.Add('Test 2 - Using Invoke  with TMyProc and CaptureValue (Outputs same value each time)'); end );
  for I := 0 to 2 do
    begin
      Proc := CaptureValue(i);
      Task.Invoke(procedure
      begin
        Proc();
      end
      );
    end;
end;

end.

OTLInvoke.dpr

OTLInvokeMain.dfm

OTLInvokeMain.pas

  • Thanks 1

Share this post


Link to post

Tricky, got me completely dazzled for a moment 🙂

 

You are now generating new capture proc for each `I`. That `Proc`, however, is just a pointer. Now your code `Task.Invoke(procedure begin Proc(); end);` captures this `Proc` by value and executes the last stored value three times. 

 

You should do it like this:

 

function CaptureValue(Value: Integer): TOmniTaskInvokeFunction;
begin
  Result := procedure begin Memo.Lines.add(Value.ToString); end;
end;

for I := 0 to 2 do
  begin
    Task.Invoke(CaptureValue(i));
  end;

 

  • Thanks 1

Share this post


Link to post

Oh, one last thing if I may Primoz, is this approach any better or worse than using Task.Comm.Send with a message handler in an onmithreadmonitor?

regards

  • Thanks 1

Share this post


Link to post

FWIW, the repro could quite readily be expressed as a console app. It's worth learning how to do that because those apps are much simpler to share, being just a single file. 

Share this post


Link to post

Agreed David.  I cannot get this to work using Console.  I do not get any values outputted at all.

 

program OTLInvoke;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, otltask, OtlTaskControl;

procedure TestTask(const Task: IOmniTask);
  function CaptureValue(Value: Integer): TOmniTaskInvokeFunction;
  begin
    Result := procedure begin Writeln(Value.ToString); end;
  end;

  var
  i: Integer;
  begin
    Writeln('Test Using Invoke');
    for I := 0 to 9 do
      begin
        Task.Invoke(CaptureValue(i));
      end;
    WriteLn('');

    WriteLn('Press any key');
  end;

begin
  try
    { TODO -oUser -cConsole Main : Insert code here }
    createTask(TestTask).
    Run;
    Readln;

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Share this post


Link to post

Actually, I am probably misleading you here. OTL needs a message loop which isn't there. I'm sure you can run one but it doesn't come for free in a blank console app. 

 

Sorry! 

Edited by David Heffernan

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×