multithreadingpowershellterminal.gui

Set TextView.Text from asynchronous function


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)

Solution

  • 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()