I recently wrote a PowerShell script that used the Windows Forms .NET assembly 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.
Now, by default, any graphical interface created via PowerShell will run in a single-threaded apartment; which basically means that the application will "block", or become unresponsive, while it is waiting for a task to complete. 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 20 over a period of 20 seconds (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.
[Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms") | Out-Null
Add-Type -AssemblyName System.Windows.Forms
# for talking across runspaces.
$sync = [Hashtable]::Synchronized(@{})
# long running task.
$counter = {
$count = [PowerShell]::Create().AddScript({
$sync.button.Enabled = $false
for ($i = 0; $i -le 20; $i++) {
$sync.label.Text = $i
start-sleep -seconds 1
}
$sync.button.Enabled = $true
})
$runspace = [RunspaceFactory]::CreateRunspace()
$runspace.ApartmentState = "STA"
$runspace.ThreadOptions = "ReuseThread"
$runspace.Open()
$runspace.SessionStateProxy.SetVariable("sync", $sync)
$count.Runspace = $runspace
$count.BeginInvoke()
}
# create the form.
$form = New-Object Windows.Forms.Form
$form.ClientSize = New-Object Drawing.Size(200, 60)
$form.Text = "Counter"
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedSingle"
$form.MaximizeBox = $false
# create the button.
$button = New-Object Windows.Forms.Button
$button.Location = New-Object Drawing.Point(10, 10)
$button.Width = 180
$button.Text = "Start Counting"
$button.Add_Click($counter)
# create the label.
$label = New-Object Windows.Forms.Label
$label.Location = New-Object Drawing.Point(10, 38)
$label.Width = 100
$label.Text = 0
# add controls to the form.
$sync.button = $button
$sync.label = $label
$form.Controls.AddRange(@($sync.button, $sync.label))
# show the form.
[Windows.Forms.Application]::Run($form)