PowerShell Tip: Utilizing Runspaces for Responsive WPF GUI Applications

For those with limited C# or VB.Net programming skills, using PowerShell with XAML code is a great way to create a simple GUI application.  But because the PowerShell host is single-threaded, all your code must run sequentially.  When you have long running commands, this can cause the GUI to freeze and be unresponsive, and you can’t do anything else in the GUI until the running command has finished.

But thanks to runspaces, it is possible to do multi-threading and run several commands in parallel.  We can send individual commands to their own runspace, or to a runspace pool, and because they are executing outside of the host application’s runspace (eg console or ISE), the GUI will be much more responsive and not suffer from locks and freezing.

Take for example this simple WPF application I built, which uses PowerShell’s Test-Connection cmdlet to “ping” up to 5 servers and get the network latency, or response delay:

CAPTURE2

In the count box, I select the number of times I want to “ping” the machine.  I can ping machines individually, or ping them all.  Because the code is running in a single thread, I have to wait for one ping to finish before I can ping another machine.  In fact, once I click Ping, I cannot do anything else in the GUI until the first ping has finished.  If I select “5” in the count box, for example (it will test the connection to the machine about once a second), it will take at least 5 seconds before I can do anything else.  if I click Ping all, and all machines have a count of 5, the GUI will freeze and it takes at least 25 seconds for the results to appear, because PowerShell will ping each machine sequentially, and then update the GUI application with the results.

CAPTURE3

Test it for yourself with the following code:


Add-Type –assemblyName PresentationFramework

#Build the GUI
[xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="PowerShell Runspace Demo" Height="283" Width="782" WindowStartupLocation = "CenterScreen">
    <Grid Margin="0,0,0,-1">
        <Button x:Name="Ping1" Content="Ping" HorizontalAlignment="Left" Margin="119,146,0,0" VerticalAlignment="Top" Width="93" Height="31"/>
        <Button x:Name="Ping2" Content="Ping" HorizontalAlignment="Left" Margin="255,146,0,0" VerticalAlignment="Top" Width="93" Height="31"/>
        <Button x:Name="Ping3" Content="Ping" HorizontalAlignment="Left" Margin="387,146,0,0" VerticalAlignment="Top" Width="93" Height="31"/>
        <Button x:Name="Ping4" Content="Ping" HorizontalAlignment="Left" Margin="524,146,0,0" VerticalAlignment="Top" Width="93" Height="31"/>
        <Button x:Name="Ping5" Content="Ping" HorizontalAlignment="Left" Margin="656,146,0,0" VerticalAlignment="Top" Width="93" Height="31"/>
        <TextBox x:Name="ComputerName1" HorizontalAlignment="Left" Height="23" Margin="105,79,0,0" TextWrapping="Wrap" Text="SERVER-01" VerticalAlignment="Top" Width="120"/>
        <TextBox x:Name="ComputerName2" HorizontalAlignment="Left" Height="23" Margin="243,79,0,0" TextWrapping="Wrap" Text="SERVER-02" VerticalAlignment="Top" Width="120"/>
        <TextBox x:Name="ComputerName3" HorizontalAlignment="Left" Height="23" Margin="374,79,0,0" TextWrapping="Wrap" Text="SERVER-03" VerticalAlignment="Top" Width="120"/>
        <TextBox x:Name="ComputerName4" HorizontalAlignment="Left" Height="23" Margin="509,79,0,0" TextWrapping="Wrap" Text="SERVER-04" VerticalAlignment="Top" Width="120"/>
        <TextBox x:Name="ComputerName5" HorizontalAlignment="Left" Height="23" Margin="640,79,0,0" TextWrapping="Wrap" Text="SERVER-05" VerticalAlignment="Top" Width="120"/>
        <ComboBox x:Name="Count1" HorizontalAlignment="Left" Margin="137,107,0,0" VerticalAlignment="Top" Width="56" Height="34">
            <ComboBoxItem Content="1"/>
            <ComboBoxItem Content="2"/>
            <ComboBoxItem Content="3"/>
            <ComboBoxItem Content="4"/>
            <ComboBoxItem Content="5"/>
            <ComboBoxItem Content="6"/>
            <ComboBoxItem Content="7"/>
            <ComboBoxItem Content="8"/>
        </ComboBox>
        <ComboBox x:Name="Count2" HorizontalAlignment="Left" Margin="274,107,0,0" VerticalAlignment="Top" Width="56" Height="34">
            <ComboBoxItem Content="1"/>
            <ComboBoxItem Content="2"/>
            <ComboBoxItem Content="3"/>
            <ComboBoxItem Content="4"/>
            <ComboBoxItem Content="5"/>
            <ComboBoxItem Content="6"/>
            <ComboBoxItem Content="7"/>
            <ComboBoxItem Content="8"/>
        </ComboBox>
        <ComboBox x:Name="Count3" HorizontalAlignment="Left" Margin="403,107,0,0" VerticalAlignment="Top" Width="56" Height="34">
            <ComboBoxItem Content="1"/>
            <ComboBoxItem Content="2"/>
            <ComboBoxItem Content="3"/>
            <ComboBoxItem Content="4"/>
            <ComboBoxItem Content="5"/>
            <ComboBoxItem Content="6"/>
            <ComboBoxItem Content="7"/>
            <ComboBoxItem Content="8"/>
        </ComboBox>
        <ComboBox x:Name="Count4" HorizontalAlignment="Left" Margin="540,107,0,0" VerticalAlignment="Top" Width="56" Height="34">
            <ComboBoxItem Content="1"/>
            <ComboBoxItem Content="2"/>
            <ComboBoxItem Content="3"/>
            <ComboBoxItem Content="4"/>
            <ComboBoxItem Content="5"/>
            <ComboBoxItem Content="6"/>
            <ComboBoxItem Content="7"/>
            <ComboBoxItem Content="8"/>
        </ComboBox>
        <ComboBox x:Name="Count5" HorizontalAlignment="Left" Margin="669,107,0,0" VerticalAlignment="Top" Width="56" Height="34">
            <ComboBoxItem Content="1"/>
            <ComboBoxItem Content="2"/>
            <ComboBoxItem Content="3"/>
            <ComboBoxItem Content="4"/>
            <ComboBoxItem Content="5"/>
            <ComboBoxItem Content="6"/>
            <ComboBoxItem Content="7"/>
            <ComboBoxItem Content="8"/>
        </ComboBox>
        <TextBox x:Name="Result1" HorizontalAlignment="Left" Height="56" Margin="128,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="75" FontSize="18"/>
        <TextBox x:Name="Result2" HorizontalAlignment="Left" Height="56" Margin="264,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="75" FontSize="18"/>
        <TextBox x:Name="Result3" HorizontalAlignment="Left" Height="56" Margin="397,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="75" FontSize="18"/>
        <TextBox x:Name="Result4" HorizontalAlignment="Left" Height="56" Margin="535,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="75" FontSize="18"/>
        <TextBox x:Name="Result5" HorizontalAlignment="Left" Height="56" Margin="669,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="75" FontSize="18"/>
        <Label Content="ComputerName:" HorizontalAlignment="Left" Margin="3,75,0,0" VerticalAlignment="Top" Height="26" Width="97"/>
        <Label Content="Count:" HorizontalAlignment="Left" Margin="3,107,0,0" VerticalAlignment="Top" Height="26" Width="94"/>
        <Label Content="Avg Latency (ms):" HorizontalAlignment="Left" Margin="0,182,0,0" VerticalAlignment="Top" Height="26" Width="111"/>
        <Label Content="Runspace Demo: Test-Connection" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="338" FontSize="20" FontWeight="Bold"/>
        <Button x:Name="Pingall" Content="Ping all" HorizontalAlignment="Left" Margin="656,14,0,0" VerticalAlignment="Top" Width="89" Height="37" FontWeight="Bold"/>
    </Grid>
</Window>
"@

$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Window=[Windows.Markup.XamlReader]::Load( $reader )

function NullCount {
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic') | Out-Null
[Microsoft.VisualBasic.Interaction]::MsgBox("Please select a ping count first",'OKOnly,Information',"Ping")
}

function Ping {
param($result,$count,$ComputerName)
$result.Clear()
if ($count.SelectedItem.Content -eq $null)
    {NullCount; break}
$Con = Test-Connection -ComputerName $($ComputerName.Text) -Count $count.SelectedItem.Content
$avg = [math]::Round(($con.ResponseTime | measure -Average).Average)
$result.AddText($avg)
}

# XAML objects
# ComputerNames
$ComputerName1 = $Window.FindName("ComputerName1")
$ComputerName2 = $Window.FindName("ComputerName2")
$ComputerName3 = $Window.FindName("ComputerName3")
$ComputerName4 = $Window.FindName("ComputerName4")
$ComputerName5 = $Window.FindName("ComputerName5")
# Count
$Count1 = $Window.FindName("Count1")
$Count2 = $Window.FindName("Count2")
$Count3 = $Window.FindName("Count3")
$Count4 = $Window.FindName("Count4")
$Count5 = $Window.FindName("Count5")
# Ping buttons
$Ping1 = $Window.FindName("Ping1")
$Ping2 = $Window.FindName("Ping2")
$Ping3 = $Window.FindName("Ping3")
$Ping4 = $Window.FindName("Ping4")
$Ping5 = $Window.FindName("Ping5")
$Pingall = $Window.FindName("Pingall")
# Result boxes
$Result1 = $Window.FindName("Result1")
$Result2 = $Window.FindName("Result2")
$Result3 = $Window.FindName("Result3")
$Result4 = $Window.FindName("Result4")
$Result5 = $Window.FindName("Result5")

# Click Actions
$Ping1.Add_Click(
    {
        Ping -result $Result1 -count $Count1 -ComputerName $ComputerName1
    })

$Ping2.Add_Click(
    {
        Ping -result $Result2 -count $Count2 -ComputerName $ComputerName2
    })

$Ping3.Add_Click(
    {
        Ping -result $Result3 -count $Count3 -ComputerName $ComputerName3
    })

$Ping4.Add_Click(
    {
        Ping -result $Result4 -count $Count4 -ComputerName $ComputerName4
    })

$Ping5.Add_Click(
    {
        Ping -result $Result5 -count $Count5 -ComputerName $ComputerName5
    })

$Pingall.Add_Click(
    {
        if ($count1.SelectedItem.Content -eq $null -or $count2.SelectedItem.Content -eq $null -or $count3.SelectedItem.Content -eq $null -or $count4.SelectedItem.Content -eq $null -or $count5.SelectedItem.Content -eq $null)
            {NullCount; break}
        Ping -result $Result1 -count $Count1 -ComputerName $ComputerName1
        Ping -result $Result2 -count $Count2 -ComputerName $ComputerName2
        Ping -result $Result3 -count $Count3 -ComputerName $ComputerName3
        Ping -result $Result4 -count $Count4 -ComputerName $ComputerName4
        Ping -result $Result5 -count $Count5 -ComputerName $ComputerName5
    })

$Window.ShowDialog() | Out-Null

To get around this delay, I can create a new runspace for each time I call the function which runs the Test-Connection cmdlet.  This means the command itself is run in a separate instance and the host runspace is not used.  Usually when running a command in another runspace, it is still necessary for the host runspace to wait for the command to complete in order to receive the results.  However, in this case, because we are running a GUI we are actually able to pass the result straight to the GUI from within the separate runspace.  This means the host PowerShell instance does not need to wait for the results, and therefore the GUI application itself remains responsive and does not suffer any freezing or delay, which is very cool 🙂

Sounds complicated, so lets explain further.

First we create a synchronized hashtable, which is a “thread-safe” way to store and access objects and properties by any runspace.  Then we load the XAML code (created using Visual Studio) for the WPF app into a window, adding it to the hashtable so it can be accessed in another runspace:


$syncHash = [hashtable]::Synchronized(@{})
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )

Next, inside a function, we create and open a new single-threaded runspace and pass any variables we need to it, including the hashtable:


$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ApartmentState = "STA"
$Runspace.ThreadOptions = "ReuseThread"
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$Runspace.SessionStateProxy.SetVariable("count",$count)
$Runspace.SessionStateProxy.SetVariable("ComputerName",$ComputerName)
$Runspace.SessionStateProxy.SetVariable("TargetBox",$TargetBox)

Next we define the code that will execute in the runspace:


$code = {
    $syncHash.Window.Dispatcher.invoke(
    [action]{ $syncHash.$TargetBox.Clear() })
    $Con = Test-Connection -ComputerName $ComputerName -Count $count
    $average = [math]::Round(($con.ResponseTime | measure -Average).Average)
    $syncHash.Window.Dispatcher.invoke(
    [action]{ $syncHash.$TargetBox.Text = $average }
    )
}

We use the Window.Dispatcher.Invoke() method to send a command to the GUI app window. First I send a command to clear the textbox that contains the latency value if it has been populated before.  Next, I run the test-connection cmdlet and then calculate the average response time.  I then pass this result to the GUI textbox.  Because the WPF app window is included in the synchronized hashtable, any runspace we create, including the host runspace, can use it.

Finally, we create an instance of PowerShell in the runspace, and invoke the code.


$PSinstance = [powershell]::Create().AddScript($Code)
$PSinstance.Runspace = $Runspace
$job = $PSinstance.BeginInvoke()

Using runspaces in this way means I can click on any of the ping commands in the GUI and I don’t have to wait for it to finish before I click on another button, or do something else, because the background work is essentially being “offloaded”.

Further, we can wrap the entire application in a separate runspace too, which means the host application that created it, ie the PowerShell console or ISE, can continue to be used for other commands while the GUI is running.  You can even interact with the GUI itself from the console window.

Here’s the full code for the app utilizing runspaces, so you can see the difference in responsiveness for yourself:


Add-Type –assemblyName PresentationFramework

$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ApartmentState = "STA"
$Runspace.ThreadOptions = "ReuseThread" 
$Runspace.Open()

$code = {

#Build the GUI
[xml]$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="PowerShell Runspace Demo" Height="283" Width="782" WindowStartupLocation = "CenterScreen">
    <Grid Margin="0,0,0,-1">
        <Button x:Name="Ping1" Content="Ping" HorizontalAlignment="Left" Margin="119,146,0,0" VerticalAlignment="Top" Width="93" Height="31"/>
        <Button x:Name="Ping2" Content="Ping" HorizontalAlignment="Left" Margin="255,146,0,0" VerticalAlignment="Top" Width="93" Height="31"/>
        <Button x:Name="Ping3" Content="Ping" HorizontalAlignment="Left" Margin="387,146,0,0" VerticalAlignment="Top" Width="93" Height="31"/>
        <Button x:Name="Ping4" Content="Ping" HorizontalAlignment="Left" Margin="524,146,0,0" VerticalAlignment="Top" Width="93" Height="31"/>
        <Button x:Name="Ping5" Content="Ping" HorizontalAlignment="Left" Margin="656,146,0,0" VerticalAlignment="Top" Width="93" Height="31"/>
        <TextBox x:Name="ComputerName1" HorizontalAlignment="Left" Height="23" Margin="105,79,0,0" TextWrapping="Wrap" Text="SERVER-01" VerticalAlignment="Top" Width="120"/>
        <TextBox x:Name="ComputerName2" HorizontalAlignment="Left" Height="23" Margin="243,79,0,0" TextWrapping="Wrap" Text="SERVER-02" VerticalAlignment="Top" Width="120"/>
        <TextBox x:Name="ComputerName3" HorizontalAlignment="Left" Height="23" Margin="374,79,0,0" TextWrapping="Wrap" Text="SERVER-03" VerticalAlignment="Top" Width="120"/>
        <TextBox x:Name="ComputerName4" HorizontalAlignment="Left" Height="23" Margin="509,79,0,0" TextWrapping="Wrap" Text="SERVER-04" VerticalAlignment="Top" Width="120"/>
        <TextBox x:Name="ComputerName5" HorizontalAlignment="Left" Height="23" Margin="640,79,0,0" TextWrapping="Wrap" Text="SERVER-05" VerticalAlignment="Top" Width="120"/>
        <ComboBox x:Name="Count1" HorizontalAlignment="Left" Margin="137,107,0,0" VerticalAlignment="Top" Width="56" Height="34">
            <ComboBoxItem Content="1"/>
            <ComboBoxItem Content="2"/>
            <ComboBoxItem Content="3"/>
            <ComboBoxItem Content="4"/>
            <ComboBoxItem Content="5"/>
            <ComboBoxItem Content="6"/>
            <ComboBoxItem Content="7"/>
            <ComboBoxItem Content="8"/>
        </ComboBox>
        <ComboBox x:Name="Count2" HorizontalAlignment="Left" Margin="274,107,0,0" VerticalAlignment="Top" Width="56" Height="34">
            <ComboBoxItem Content="1"/>
            <ComboBoxItem Content="2"/>
            <ComboBoxItem Content="3"/>
            <ComboBoxItem Content="4"/>
            <ComboBoxItem Content="5"/>
            <ComboBoxItem Content="6"/>
            <ComboBoxItem Content="7"/>
            <ComboBoxItem Content="8"/>
        </ComboBox>
        <ComboBox x:Name="Count3" HorizontalAlignment="Left" Margin="403,107,0,0" VerticalAlignment="Top" Width="56" Height="34">
            <ComboBoxItem Content="1"/>
            <ComboBoxItem Content="2"/>
            <ComboBoxItem Content="3"/>
            <ComboBoxItem Content="4"/>
            <ComboBoxItem Content="5"/>
            <ComboBoxItem Content="6"/>
            <ComboBoxItem Content="7"/>
            <ComboBoxItem Content="8"/>
        </ComboBox>
        <ComboBox x:Name="Count4" HorizontalAlignment="Left" Margin="540,107,0,0" VerticalAlignment="Top" Width="56" Height="34">
            <ComboBoxItem Content="1"/>
            <ComboBoxItem Content="2"/>
            <ComboBoxItem Content="3"/>
            <ComboBoxItem Content="4"/>
            <ComboBoxItem Content="5"/>
            <ComboBoxItem Content="6"/>
            <ComboBoxItem Content="7"/>
            <ComboBoxItem Content="8"/>
        </ComboBox>
        <ComboBox x:Name="Count5" HorizontalAlignment="Left" Margin="669,107,0,0" VerticalAlignment="Top" Width="56" Height="34">
            <ComboBoxItem Content="1"/>
            <ComboBoxItem Content="2"/>
            <ComboBoxItem Content="3"/>
            <ComboBoxItem Content="4"/>
            <ComboBoxItem Content="5"/>
            <ComboBoxItem Content="6"/>
            <ComboBoxItem Content="7"/>
            <ComboBoxItem Content="8"/>
        </ComboBox>
        <TextBox x:Name="Result1" HorizontalAlignment="Left" Height="56" Margin="128,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="75" FontSize="18"/>
        <TextBox x:Name="Result2" HorizontalAlignment="Left" Height="56" Margin="264,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="75" FontSize="18"/>
        <TextBox x:Name="Result3" HorizontalAlignment="Left" Height="56" Margin="397,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="75" FontSize="18"/>
        <TextBox x:Name="Result4" HorizontalAlignment="Left" Height="56" Margin="535,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="75" FontSize="18"/>
        <TextBox x:Name="Result5" HorizontalAlignment="Left" Height="56" Margin="669,182,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="75" FontSize="18"/>
        <Label Content="ComputerName:" HorizontalAlignment="Left" Margin="3,75,0,0" VerticalAlignment="Top" Height="26" Width="97"/>
        <Label Content="Count:" HorizontalAlignment="Left" Margin="3,107,0,0" VerticalAlignment="Top" Height="26" Width="94"/>
        <Label Content="Avg Latency (ms):" HorizontalAlignment="Left" Margin="0,182,0,0" VerticalAlignment="Top" Height="26" Width="111"/>
        <Label Content="Runspace Demo: Test-Connection" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="338" FontSize="20" FontWeight="Bold"/>
        <Button x:Name="Pingall" Content="Ping all" HorizontalAlignment="Left" Margin="656,14,0,0" VerticalAlignment="Top" Width="89" Height="37" FontWeight="Bold"/>
    </Grid>
</Window>
"@

$syncHash = [hashtable]::Synchronized(@{})
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )

function RunspacePing {
param($syncHash,$Count,$ComputerName,$TargetBox)
if ($Count -eq $null)
    {NullCount; break}

$syncHash.Host = $host
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ApartmentState = "STA"
$Runspace.ThreadOptions = "ReuseThread" 
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash) 
$Runspace.SessionStateProxy.SetVariable("count",$count)
$Runspace.SessionStateProxy.SetVariable("ComputerName",$ComputerName)
$Runspace.SessionStateProxy.SetVariable("TargetBox",$TargetBox)

$code = {
    $syncHash.Window.Dispatcher.invoke(
    [action]{ $syncHash.$TargetBox.Clear() })
    $Con = Test-Connection -ComputerName $ComputerName -Count $count
    $average = [math]::Round(($con.ResponseTime | measure -Average).Average)
    $syncHash.Window.Dispatcher.invoke(
    [action]{ $syncHash.$TargetBox.Text = $average }
    )
}
$PSinstance = [powershell]::Create().AddScript($Code)
$PSinstance.Runspace = $Runspace
$job = $PSinstance.BeginInvoke()
}

function NullCount {
[System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic') | Out-Null
[Microsoft.VisualBasic.Interaction]::MsgBox("Please select a ping count first",'OKOnly,Information',"Ping")
}

# XAML objects
# ComputerNames
$syncHash.ComputerName1 = $syncHash.Window.FindName("ComputerName1")
$syncHash.ComputerName2 = $syncHash.Window.FindName("ComputerName2")
$syncHash.ComputerName3 = $syncHash.Window.FindName("ComputerName3")
$syncHash.ComputerName4 = $syncHash.Window.FindName("ComputerName4")
$syncHash.ComputerName5 = $syncHash.Window.FindName("ComputerName5")
# Count
$syncHash.Count1 = $syncHash.Window.FindName("Count1")
$syncHash.Count2 = $syncHash.Window.FindName("Count2")
$syncHash.Count3 = $syncHash.Window.FindName("Count3")
$syncHash.Count4 = $syncHash.Window.FindName("Count4")
$syncHash.Count5 = $syncHash.Window.FindName("Count5")
# Ping buttons
$syncHash.Ping1 = $syncHash.Window.FindName("Ping1")
$syncHash.Ping2 = $syncHash.Window.FindName("Ping2")
$syncHash.Ping3 = $syncHash.Window.FindName("Ping3")
$syncHash.Ping4 = $syncHash.Window.FindName("Ping4")
$syncHash.Ping5 = $syncHash.Window.FindName("Ping5")
$syncHash.Pingall = $syncHash.Window.FindName("Pingall")
# Result boxes
$syncHash.Result1 = $syncHash.Window.FindName("Result1")
$syncHash.Result2 = $syncHash.Window.FindName("Result2")
$syncHash.Result3 = $syncHash.Window.FindName("Result3")
$syncHash.Result4 = $syncHash.Window.FindName("Result4")
$syncHash.Result5 = $syncHash.Window.FindName("Result5")

# Click Actions
$syncHash.Ping1.Add_Click(
    {
        RunspacePing -syncHash $syncHash -count $syncHash.Count1.SelectedItem.Content -ComputerName $syncHash.ComputerName1.Text -TargetBox "Result1"
    })

$syncHash.Ping2.Add_Click(
    {
        RunspacePing -syncHash $syncHash -count $syncHash.Count2.SelectedItem.Content -ComputerName $syncHash.ComputerName2.Text -TargetBox "Result2"
    })

$syncHash.Ping3.Add_Click(
    {
        RunspacePing -syncHash $syncHash -count $syncHash.Count3.SelectedItem.Content -ComputerName $syncHash.ComputerName3.Text -TargetBox "Result3"
    })

$syncHash.Ping4.Add_Click(
    {
        RunspacePing -syncHash $syncHash -count $syncHash.Count4.SelectedItem.Content -ComputerName $syncHash.ComputerName4.Text -TargetBox "Result4"
    })

$syncHash.Ping5.Add_Click(
    {
        RunspacePing -syncHash $syncHash -count $syncHash.Count5.SelectedItem.Content -ComputerName $syncHash.ComputerName5.Text -TargetBox "Result5"
    })

$syncHash.Pingall.Add_Click(
    {
        if ($syncHash.count1.SelectedItem.Content -eq $null -or $syncHash.count2.SelectedItem.Content -eq $null -or $syncHash.count3.SelectedItem.Content -eq $null -or $syncHash.count4.SelectedItem.Content -eq $null -or $syncHash.count5.SelectedItem.Content -eq $null)
            {NullCount; break}
        RunspacePing -syncHash $syncHash -count $syncHash.Count1.SelectedItem.Content -ComputerName $syncHash.ComputerName1.Text -TargetBox "Result1"
        RunspacePing -syncHash $syncHash -count $syncHash.Count2.SelectedItem.Content -ComputerName $syncHash.ComputerName2.Text -TargetBox "Result2"
        RunspacePing -syncHash $syncHash -count $syncHash.Count3.SelectedItem.Content -ComputerName $syncHash.ComputerName3.Text -TargetBox "Result3"
        RunspacePing -syncHash $syncHash -count $syncHash.Count4.SelectedItem.Content -ComputerName $syncHash.ComputerName4.Text -TargetBox "Result4"
        RunspacePing -syncHash $syncHash -count $syncHash.Count5.SelectedItem.Content -ComputerName $syncHash.ComputerName5.Text -TargetBox "Result5"
    })

$syncHash.Window.ShowDialog()
$Runspace.Close()
$Runspace.Dispose()

}

