Scope in a PowerShell GUI App

September 9th, 2015 by June Blender
Last updated on September 7th, 2015

 

If you search the SAPIEN blog, you’ll realize that this is the not our first blog about scope in PowerShell GUIs. In March 2013, SAPIEN CTO Alex Riedel wrote First Rule of PowerShell Scoping Rules, a title that implies the final word in a discussion that wasn’t easy to end. Alex’s blog explained the differences between scoping rules in PowerShell and other languages, like VB Script.

Then, just a month later, SAPIEN Senior Developer David Corrales wrote PowerShell Scoping Revisited, another great post about function scope in PowerShell. A quick search reveals even older posts by Don Jones and Jeffery Hicks.

It seems that everyone is a bit baffled by scopes in PowerShell GUIs and each of us needs to resolve the scope issue that most confounds us. For me, the most baffling part of scoping in a very simple PowerShell GUI app was the lack of an evident scope boundary.

Where is the scope boundary?

I know from about_Scopes and other sources that when a script includes functions, the script is a scope and each function in the script has its own child scope. The child scope can read from, but not change (write to), the values of variables in the parent scope. If you try to write to a parent scope, you create a local variable with the same name.

But, a very simple PowerShell GUI app is just one big script or function. The script contains event handlers, but those seem to be just commands assigned to a variable.

$buttonClick _Close = { $form.Close() }
Is this a scope?

However, when I create a variable that is designed to be shared by the event handlers, the event handlers can read the value, but they can’t change it.

For example, in this little GUI app, the $sharedValue variable is supposed to be shared by all event handlers. The $buttonReadValue_Click event handler reads the value and assigns it to a read-only textbox. The $textboxShared.TextChanged event handler changes the shared value to whatever you type in the text box.

clip_image001

$sharedValue = 'Start'
 
$formTestScope_Load = {
    $textboxShared.Text = $sharedValue
}
 
$buttonReadValue_Click={
    $textboxRead.Text = $sharedValue
}
 
$textboxShared_TextChanged={
    $sharedValue = $textboxShared.Text.Trim()
}

But, it doesn’t work. The $buttonReadValue_Click event handler can read the shared value (and assign it to its textbox), but the $textboxShared_TextChanged event handler doesn’t change it. When you change the value in the Shared value textbox and click Read value again, it gets (and displays) the initial value.

clip_image002

This behaves like a scope issue, but I couldn’t see the scope boundary.

Event Handlers and Script Scope

Then it occurred to me:

Each event handler contains a script block. And, each script block is a script with its own scope.

To prove my theory about the scope boundaries, in the console, I created two variables that contained script blocks. My variables are not event handler, but they have the same syntax.

— The $readScript script block reads a variable value from the parent scope.
— The $writeScript script block changes the value of a variable in the parent scope.

To run the code in the variables that contain script blocks, you need to use the invoke operator (&). The results confirm that each script block is a script that has its own scope. The attempt (in $writeScript) to change the $name variable value fails.

PS C:\> $name = 'PowerShell'
PS C:\> $readScript = { Write-Host "I love $name." }
PS C:\> $writeScript = { $name = 'Windows PowerShell'; Write-Host "I love $name." }

PS C:\> & $readScript
I love PowerShell.

PS C:\> & $writeScript
I love Windows PowerShell.

PS C:\> & $readScript
I love PowerShell.

PS C:\> $name
PowerShell

Next, I’ll scope the variables. I almost never use Global scope. As a best practice, I use the smallest effective scope, but Global is the smallest scope for the command line.

This time, $writeScript changes the value of $name.

PS C:\> $name = 'PowerShell'
PS C:\> $readScript = { Write-Host "I love $name." }
PS C:\> $writeScript = { $global:name = 'Windows PowerShell'; Write-Host "I love $name." }

PS C:\> & $readScript
I love PowerShell.

PS C:\> & $writeScript
I love Windows PowerShell.

PS C:\> & $readScript
I love Windows PowerShell.

PS C:\> $name
Windows PowerShell

The alternative to using a scope modifier (e.g. $global:<varName>), is to use the Set-Variable cmdlet with its Scope parameter.

PS C:\> $name = 'PowerShell'
PS C:\> $writeScript = {
>> Set-Variable -Name Name -Scope Global -Value 'Windows PowerShell'
>> Write-Host "I love $name"
>> }
>>

PS C:\> & $writeScript
I love Windows PowerShell

PS C:\> $name
Windows PowerShell

That works.

Now that I understand that the event handler variables contains scripts with their own scope. Let’s fix the little PowerShell GUI app.

I scoped the $sharedValue variable to the script and, in the $textboxShared_TextChanged event, which is the only place that I change that variable value, I scope the reference to the $sharedValue variable.

$script:sharedValue = 'Start'
 
$formTestScope_Load = {
    $textboxShared.Text = $sharedValue
}
 
$buttonReadValue_Click={
    $textboxRead.Text = $sharedValue
}
 
$textboxShared_TextChanged={
    # scoped variable
    $script:sharedValue = $textboxShared.Text.Trim()
}

Now, when I type in the Shared value box, $textboxShared_TextChanged changes the value of the $sharedValue variable. And, when I click Read value the $buttonReadValue_Click reads the changed value.

clip_image003

In a real script that I plan to save and share, I would explicitly scope all references to a variable in a parent scope, even though it’s not necessary. This makes it very clear that I intended to create a script-scope variable and that I’m referencing a variable in a script scope.

# Script-scoped variables
$script:sharedValue = 'Start'
 
$formTestScope_Load = {
    # scoped variable
    $textboxShared.Text = $script:sharedValue
}
 
$buttonReadValue_Click={
    # scoped variable
    $textboxRead.Text = $script:sharedValue
}
 
$textboxShared_TextChanged={
    # scoped variable
    $script:sharedValue = $textboxShared.Text.Trim()
}

One more consideration. PowerShell Studio encloses every form in a separate function. This structure make it very easy open one form as a modal dialog of another form or share data between forms. However, it makes the scoping one level more complex.

So, this is now the last word about scoping in a PowerShell GUI app. Well, probably not.

June Blender is a technology evangelist at SAPIEN Technologies, Inc and a Windows PowerShell MVP. You can reach her at juneb@sapien.com and follow her on Twitter at @juneb_get_help.

Thanks to Windows PowerShell MVPs Keith Hill and Stephen Owen for their help with this post.

 

 
[Google+]   [Facebook]   [LinkedIn]   [StumbleUpon]   [Digg]   [Reddit]   [Google Bookmark]  

Tags: , , , , ,