multithreadingpowershellpowershell-jobs

Update a WPF GUI using Powershell Jobs


I've been trying to create responsive GUIs for my personal Powershell scripts. I've come up with a problem that is highly discussed online: Freezing GUI (since Powershell is single threaded).

Similar to this problem, but my case is specific to Powershell. I successfully implemented a Powershell based solution for creating GUIs relying on XAML form. Now, let's consider this code:

#EVENT Handler
$Somebutton.add_Click({
    $SomeLabel.Content = "Calculating..." 

    Start-Job -ScriptBlock {
        #Computation that takes time
        #...
        $SomeLabel.Content = "Calculated value" 
    }
})

#Show XAML GUI
$xamlGUI.ShowDialog() | out-null

xamlGUI is the form itself and $Somebutton/$SomeLabel are controls I was able to read from xaml and transform to Powershell variables.

I'm trying to understand why the Job that I start is not updating my label when the computation is done. It actually does nothing.

Im new to Powershell jobs and I'm wondering if there is something I'm missing.


Solution

  • Here's a little boilerplate I use for reactive WPF forms in PowerShell:

    # Hide yo console
    $SW_HIDE, $SW_SHOW = 0, 5
    $TypeDef = '[DllImport("User32.dll")]public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);'
    Add-Type -MemberDefinition $TypeDef -Namespace Win32 -Name Functions
    $hWnd = (Get-Process -Id $PID).MainWindowHandle
    $Null = [Win32.Functions]::ShowWindow($hWnd,$SW_HIDE)
    
    # Define your app + form
    Add-Type -AssemblyName PresentationFramework
    $App = [Windows.Application]::new() # or New-Object -TypeName Windows.Application
    $Form = [Windows.Markup.XamlReader]::Load(
        [Xml.XmlNodeReader]::new([xml]@'
    WPF form definition goes here
    '@)
    )
    # or ::Load((New-Object -TypeName Xml.XmlNodeReader -ArgumentList ([xml]@'
    #wpfdef
    #'@))
    #)
    
    # Fixes the "freeze" problem
    function Update-Gui {
        # Basically WinForms Application.DoEvents()
        $App.Dispatcher.Invoke([Windows.Threading.DispatcherPriority]::Background, [action]{})
    }
    
    # Event handlers go here
    $Form.add_Closing({
        $Form.Close()
        $App.Shutdown()
        Stop-Process -Id $PID # or return your console: [Win32.Functions]::ShowWindow($hWnd,$SW_SHOW)
    })
    
    # Finally
    $App.Run($Form)
    

    Remember to clean up when your app is shutting down:

    $Form.Close()
    $App.Shutdown()
    Stop-Process -Id $PID
    

    Whenever you need your changes to the GUI to be reflected, call the Update-Gui function.