$PSinstance1 = [powershell]::Create().AddScript($Code)
$PSinstance1.Runspace = $Runspace
$job = $PSinstance1.BeginInvoke()

I haven’t covered the details of creating a WPF application in PowerShell in this post, but there are some good blogs online that cover the essentials:

http://learn-powershell.net/category/wpf/
http://blogs.technet.com/b/heyscriptingguy/archive/2014/08/01/i-39-ve-got-a-powershell-secret-adding-a-gui-to-scripts.aspx
http://foxdeploy.com/2015/04/10/part-i-creating-powershell-guis-in-minutes-using-visual-studio-a-new-hope/
http://learn-powershell.net/2012/10/14/powershell-and-wpf-writing-data-to-a-ui-from-a-different-runspace/
http://learn-powershell.net/2012/05/13/using-background-runspaces-instead-of-psjobs-for-better-performance/

7 thoughts on “PowerShell Tip: Utilizing Runspaces for Responsive WPF GUI Applications

  1. Hello,
    Nice script, but it looks like these four lines can be safely removed:

    $syncHash1 = [hashtable]::Synchronized(@{})
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $syncHash1.Window=[Windows.Markup.XamlReader]::Load( $reader )

    $syncHash1.Host = $host

    I somehow didnt get, where is actually that $SyncHash1 var used,
    moreover if you try to run this: $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    before defining $xaml, it will throw error.

    Thank you for explanation in advance.

    🙂

    1. Hi Martin, you are absolutely right, those lines are not needed, and I’ve updated the script. I think I must have left them in there from some previous testing and forgot about them!

      Thanks for pointing it out 🙂

  2. Hi,

    Working on powershell based GUI application which connect to multiple vcenter servers (each vcenter is separate button) additional click on button disconnect session with vcenter, GUI has 3 additional buttons to create/delete/schedule snapshot. Main problem was hang/freeze on GUI when powerCLI modules need to load or when snapshots are executed with large number of servers.

    i was totally inspired by your article…

    Re worked whole script to use runspaces with synchronized $syncHash, everything works fine, however i want more….

    At this moment every time when button is pressed for vcenter connect/disconnect snapshot execution powercli modules need to be loaded as every time new runspace is created (is it possible to connect to already existing runspace within same process?) Powercli loading is time consuming (at least by GUI is responsive)

    Had idea…

    Open out of process runspace load powercli module to this process and use it for script execution, enter to freshly created separate powershell processes using Enter-PSHostProcess, problem is that I’m unable to send data back to main GUI outputbox.

    https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/enter-pshostprocess?view=powershell-6

    That my main question is there a way to use synchronized hash table $syncHash between runspaces running on a separate powershell processes? i guess it’s not possible… in this case is there a other way to achieve my goal? What about using/entering already existing runspaces(not out of proccess)? is that possible?

    get-runspace command output:

    Id Name ComputerName Type State Availability
    — —- ———— —- —– ————
    1 Runspace1 localhost Local Opened Busy
    2 Runspace2 localhost Local Opened Busy
    **3 Runspace3 localhost Local Opened Available**

    Here is code sample to create out of process runspace

    function test {
    param($syncHash)

    $test=[System.Management.Automation.Runspaces.TypeTable]::GetDefaultTypeFiles()
    $test2 = [System.Management.Automation.Runspaces.PowerShellProcessInstance]::new();
    $syncHash.Host = $host
    $Runspace = [runspacefactory]::CreateOutOfProcessRunspace($test,$test2)
    $Runspace.ApartmentState = “STA”
    $Runspace.ThreadOptions = “ReuseThread”
    $Runspace.Open()
    $Runspace.SessionStateProxy.SetVariable(“syncHash”,$syncHash)

    $code ={
    #Enter-PSHostProcess -id 2404
    $syncHash.Window.Dispatcher.invoke(
    [action]{
    $syncHash.outputbox.Text += (get-date -format G) + “`ttest…`r`n”
    })

    }

    $PSinstance1 = [powershell]::Create().AddScript($Code)
    $PSinstance1.Runspace = $Runspace
    $job = $PSinstance1.BeginInvoke()

    }

    Here is sample of code where runspace is created locally and that work fine

    function test2 {
    param($syncHash)

    $syncHash.Host = $host
    $Runspace = [runspacefactory]::CreateRunspace()
    $Runspace.ApartmentState = “STA”
    $Runspace.ThreadOptions = “ReuseThread”
    $Runspace.Open()
    $Runspace.SessionStateProxy.SetVariable(“syncHash”,$syncHash)

    $code ={
    $syncHash.Window.Dispatcher.invoke(
    [action]{
    $syncHash.outputbox.Text += (get-date -format G) + “`ttest2…`r`n”
    })

    }

    $PSinstance1 = [powershell]::Create().AddScript($Code)
    $PSinstance1.Runspace = $Runspace
    $job = $PSinstance1.BeginInvoke()

    }

  3. How do you prevent memory consumption from climbing? I’m guessing the runspaces need to be closed somehow. Would you happen to know how to do this?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.