gorepj 3 Posted June 25, 2019 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? 1 Share this post Link to post
David Heffernan 2345 Posted June 25, 2019 You have captured the variable. But you need to capture the value. Capturing the value is not supported directly so you need to fake that by making a copy of the variable into a new value, one per task. More detail here http://docwiki.embarcadero.com/RADStudio/en/Anonymous_Methods_in_Delphi https://stackoverflow.com/a/24223683/505088 Share this post Link to post
gorepj 3 Posted June 25, 2019 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
David Heffernan 2345 Posted June 25, 2019 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
gorepj 3 Posted June 26, 2019 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
David Heffernan 2345 Posted June 26, 2019 (edited) 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 June 26, 2019 by David Heffernan Share this post Link to post
Primož Gabrijelčič 223 Posted June 26, 2019 Reproducible test case, please ... Share this post Link to post
gorepj 3 Posted June 28, 2019 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 1 Share this post Link to post
Primož Gabrijelčič 223 Posted June 28, 2019 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; 1 Share this post Link to post
gorepj 3 Posted June 28, 2019 Yes, that worked, thank you so much Primoz. Case closed Share this post Link to post
gorepj 3 Posted June 28, 2019 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 1 Share this post Link to post
Primož Gabrijelčič 223 Posted June 28, 2019 Functionally it's about the same. I like the anonymous method approach more because all functionality is concentrated in one space. Share this post Link to post
David Heffernan 2345 Posted June 28, 2019 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
gorepj 3 Posted June 28, 2019 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
David Heffernan 2345 Posted June 28, 2019 (edited) 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 June 28, 2019 by David Heffernan Share this post Link to post
Primož Gabrijelčič 223 Posted June 28, 2019 1 hour ago, David Heffernan said: OTL needs a message loop which isn't there. Indeed.This is documented. Share this post Link to post