Table of Contents
I recently wrote a PowerShell script that used the Windows Forms .NET assembly (Winforms) to create a graphical user interface. The form included a button that initiated a long-running task.
While the task was executing, I wanted to ensure the form remained responsive to user input, and I also wanted to update a status field on the form with the progress of the executing task.
Below is the code for the script that we’ve create.
Add-Type -AssemblyName System.Windows.Forms
# Long running task with runspace
function PoSHCounter {
# Disable the button
$button.Enabled = $false
for ($i = 10; $i -le 99; $i++) {
$label.Text = $i
Start-Sleep -Milliseconds 50
}
# Re-enable the button when the loop finishes.
$button.Enabled = $true
}
# Create the form.
$form = New-Object Windows.Forms.Form
$form.ClientSize = New-Object Drawing.Size(400, 200)
$form.Text = "Responsive Windows Form"
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedSingle"
# Create the button.
$button = New-Object Windows.Forms.Button
$button.Location = New-Object Drawing.Point(100, 20)
$button.Width = 180
$button.Text = "Start Counting"
$button.Add_Click({PoSHCounter})
# Create the label.
$label = New-Object Windows.Forms.Label
$label.Location = New-Object Drawing.Point(125, 60)
$label.Font = New-Object System.Drawing.Font("Consolas",63,[System.Drawing.FontStyle]::Bold)
$label.ForeColor = [System.Drawing.Color]::DarkBlue
$label.AutoSize = $True
$label.Text = 10
# Add controls to the form.
$form.Controls.AddRange(@($button, $label))
# Show the form.
[Windows.Forms.Application]::Run($form)
Now, by default, any graphical interface created via PowerShell will run in a single-threaded apartment; which means that the application will be blocked, or become unresponsive, while it is waiting for a task to complete.
Responsive Windows Form with runspace
The best way of overcoming this restriction is to leverage .NET runspaces (each runspace provides a separate execution context for a PowerShell pipeline).
The following code provides a simplified example of how I used runspaces to manage the execution of a long running task.
- The script creates a form with a button and a text label.
- The button initiates a script block that counts to 99.
- The button is intentionally disabled during this period.
- The window remains responsive throughout, and the label is incrementally refreshed to show the value of the counter.
Add-Type -AssemblyName System.Windows.Forms
# Long running task with runspace
function PoSHCounter {
# The main script block
$scriptBlock = {
# Disable the button
$sync.button.Enabled = $false
for ($i = 10; $i -le 99; $i++) {
$sync.label.Text = $i
Start-Sleep -Milliseconds 50
}
# Re-enable the button when the loop finishes.
$sync.button.Enabled = $true
}
# Create a PowerShell instance with the script block
$psInstance = [PowerShell]::Create().AddScript($scriptBlock)
# Create then open a new runspace
$runspace = [RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
# Add shared data to the runspace
$runspace.SessionStateProxy.SetVariable("sync", $sync)
# Associate the PowerShell instance with the opened runspace
$psInstance.Runspace = $runspace
# Execute the script asynchronously using BeginInvoke() method
$psInstance.BeginInvoke()
}
# Create the form.
$form = New-Object Windows.Forms.Form
$form.ClientSize = New-Object Drawing.Size(400, 200)
$form.Text = "Responsive Windows Form"
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedSingle"
# Create the button.
$button = New-Object Windows.Forms.Button
$button.Location = New-Object Drawing.Point(100, 20)
$button.Width = 180
$button.Text = "Start Counting"
$button.Add_Click({PoSHCounter})
# Create the label.
$label = New-Object Windows.Forms.Label
$label.Location = New-Object Drawing.Point(125, 60)
$label.Font = New-Object System.Drawing.Font("Consolas",63,[System.Drawing.FontStyle]::Bold)
$label.ForeColor = [System.Drawing.Color]::DarkBlue
$label.AutoSize = $True
$label.Text = 10
# For talking across runspaces (share info between runspaces)
$sync = [hashtable]::Synchronized(@{})
$sync.runspace = $runspace
$sync.host = $host
$sync.form = $form
$sync.button = $button
$sync.label = $label
# Add controls to the form.
$form.Controls.AddRange(@($sync.button, $sync.label))
# Show the form.
[Windows.Forms.Application]::Run($form)
As you can see in the above screenshot. This time, when the job is running ( in the background) the GUI can be moved normally.
In the next section, I’ve created some examples of using runspace to do some tasks in Windows.
PowerShell runspace examples
Example 1: Ping a host:
- The script creates a form with a button and a textbox.
- The button initiates a script block to ping a host
- The button is intentionally disabled during this period.
- The window remains responsive throughout, and the textbox shows the status.
Add-Type -AssemblyName System.Windows.Forms
# Long running task with runspace
function PoSHCounter {
# The main script block
$scriptBlock = {
# Disable the button
$sync.button.Enabled = $false
$sync.textbox.Text = "Pinging to the host..."
$sync.textbox.AppendText([Environment]::NewLine)
$sync.textbox.AppendText("Completed. Below are the results:")
$sync.textbox.AppendText([Environment]::NewLine)
$sync.textbox.AppendText((powershell -noprofile -Command "ping 127.0.0.1" | Out-String))
# Re-enable the button when the loop finishes.
$sync.button.Enabled = $true
}
# Create a PowerShell instance with the script block
$psInstance = [PowerShell]::Create().AddScript($scriptBlock)
# Create then open a new runspace
$runspace = [RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
# Add shared data to the runspace
$runspace.SessionStateProxy.SetVariable("sync", $sync)
# Associate the PowerShell instance with the opened runspace
$psInstance.Runspace = $runspace
# Execute the script asynchronously using BeginInvoke() method
$psInstance.BeginInvoke()
}
# Create the form.
$form = New-Object Windows.Forms.Form
$form.ClientSize = New-Object Drawing.Size(772, 380)
$form.Text = "Responsive Windows Form"
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedSingle"
# Create the button.
$button = New-Object Windows.Forms.Button
$button.Location = New-Object Drawing.Point(20, 20)
$button.BackColor = [System.Drawing.Color]::Green
$button.ForeColor = [System.Drawing.Color]::White
$button.Size = New-Object System.Drawing.Size(150,40)
$button.Text = "Start Pinging"
$button.Add_Click({PoSHCounter})
# Create a textbox to display the output
$textbox = New-Object system.Windows.Forms.TextBox
$textbox.Multiline = $true
$textbox.Text = "Waiting for output..."
$textbox.Font = New-Object System.Drawing.Font("Consolas",9,[System.Drawing.FontStyle]::Regular)
$textbox.Size = New-Object System.Drawing.Size(728,270)
$textbox.Location = New-Object System.Drawing.Point(20,70)
$textbox.BackColor = "#1F1F1F"
$textbox.ForeColor = 'Cyan'
# For talking across runspaces (share info between runspaces)
$sync = [hashtable]::Synchronized(@{})
$sync.runspace = $runspace
$sync.host = $host
$sync.form = $form
$sync.button = $button
$sync.label = $label
$sync.textbox = $textbox
# Add controls to the form.
$form.Controls.AddRange(@($sync.button, $sync.label, $sync.textbox))
# Show the form.
[Windows.Forms.Application]::Run($form)
Example 2: Download a file using PowerShell
- The script creates a form with a button and a textbox.
- The button initiates a script block to download a file from the internet.
- The button is intentionally disabled during this period.
- The window remains responsive throughout, and the textbox shows the download status.
Add-Type -AssemblyName System.Windows.Forms
# Long running task with runspace
function PoSHCounter {
# The main script block
$scriptBlock = {
# Disable the button
$sync.button.Enabled = $false
$sync.textbox.Text = "Downloading..."
$sync.textbox.AppendText([Environment]::NewLine)
$source = 'http://ipv4.download.thinkbroadband.com/1MB.zip'
$destination = 'D:\temp\1MB.zip'
Invoke-WebRequest -Uri $source -OutFile $destination
$sync.textbox.AppendText("Download completed")
$sync.textbox.AppendText([Environment]::NewLine)
$sync.textbox.AppendText("The downloaded file is saved to:" + "$(Get-ChildItem -Path D:\temp | Out-String)")
# Re-enable the button when the loop finishes.
$sync.button.Enabled = $true
}
# Create a PowerShell instance with the script block
$psInstance = [PowerShell]::Create().AddScript($scriptBlock)
# Create then open a new runspace
$runspace = [RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
# Add shared data to the runspace
$runspace.SessionStateProxy.SetVariable("sync", $sync)
# Associate the PowerShell instance with the opened runspace
$psInstance.Runspace = $runspace
# Execute the script asynchronously using BeginInvoke() method
$psInstance.BeginInvoke()
}
# Create the form.
$form = New-Object Windows.Forms.Form
$form.ClientSize = New-Object Drawing.Size(772, 380)
$form.Text = "Responsive Windows Form"
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedSingle"
# Create the button.
$button = New-Object Windows.Forms.Button
$button.Location = New-Object Drawing.Point(20, 20)
$button.BackColor = [System.Drawing.Color]::Green
$button.ForeColor = [System.Drawing.Color]::White
$button.Size = New-Object System.Drawing.Size(150,40)
$button.Text = "Start Pinging"
$button.Add_Click({PoSHCounter})
# Create a textbox to display the output
$textbox = New-Object system.Windows.Forms.TextBox
$textbox.Multiline = $true
$textbox.Text = "Waiting for output..."
$textbox.Font = New-Object System.Drawing.Font("Consolas",9,[System.Drawing.FontStyle]::Regular)
$textbox.Size = New-Object System.Drawing.Size(728,270)
$textbox.Location = New-Object System.Drawing.Point(20,70)
$textbox.BackColor = "#1F1F1F"
$textbox.ForeColor = 'Cyan'
# For talking across runspaces (share info between runspaces)
$sync = [hashtable]::Synchronized(@{})
$sync.runspace = $runspace
$sync.host = $host
$sync.form = $form
$sync.button = $button
$sync.label = $label
$sync.textbox = $textbox
# Add controls to the form.
$form.Controls.AddRange(@($sync.button, $sync.label, $sync.textbox))
# Show the form.
[Windows.Forms.Application]::Run($form)
Example 3: Get the list of processes
- The script creates a form with a button and a textbox.
- The button initiates a script block to get the first five processes on a computer.
- The button is intentionally disabled during this period.
- The window remains responsive throughout, and the textbox shows the results.
Add-Type -AssemblyName System.Windows.Forms
# Long running task with runspace
function PoSHCounter {
# The main script block
$scriptBlock = {
# Disable the button
$sync.button.Enabled = $false
$sync.textbox.AppendText([Environment]::NewLine)
$sync.textbox.AppendText((Get-Process | Select-Object -First 5 | Out-String))
# Re-enable the button when the loop finishes.
$sync.button.Enabled = $true
}
# Create a PowerShell instance with the script block
$psInstance = [PowerShell]::Create().AddScript($scriptBlock)
# Create then open a new runspace
$runspace = [RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
# Add shared data to the runspace
$runspace.SessionStateProxy.SetVariable("sync", $sync)
# Associate the PowerShell instance with the opened runspace
$psInstance.Runspace = $runspace
# Execute the script asynchronously using BeginInvoke() method
$psInstance.BeginInvoke()
}
# Create the form.
$form = New-Object Windows.Forms.Form
$form.ClientSize = New-Object Drawing.Size(772, 380)
$form.Text = "Responsive Windows Form"
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedSingle"
# Create the button.
$button = New-Object Windows.Forms.Button
$button.Location = New-Object Drawing.Point(20, 20)
$button.BackColor = [System.Drawing.Color]::Green
$button.ForeColor = [System.Drawing.Color]::White
$button.Size = New-Object System.Drawing.Size(150,40)
$button.Text = "Submit"
$button.Add_Click({PoSHCounter})
# Create a textbox to display the output
$textbox = New-Object system.Windows.Forms.TextBox
$textbox.Multiline = $true
$textbox.Text = "Waiting for output..."
$textbox.Font = New-Object System.Drawing.Font("Consolas",9,[System.Drawing.FontStyle]::Regular)
$textbox.Size = New-Object System.Drawing.Size(728,270)
$textbox.Location = New-Object System.Drawing.Point(20,70)
$textbox.BackColor = "#1F1F1F"
$textbox.ForeColor = 'Cyan'
# For talking across runspaces (share info between runspaces)
$sync = [hashtable]::Synchronized(@{})
$sync.runspace = $runspace
$sync.host = $host
$sync.form = $form
$sync.button = $button
$sync.label = $label
$sync.textbox = $textbox
# Add controls to the form.
$form.Controls.AddRange(@($sync.button, $sync.label, $sync.textbox))
# Show the form.
[Windows.Forms.Application]::Run($form)
Adding controls to the form
There are a lot of controls that we can use to create a GUI for a PowerShell script. You can follow the below link to get more details.
Not a reader? Watch this related video tutorial: