2

Using Terminal.Gui I'm trying to set a TextView's text from a asynchronous function, I can do it in C# just fine from a task but not from a Powershell Job, there might be a PS concept I'm missing here.

TextView tv = new TextView();

Task.Run(() =>
{
    Thread.Sleep(5000);
    tv.Text = "task";
    Application.DoEvents();
});

I've tried running a job script block

Start-Job -ScriptBlock { $textView.Text = "task" }

And also tried to invoke it from the MainLoop, not sure I'm doing this properly but it looks like this

function FromJob {
    param (
        [Terminal.Gui.TextView] $tv
    )

    $delegate = [System.Action]{
        $tv.Text = "Delegate"
    }

    [Terminal.Gui.Application]::MainLoop.Invoke($delegate)
}

Start-Job -ScriptBlock { FromJob } -ArgumentList @($textView)
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
dragonmost
  • 43
  • 6
  • 2
    there are 2 issues with your code, I have no experience with `Terminal.Gui.Application` but what is clearly wrong is, 1. `FromJob` function doesn't exist in the job's scope and 2. `Start-Job` is not the right command to use, it runs on a different process and has no access to update reference variables. You could use a pwsh instance instead: `[powershell]::Create().AddScript(...` or use `Start-ThreadJob` – Santiago Squarzon Mar 14 '23 at 03:54
  • 1
    Thank you, `Start-ThreadJob` was the right answer. I'm now running the whole function in the script block and passing the TextView as parameter – dragonmost Mar 14 '23 at 04:20

1 Answers1

2

As explained in comments, the main issue with your code is that:

  1. The FromJob function doesn't exist in the scope of your Job, it would preferably need to defined inside it or the definition of the function should be passed as argument and then defined in that scope.
  2. Sart-Job Jobs run in a different PowerShell process and has no access to update a reference variable passed to it. All arguments passed to these jobs are serialized and then deserialized and are no longer the same reference.

To overcome the point 2, the easiest option is with Start-ThreadJob which executes in the same process in a different runspace. Also, in this case I don't see the need for a function.

$job = Start-ThreadJob -ScriptBlock {
    # brings the locally defined variable to this scope
    $textView = $using:textView
    $delegate = [System.Action]{
        $textView.Text = "Delegate"
    }

    [Terminal.Gui.Application]::MainLoop.Invoke($delegate)
}

The other option is with a PowerShell instance which comes in handy if for some reason you can't install the ThreadJob Module and are using Windows PowerShell 5.1 (it comes preinstalled in newer versions).

$ps = [powershell]::Create().AddScript({
    param([Terminal.Gui.TextView] $tv)

    $delegate = [System.Action]{
        $tv.Text = "Delegate"
    }

    [Terminal.Gui.Application]::MainLoop.Invoke($delegate)
}).AddParameter('tv', $textView)
$async = $ps.BeginInvoke()
Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37