multithreadingpowershellwinformsinvokerunspace

I can't close the form from another thread


I've tried different ways to call the form's close method. The code below seems the most optimal to me, but it doesn't work as expected.

I'd also like to avoid using Start-ThreadJob, as it requires the ThreadJob package to be installed.

I've also seen recommendations to use Runspace, but I don't think that's the best way to solve a seemingly simple problem.

As you can see from the commented lines, I tried to use different cmdlets to call the method in the current process thread. Considering that the form is passed as a parameter, this does not work correctly.

I have to ask a question here because methods for C# do not always work in powershell. Thanks.

$form = New-Object System.Windows.Forms.Form
$form.Text = "test"
$form.Size = New-Object System.Drawing.Size(600, 250)
$form.FormBorderStyle = "FixedDialog"


function FormCloseAsync(){
    param
    (
        [System.Windows.Forms.Form] $f
    )
    Start-Sleep -Seconds 5

    if ($f.InvokeRequired) {
        $f.Invoke({
            $f.Close()
        })
    } else {
        $f.Close()
    }
}


#Start-Job -ScriptBlock ${function:Start-MethodWithDelay}
#Start-ThreadJob -ScriptBlock ${function:FormCloseAsync} -ArgumentList $form

InvokeAsync-Command -ScriptBlock ${function:FormCloseAsync} -ArgumentList $form

$form.ShowDialog() | Out-Null

Solution

  • I'm not clear if your question is about asking for help on an implementation for InvokeAsync-Command, if that's the case, this implementation might help. Basically creates a new runspace to invoke the scriptblock passed as argument. Additionally, subscribes to the PowerShell instance InvocationStateChanged Event to auto-dispose when completed.

    function Invoke-CommandAsync {
        param(
            [Parameter(Mandatory)]
            [scriptblock] $Action,
    
            [Parameter()]
            [object[]] $ArgumentList)
    
        $ps = [powershell]::Create().AddScript($Action)
        foreach ($arg in $ArgumentList) {
            $ps = $ps.AddArgument($arg)
        }
    
        $registerObjectEventSplat = @{
            InputObject = $ps
            EventName   = 'InvocationStateChanged'
        }
    
        # Auto-dispose mechanism
        $null = Register-ObjectEvent @registerObjectEventSplat -Action {
            param(
                [powershell] $s,
                [System.Management.Automation.PSInvocationStateChangedEventArgs] $e)
    
            if ($e.InvocationStateInfo.State -ne 'Running') {
                $s.Dispose()
                Unregister-Event -SourceIdentifier $Event.SourceIdentifier
            }
        }
    
        $null = $ps.BeginInvoke()
    }
    

    With it, your current code would work properly, the form would close after 5 second in an async manner, without blocking it.

    I'm changing your example a bit for the demo:

    Add-Type -AssemblyName System.Windows.Forms
    
    $form = New-Object System.Windows.Forms.Form
    $form.Text = 'test'
    $form.Size = New-Object System.Drawing.Size(600, 250)
    $form.FormBorderStyle = 'FixedDialog'
    $form.Controls.Add(
        [System.Windows.Forms.TextBox]@{
            Name    = 'myTxtBox'
            Enabled = $false
            Size    = [System.Drawing.Size]::new(500, 50)
        }
    )
    
    function FormCloseAsync {
        param([System.Windows.Forms.Form] $f)
    
        $txt = $f.Controls.Find('myTxtBox', $false)[0]
        5..1 | ForEach-Object {
            $txt.Text = "Exiting in $_ seconds..."
            Start-Sleep 1
        }
    
        $f.Close()
    }
    
    Invoke-CommandAsync ${function:FormCloseAsync} -ArgumentList $form
    $form.ShowDialog()