3

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.

scharette
  • 9,437
  • 8
  • 33
  • 67
  • Once you start a job it is running in a completely different context. It is not aware of the objects that you have in memory in the context that the form is running in. In theory I suspect that you should be able to pass the button to the job. – EBGreen May 07 '18 at 15:25
  • Thanks for answer, I would pass the label using: `-ArgumentList $SomeLabel` ? – scharette May 07 '18 at 15:48
  • 1
    Most people who go for concurrency with PowerShell utilize runspaces over jobs since you can communicate with them more easily. Also, I'd suggest moving away from `ShowDialog()` in favor of `$App = [Windows.Application]::new(); $App.Run($Form)` – Maximilian Burszley May 07 '18 at 15:49
  • You need to accept it as a parameter in the scriptblock too. – EBGreen May 07 '18 at 15:49
  • Something I've used to manually update WPF forms in powershell: `$App.Dispatcher.Invoke([Windows.Threading.DispatcherPriority]::Background, [action]{})` – Maximilian Burszley May 07 '18 at 15:50
  • Also, @TheIncorrigible1 has a better solution. I'm not even positive that my idea will work. It was just the first thing that came to mind. – EBGreen May 07 '18 at 15:50
  • @TheIncorrigible1 Method invocation failed because [System.Windows.Application] doesn't contain a method named 'new'. Is there something I'm doing wrong ? – scharette May 07 '18 at 15:57
  • @scharette That's the PowerShell 5 way of doing it. You have to do `New-Object -TypeName Windows.Application` – Maximilian Burszley May 07 '18 at 15:59
  • @TheIncorrigible1 Oh damn my bad... Thanks a lot! – scharette May 07 '18 at 15:59
  • @scharette I updated my answer with a boilerplate I use, alongside pre-v5 methods of accomplishing what I do. – Maximilian Burszley May 07 '18 at 16:10

1 Answers1

4

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.

Maximilian Burszley
  • 18,243
  • 4
  • 34
  • 63
  • Thanks a lot for your answer! Can't believe this was not on SO yet... I will probably go for a c# based solution though. Working with multi-threading in c# seems so much easier and is in my opinion worth learning it for easier runspaces handling. I can then call my scripts in c# controls handlers like you proposed on another of my thread. If you have any comments on that too it would be appreciated. Thanks agian. – scharette May 07 '18 at 17:09
  • 2
    @scharette C# is definitely the way to go for anything more than the most basic forms. – Maximilian Burszley May 07 '18 at 17:10
  • Just because i'm curious, isn't it a comparable to `Application.DoEvents()` ? It is often seen as the devil by the community (see [this](https://blog.codinghorror.com/is-doevents-evil/)) – scharette May 08 '18 at 12:02
  • @scharette Yes, it is comparable (and I left a comment in my code as such). It's seen as the devil in C# apps because that's not how you're supposed to design a wpf app, but in PowerShell, it requires significantly more effort to do the same things as C# so I use it as a shortcut – Maximilian Burszley May 08 '18 at 13:59
  • I see. Thanks for the insight ! – scharette May 08 '18 at 14:10
  • Hi, since on SO there is no "private messaging" I have to ask here. You where talking you will work in C#. What do you mean exactly by that? I throught WPF is C#? I'm asking because I want to build a Tool that does many things with AD users using Powershell and WPF as Gui. – GeraltDieSocke Jul 05 '18 at 09:28
  • @GeraltDieSocke If you want to learn about creating GUIs in PowerShell with WPF, I recommend [reading this article](https://foxdeploy.com/2015/04/10/part-i-creating-powershell-guis-in-minutes-using-visual-studio-a-new-hope/). – Maximilian Burszley Jul 06 '18 at 15:08