I saw a question with an answer a while back showing how to work around an issue that had previously worked up until PowerShell V3 involving variables and how they work in an event handler in a WinForm or WPF UI. The suggested answer was to create the variable with the Script variable scope so that way the data in the variable would be available in the event handler and its scope.
An example of this can be shown below, first with the code that the variable would not work with because it is out of the scope of the event handler.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$window = New-Object System.Windows.Forms.Form
$window.Width = 1000
$window.Height = 1000
$windowTextBox = New-Object System.Windows.Forms.TextBox
$windowTextBox.Location = New-Object System.Drawing.Size(10,10)
$windowTextBox.Size = New-Object System.Drawing.Size(500,500)
$windowButtonOK = New-Object System.Windows.Forms.Button
$windowButtonOK.Location = New-Object System.Drawing.Size(10,510)
$windowButtonOK.Size = New-Object System.Drawing.Size(50,50)
$windowButtonOK.Text = "OK"
$windowButtonOK.Add_Click({
$text = $windowTextBox.Text
$window.Dispose()
})
$window.Controls.Add($windowTextBox)
$window.Controls.Add($windowButtonOK)
[void]$window.ShowDialog()
Write-Host -ForegroundColor Yellow "Test: $($Text)"
What happens here is that even though we set the variable ($Text) in the event handler, we cannot access it outside of the event handler at all.
As you can see below, the actual text inputted doesn’t make it outside of the event handler.
The solution to was to change the scope of the variable to Script: (not Global which would be beyond what is needed) to allow for the variable to be accessible to commands run in the script. As shown with the code below, we need to ensure that each time the variable is used, that we specify it as Script: to ensure it is read properly, otherwise the variable will just be treated like it was in the previous example.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$window = New-Object System.Windows.Forms.Form
$window.Width = 1000
$window.Height = 1000
$ThreadID = [System.Threading.Thread]::CurrentThread.ManagedThreadId
Write-Host -ForegroundColor Green "ThreadID: $($ThreadID) - ProcessID: $PID)"
$windowTextBox = New-Object System.Windows.Forms.TextBox
$windowTextBox.Location = New-Object System.Drawing.Size(10,10)
$windowTextBox.Size = New-Object System.Drawing.Size(500,500)
$windowButtonOK = New-Object System.Windows.Forms.Button
$windowButtonOK.Location = New-Object System.Drawing.Size(10,510)
$windowButtonOK.Size = New-Object System.Drawing.Size(50,50)
$windowButtonOK.Text = "OK"
$windowButtonOK.Add_Click({
$ThreadID = [System.Threading.Thread]::CurrentThread.ManagedThreadId
Write-Host -ForegroundColor Green "[EventHandler] ThreadID: $($ThreadID) - ProcessID: $PID)"
$Script:text = $windowTextBox.Text
$window.Dispose()
})
$window.Controls.Add($windowTextBox)
$window.Controls.Add($windowButtonOK)
[void]$window.ShowDialog()
Write-Host -ForegroundColor Yellow "Test: $($Script:text)"
This time we have our variable available outside of the event handler scope and now shows up when we display the information. Definitely something to remember when you are working with UIs in PowerShell V3 and above so you are not left wondering why specific events are not working properly.
In case you are wondering if the event handler occurs in a different process id and/or thread, it doesn’t.
I started with this because it works. But it isn’t the only approach to dealing with this issue by adjusting the variable scope. My approach to this is done using synchronized collections, or what I commonly use with runspaces, the synchronized hash table.
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$hash = [hashtable]::Synchronized(@{})
$hash.text = ""
$window = New-Object System.Windows.Forms.Form
$window.Width = 1000
$window.Height = 1000
$windowTextBox = New-Object System.Windows.Forms.TextBox
$windowTextBox.Location = New-Object System.Drawing.Size(10,10)
$windowTextBox.Size = New-Object System.Drawing.Size(500,500)
$windowButtonOK = New-Object System.Windows.Forms.Button
$windowButtonOK.Location = New-Object System.Drawing.Size(10,510)
$windowButtonOK.Size = New-Object System.Drawing.Size(50,50)
$windowButtonOK.Text = "OK"
$windowButtonOK.Add_Click({
$hash.text = $windowTextBox.Text
$window.Dispose()
})
$window.Controls.Add($windowTextBox)
$window.Controls.Add($windowButtonOK)
[void]$window.ShowDialog()
Write-Host -ForegroundColor Yellow "Test: $($hash.text)"
Works like a charm without having to mess with variable scopes. But it does require that you initialize the hash table and make sure it is synchronized. This is a nice approach also if you have multiple variables that you may need to track across other event handlers rather than dealing with the variable scope.
Now, this doesn’t actually just apply to working with the UIs in PowerShell. This can also be applied to other event handlers such as those that have action script blocks using the Register-*Event cmdlets as well as shown in the demo below.
$synchash = [hashtable]::Synchronized(@{})
$synchash.test = 'test1'
$ThreadID = [System.Threading.Thread]::CurrentThread.ManagedThreadId
Write-Host -ForegroundColor Green "ThreadID: $($ThreadID) - ProcessID: $PID)"
$Job = start-job -ScriptBlock {start-sleep -seconds 2}
Register-ObjectEvent -InputObject $Job -EventName StateChanged -Action {
$ThreadID = [System.Threading.Thread]::CurrentThread.ManagedThreadId
Write-Host -ForegroundColor Green "[EventHandler] ThreadID: $($ThreadID) - ProcessID: $PID)"
Write-Host "Data: $($synchash.test)" -ForegroundColor Yellow -BackgroundColor Black
}
Using the same approach worked again to display the data from within the Event handler script block. And as you can see again, the process id and thread id is the same as what is outside of the handler. We could also write to the hash table as well from within the handler and the data would be available for us to use in the current script.
So with that, we have an alternative to messing with the scope of variables if needed if you would rather not have to deal with it. This doesn’t mean that you have to stop using scopes such as Script, it just means that like most things in PowerShell, there are always alternatives to performing a single action. It does have a little bit more work in creating and initializing the synchronized collection, but it gives you something that will work perfectly within the scope of event handlers as well as offering a way to transport multiple variables within a single collection.
