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:
 
			 
								 
								


