Dealing with Variables in a WinForm Event Handler: An Alternative to Script Scope

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.

image

As you can see below, the actual text inputted doesn’t make it outside of the event handler.

image

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)"

image

SNAGHTML1686c8

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.

image

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)"

image

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
}

image

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.

This entry was posted in powershell and tagged , , , . Bookmark the permalink.

6 Responses to Dealing with Variables in a WinForm Event Handler: An Alternative to Script Scope

  1. GMax says:

    Hmm, I can’t reproduce JOB approach in clear new powershel (v4):
    D:>powershell -noprofile -command D:\2.ps1
    ThreadID: 3 – ProcessID: 32412)

    Id Name PSJobTypeName State HasMoreData Location Command
    — —- ————- —– ———– ——– ——-
    3 c3e8b882-b8d… NotStarted False …
    1 Job1 BackgroundJob Running True localhost star…

    [EventHandler] ThreadID: 1 – ProcessID: 32412)
    Data:

    where 2.ps1 – it’s your script (with added $job output)
    and if i try to do $synchash.test = ‘test2’ inside event i get
    Can’t find “test” property exception inside event (because $synchash -eq $null)

  2. SteveD says:

    Thank you gentlemen -that’s clear now. It’s the type of thing PowerShell tries to do behind the scenes to help us creates this confusion 🙂

  3. SteveD says:

    Thanks Boe – neatly explained.

    A question on the below lines in your script samples:

    $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)

    How does the Size object fit into both the Location and Size property of textbox object here?

    • Boe Prox says:

      The location property just defines the location on the Windows from the top left position. So in this case, the TextBox would be 10 pixels from the left and 10 pixels from the top.

      The size is a little different, especially for this instance. It goes by a Width, Height when creating the object. so the width of 500 pixels matches up with what is showing up when creating the form. I am a little unsure about why the height of 500 pixels doesn’t actually reflect that in the form, but I am assuming it would auto size itself if you starting adding returns in the field up until the 500 pixel limit. You will notice that the button starts at the 510 pixel height spot which goes along with what I am assuming is the max height size of the text box.

      • Dave Wyatt says:

        Technically, the Location property is supposed to have a Point rather than a Size, but since they’re both just structs with X and Y properties, PowerShell figures it out and does the conversion without an error.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s