Solving the PowerShell GUI Paradox (With Example Scripts)
In GUI-based PowerShell scripts, a problem can arise when you combine GUI components with traditional PowerShell code. Here’s the solution.
February 27, 2024
Although I have created numerous GUI-based PowerShell scripts, there has always been one issue that I struggled to wrap my head around, let alone solve.
To be frank, this issue was so problematic that I searched for inventive ways to avoid it rather than dealing with it. However, in a recent project, I found myself unable to creatively sidestep this particular challenge, so I had to confront the issue head-on. In doing so, I came up with a solution for what I often refer to as the “PowerShell GUI Paradox.”
What Is the PowerShell GUI Paradox?
The PowerShell GUI Paradox revolves around how PowerShell handles Windows forms, which are the basis for GUI-based scripts. Unfortunately, it probably wouldn’t make a lot of sense if I tried to explain the issue upfront. Instead, I will demonstrate the root cause of the paradox and then show how you can address it.
The two threads in GUI-based PowerShell scripts
The first thing you must know is that GUI (forms)-based PowerShell scripts operate with two distinct threads of execution. The pair of threads may not always synchronize. A normal PowerShell script executes instructions sequentially, whereas a GUI-based script uses objects (like buttons, combo boxed, etc.) and event handlers (specifying actions when a button is clicked, for instance). This means that GUI-based scripts use a different logic structure compared to traditional PowerShell scripts.
Mixing form objects and PowerShell code
The problem is that complex PowerShell scripts frequently use a combination of form objects and conventional PowerShell code. While there are methods to mesh the two together, doing so can cause some pretty interesting problems. Even something as simple as prompting a user for input can become problematic.
Let me show you what I mean. My first example uses the code shown below:
# Load Assemblies
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
#Initialize Variable
$ClickedButton=3
# Create the form
$form = New-Object Windows.Forms.Form
$form.Text = "PowerShell Paradox"
$form.Size = New-Object Drawing.Size(1800,1000)
$form.FormBorderStyle = [Windows.Forms.FormBorderStyle]::FixedDialog
$form.StartPosition = [Windows.Forms.FormStartPosition]::CenterScreen
$Form.BackColor="white"
# Text Label 1
$TextLabel1 = New-Object Windows.Forms.Label
$TextLabel1.Text = "Please Click a Button"
$TextLabel1.Font = New-Object Drawing.Font("Arial", 24, [Drawing.FontStyle]::Bold)
$TextLabel1.AutoSize = $True
$TextLabel1.Location = New-Object Drawing.Point(20, 100)
$TextLabel1.ForeColor = [System.Drawing.Color]::Black
# Text Label 2
$TextLabel2 = New-Object Windows.Forms.Label
$TextLabel2.Text = "You clicked button " + $ClickedButton
$TextLabel2.Font = New-Object Drawing.Font("Arial", 24, [Drawing.FontStyle]::Bold)
$TextLabel2.AutoSize = $true
$TextLabel2.Location = New-Object Drawing.Point(20, 600)
$TextLabel2.ForeColor = [System.Drawing.Color]::Black
# Button 1
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Location = New-Object System.Drawing.Size (120,400)
$Button1.Size = New-Object System.Drawing.Size(110,110)
$Button1.BackColor = "lightgray"
$Button1.Font=New-Object System.Drawing.Font("Lucida Console",24,[System.Drawing.FontStyle]::Regular)
$Button1.Text="1"
$Button1.Add_Click({
$ClickedButton=1
})
# Button 2
$Button2 = New-Object System.Windows.Forms.Button
$Button2.Location = New-Object System.Drawing.Size (320,400)
$Button2.Size = New-Object System.Drawing.Size(110,110)
$Button2.BackColor = "lightgray"
$Button2.Font=New-Object System.Drawing.Font("Lucida Console",24,[System.Drawing.FontStyle]::Regular)
$Button2.text="2"
$Button2.Add_Click({
$ClickedButton=2
})
# Add the buttons to the form
$Form.Controls.Add($Button1)
$Form.Controls.Add($Button2)
$Form.Controls.Add($TextLabel1)
$Form.Controls.Add($TextLabel2)
$form.ShowDialog()
This script creates a variable named $ClickedButton with an initial value of 3. The script then displays two buttons and asks the user to click one. Depending on which button the user clicks, the $ClickedButton variable is set to a value of either 1 or 2. Near the end of the script, there is a line of code that displays the value of the $ClickedButton variable. You can see what the script looks like in Figure 1.
Figure 1. The script prompts the user to click a button.
Problem #1: Lack of a Pause Mechanism
The script that I just showed you has two problems. Firstly, it doesn’t pause and wait for the user to click a button. Instead, it simply echoes the initial value of the $ClickedButton variable (which is 3). This happens because the line displaying the variable’s value executes before the user even has a chance to click a button. Interestingly, there is no PowerShell command that I know of that tells it to pause and refrain from executing further instructions until a button is clicked.
To solve this first of the two problems, remember that PowerShell forms are based on objects and event handlers rather than traditional scripting techniques. As such, the solution is to add a line of code within the button click actions that directly manipulates the text label. The approach goes beyond merely modifying the value of the $ClickedButton variable.
Here is what one of the revised click actions within the script looks like:
$Button1.Add_Click({
$ClickedButton=1
$TextLabel2.Text = "You clicked button 1"
})
Figure 2. The script now seems to work correctly.
As you can see, the script now does what it is supposed to do.
Problem #2: Using Traditional Code in GUI-Based Scripts
There is one more issue that we need to address – and this is the big one. In the real world, a script typically does more than just display a variable's value. Instead, it usually takes some action based on the variable's value.
It would be easy enough to modify the existing script to perform a calculation based on the clicked button’s value, and then display the results using a GUI label. However, things become really interesting (and problematic) when you attempt to weave in more traditional PowerShell code.
Example Scenario: Mathematical calculation
Imagine a scenario where your goal is to write a script that prompts the user to click one of two buttons. The script must then take the value associated with that button and perform a mathematical calculation on it (e.g., multiplying the value by two). How might you do this?
The most obvious solution is to modify the button click actions so that the calculation is performed as a part of the click action. Alternatively, you could have the click action call a function that performs the required calculation. While effective in some cases, these approaches can’t be used in every situation.
In a complex script that I recently developed, using functions or multi-step click actions just wasn’t an option due to specific script requirements (for reasons that I won't go into here). In the script, the code for taking action on the variable absolutely had to be placed at the end of the script. There was just no getting around that requirement.
Adding a line of code to the end of a script probably doesn't seem like a big deal, but this is where the previously mentioned paradox comes into play. There is one essential command that must be included in forms-based PowerShell scripts, and yet this single command causes a major problem for anyone needing to run traditional PowerShell code in conjunction with the GUI.
Let’s now examine that problem and its non-conventional solution.
The Root of the PowerShell GUI Paradox
That one command that causes problems is ShowDialog().
The ShowDialog() command is responsible for displaying a form (the GUI). The challenge with this command stems from the fact that any instructions following it will not execute until the GUI interface is closed.
With that in mind, let's revisit the mathematical calculation scenario from earlier. We wanted to create a PowerShell script that displays two buttons, waits for user input, and then, upon button click, performs a multiplication by two with the result displayed at the end. Adding a twist, the script should be written without resorting to the use of functions.
To highlight the complexity of this seemingly straightforward script, take a look at the two example scripts below.
Example script #1
The first example places the calculation and output instructions just before the ShowDialog() command. You can see the full code below. The script is identical to the one that I showed you earlier aside from the final three lines:
# Load Assemblies
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
#Initialize Variable
$ClickedButton=3
# Create the form
$form = New-Object Windows.Forms.Form
$form.Text = "PowerShell Paradox"
$form.Size = New-Object Drawing.Size(1800,1000)
$form.FormBorderStyle = [Windows.Forms.FormBorderStyle]::FixedDialog
$form.StartPosition = [Windows.Forms.FormStartPosition]::CenterScreen
$Form.BackColor="white"
# Text Label 1
$TextLabel1 = New-Object Windows.Forms.Label
$TextLabel1.Text = "Please Click a Button"
$TextLabel1.Font = New-Object Drawing.Font("Arial", 24, [Drawing.FontStyle]::Bold)
$TextLabel1.AutoSize = $True
$TextLabel1.Location = New-Object Drawing.Point(20, 100)
$TextLabel1.ForeColor = [System.Drawing.Color]::Black
# Text Label 2
$TextLabel2 = New-Object Windows.Forms.Label
$TextLabel2.Text = "You clicked button " + $ClickedButton
$TextLabel2.Font = New-Object Drawing.Font("Arial", 24, [Drawing.FontStyle]::Bold)
$TextLabel2.AutoSize = $true
$TextLabel2.Location = New-Object Drawing.Point(20, 600)
$TextLabel2.ForeColor = [System.Drawing.Color]::Black
# Button 1
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Location = New-Object System.Drawing.Size (120,400)
$Button1.Size = New-Object System.Drawing.Size(110,110)
$Button1.BackColor = "lightgray"
$Button1.Font=New-Object System.Drawing.Font("Lucida Console",24,[System.Drawing.FontStyle]::Regular)
$Button1.Text="1"
$Button1.Add_Click({
$ClickedButton=1
$TextLabel2.Text = "You clicked button 1"
})
# Button 2
$Button2 = New-Object System.Windows.Forms.Button
$Button2.Location = New-Object System.Drawing.Size (320,400)
$Button2.Size = New-Object System.Drawing.Size(110,110)
$Button2.BackColor = "lightgray"
$Button2.Font=New-Object System.Drawing.Font("Lucida Console",24,[System.Drawing.FontStyle]::Regular)
$Button2.text="2"
$Button2.Add_Click({
$ClickedButton=2
$TextLabel2.Text = "You clicked button 2"
})
# Add the buttons to the form
$Form.Controls.Add($Button1)
$Form.Controls.Add($Button2)
$Form.Controls.Add($TextLabel1)
$Form.Controls.Add($TextLabel2)
$ClickedButton=$ClickedButton*2
Write-Host $ClickedButton
$form.ShowDialog() | Out-Null
The problem with this script is that it does not wait for the user to click a button before performing the calculation. As you may recall, the $ClickedButton variable is given an initial value of 3. In Figure 3, you can see that PowerShell displays the number 6 because it has multiplied the initial value (3) by two.
Figure 3. The script does not wait for the user to click a button.
Before moving on, note that if the intention were to perform the calculation and display its results within the GUI, the problem I just showed you would be a total non-issue. However, the goal here is to demonstrate what can happen when trying to mesh Windows forms with more traditional PowerShell scripting techniques.
Example script #2
Let’s now look at my second example. In this version, I have modified the script’s last three lines of code so that the ShowDialog() command comes before the calculation. Here are the last three lines of code:
$form.ShowDialog()
$ClickedButton=$ClickedButton*2
Write-Host $ClickedButton
This time, the script does not display the calculation result, as shown in Figure 4. The result is displayed only after closing the GUI. Even at that, the result is incorrect. The calculation is based on the initial value of the $ClickedButton variable rather than on the value assigned to the variable when a button is clicked.
Figure 4. The script does not perform the calculation because the GUI is still open.
Solutions to the Problems
So, how in the world can you get around these problems? Let’s first deal with the issue of the calculation relying on an initial value rather than on the value assigned when a button is clicked.
Global variable solution
The reason why that happens is because the variable assignment is local to the click action and therefore not recognized by the rest of the script.
The solution is to convert the $ClickedButton variable into a global variable. To do so, just add the word Global to the variable assignment. For example, instead of typing $ClickedButton=1, use $Global:ClickedButton=1 instead.
Dealing with ShowDialog()
But what about the problem caused by the ShowDialog() command? To recap, the ShowDialog() command is necessary for displaying the form. The presence of the ShowDialog() command also forces PowerShell to pause and wait for a button click. However, any code that comes after the ShowDialog() command is effectively ignored so long as the GUI is open. This is why I refer to the ShowDialog() command as the PowerShell GUI paradox.
So, in the context of this script, code placed before ShowDialog() executes incorrectly because PowerShell doesn’t wait for a button click, but code placed after ShowDialog() is completely ignored.
As previously mentioned, I really struggled with finding a solution to this problem. Eventually, however, I realized that nothing is preventing a script from closing and then reopening a GUI. That way, a script can render a GUI, close the GUI, process any non-GUI code, and then reopen the GUI.
Let's explore how this works.
My first step in solving the problem involved adding a line of code to both button click actions. That line comes at the end of the click action block and is as follows:
[Void]$Form.Close()
This line makes it so that the click action assigns a value to the $ClickedButton variable and updates the label displaying text that describes which button was clicked. After that, the new line of code causes the form to close, thereby allowing the execution of the non-GUI code at the end of the script.
Another thing I did to fix the problem was to add a second instance of the $Form.ShowDialog() command to the end of the script. Here is the command:
$Form.ShowDialog() | Out-Null
In case you are wondering, the Out-Null portion of the command prevents PowerShell from displaying the word Cancel when the GUI is closed.
In conclusion, the script works in the following way:
The first ShowDialog command prompts the display of the GUI form, allowing the user to interact with the buttons.
When a button is clicked, the $Form.Close() command causes the GUI to close. This allows any code that comes after $Form.ShowDialog() to execute. Remember, that code cannot run so long as the GUI is open.
After the non-GUI code runs, the $Form.ShowDialog() command is used to reopen the form.
You can see what this looks like in Figure 5.
Figure 5. Both the GUI and the non-GUI code were allowed to execute.
The Full Script
Here is the full script:
# Load Assemblies
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
#Initialize Variable
$Global:ClickedButton=3
# Create the form
$form = New-Object Windows.Forms.Form
$form.Text = "PowerShell Paradox"
$form.Size = New-Object Drawing.Size(1800,1000)
$form.FormBorderStyle = [Windows.Forms.FormBorderStyle]::FixedDialog
$form.StartPosition = [Windows.Forms.FormStartPosition]::CenterScreen
$Form.BackColor="white"
# Text Label 1
$TextLabel1 = New-Object Windows.Forms.Label
$TextLabel1.Text = "Please Click a Button"
$TextLabel1.Font = New-Object Drawing.Font("Arial", 24, [Drawing.FontStyle]::Bold)
$TextLabel1.AutoSize = $True
$TextLabel1.Location = New-Object Drawing.Point(20, 100)
$TextLabel1.ForeColor = [System.Drawing.Color]::Black
# Text Label 2
$TextLabel2 = New-Object Windows.Forms.Label
$TextLabel2.Text = "You clicked button " + $Global:ClickedButton
$TextLabel2.Font = New-Object Drawing.Font("Arial", 24, [Drawing.FontStyle]::Bold)
$TextLabel2.AutoSize = $true
$TextLabel2.Location = New-Object Drawing.Point(20, 600)
$TextLabel2.ForeColor = [System.Drawing.Color]::Black
# Button 1
$Button1 = New-Object System.Windows.Forms.Button
$Button1.Location = New-Object System.Drawing.Size (120,400)
$Button1.Size = New-Object System.Drawing.Size(110,110)
$Button1.BackColor = "lightgray"
$Button1.Font=New-Object System.Drawing.Font("Lucida Console",24,[System.Drawing.FontStyle]::Regular)
$Button1.Text="1"
$Button1.Add_Click({
$Global:ClickedButton=1
$TextLabel2.Text = "You clicked button 1"
[Void]$Form.Close()
})
# Button 2
$Button2 = New-Object System.Windows.Forms.Button
$Button2.Location = New-Object System.Drawing.Size (320,400)
$Button2.Size = New-Object System.Drawing.Size(110,110)
$Button2.BackColor = "lightgray"
$Button2.Font=New-Object System.Drawing.Font("Lucida Console",24,[System.Drawing.FontStyle]::Regular)
$Button2.text="2"
$Button2.Add_Click({
$Global:ClickedButton=2
$TextLabel2.Text = "You clicked button 2"
$Form.Close()
})
# Add the buttons to the form
$Form.Controls.Add($Button1)
$Form.Controls.Add($Button2)
$Form.Controls.Add($TextLabel1)
$Form.Controls.Add($TextLabel2)
$form.ShowDialog() | Out-Null
$Global:ClickedButton=$ClickedButton*2
Write-Host $ClickedButton
$Form.ShowDialog() | Out-Null
About the Author(s)
You May Also Like