Creating WPF GUI Applications with Pure PowerShell

When creating a GUI application in PowerShell, I usually use Visual Studio, or Blend for Visual Studio, to design a WPF application, then copy and run the XAML code in PowerShell.  Designing in VS is generally easier and quicker and creates less code, but it is also perfectly possible to create a WPF GUI using pure PowerShell, which is more akin to the Windows Forms method of GUI creation.  For more complex applications that’s not the ideal way because it takes longer and creates a lot more code, but for simple applications, or if you are used to designing something in Windows Forms, why not give it a go?  You simply need to create a window from the [System.Windows] .Net namespace, then add some controls from the [System.Windows.Controls] namespace, and you’re away!

Here’s a simple example application that displays the running processes on your machine, the list of services, and a little game “Click the Fruit” as a bonus 😉

capture


# Add required assembly
Add-Type -AssemblyName PresentationFramework

# Create a Window
$Window = New-Object Windows.Window
$Window.Height = "670"
$Window.Width = "700"
$Window.Title = "PowerShell WPF Window"
$window.WindowStartupLocation="CenterScreen"

# Create a grid container with 2 rows, one for the buttons, one for the datagrid
$Grid =  New-Object Windows.Controls.Grid
$Row1 = New-Object Windows.Controls.RowDefinition
$Row2 = New-Object Windows.Controls.RowDefinition
$Row1.Height = "70"
$Row2.Height = "100*"
$grid.RowDefinitions.Add($Row1)
$grid.RowDefinitions.Add($Row2)

# Create a button to get running Processes
$Button_Processes = New-Object Windows.Controls.Button
$Button_Processes.SetValue([Windows.Controls.Grid]::RowProperty,0)
$Button_Processes.Height = "50"
$Button_Processes.Width = "150"
$Button_Processes.Margin = "10,10,10,10"
$Button_Processes.HorizontalAlignment = "Left"
$Button_Processes.VerticalAlignment = "Top"
$Button_Processes.Content = "Get Processes"
$Button_Processes.Background = "Aquamarine"

# Create a button to get services
$Button_Services = New-Object Windows.Controls.Button
$Button_Services.SetValue([Windows.Controls.Grid]::RowProperty,0)
$Button_Services.Height = "50"
$Button_Services.Width = "150"
$Button_Services.Margin = "180,10,10,10"
$Button_Services.HorizontalAlignment = "Left"
$Button_Services.VerticalAlignment = "Top"
$Button_Services.Content = "Get Services"
$Button_Services.Background = "Aquamarine"

# Create a button to play Click the fruit
$Button_Cool = New-Object Windows.Controls.Button
$Button_Cool.SetValue([Windows.Controls.Grid]::RowProperty,0)
$Button_Cool.Height = "50"
$Button_Cool.Width = "150"
$Button_Cool.Margin = "350,10,10,10"
$Button_Cool.HorizontalAlignment = "Left"
$Button_Cool.VerticalAlignment = "Top"
$Button_Cool.Content = "Play 'Click the Fruit'"
$Button_Cool.Background = "Aquamarine"

# Create a datagrid
$DataGrid = New-Object Windows.Controls.DataGrid
$DataGrid.SetValue([Windows.Controls.Grid]::RowProperty,1)
$DataGrid.MinHeight = "100"
$DataGrid.MinWidth = "100"
$DataGrid.Margin = "10,0,10,10"
$DataGrid.HorizontalAlignment = "Stretch"
$DataGrid.VerticalAlignment = "Stretch"
$DataGrid.VerticalScrollBarVisibility = "Auto"
$DataGrid.GridLinesVisibility = "none"
$DataGrid.IsReadOnly = $true

# Add the elements to the relevant parent control
$Grid.AddChild($DataGrid)
$grid.AddChild($Button_Processes)
$grid.AddChild($Button_Services)
$grid.AddChild($Button_Cool)
$window.Content = $Grid

# Add an event on the Get Processes button
$Button_Processes.Add_Click({
    $Processes = Get-Process | Select ProcessName,HandleCount,NonpagedSystemMemorySize,PrivateMemorySize,WorkingSet,UserProcessorTime,Id
    $DataGrid.ItemsSource = $Processes
    })

# Add an event on the Get Services button
$Button_Services.Add_Click({
    $Services = Get-Service | Select Name,ServiceName,Status,StartType
    $DataGrid.ItemsSource = $Services
    })

# Add an event to play Click the fruit
$Button_Cool.Add_Click({

    $Fruit = @{
        Apples = "Green"
        Bananas = "Yellow"
        Oranges = "Orange"
        Plums = "Maroon"
    }

    $Fruit.GetEnumerator() | Foreach {
        # Create a transparent window at a random location on the screen
        $NewWindow = New-Object Windows.Window
        $NewWindow.SizeToContent = "WidthAndHeight"
        $NewWindow.AllowsTransparency = $true
        $NewWindow.WindowStyle = "none"
        $NewWindow.Background = "Transparent"
        $NewWindow.WindowStartupLocation = "Manual"
        $Height = Get-Random -Maximum (([System.Windows.SystemParameters]::PrimaryScreenHeight) - 100)
        $Width = Get-Random -Maximum (([System.Windows.SystemParameters]::PrimaryScreenWidth) - 100)
        $NewWindow.Left = $Width
        $NewWindow.Top = $Height

        # Add a label control for the fruit
        $NewLabel = New-Object Windows.Controls.Label
        $NewLabel.Height = "150"
        $NewLabel.Width = "400"
        $NewLabel.Content = $_.Name
        $NewLabel.FontSize = "100"
        $NewLabel.FontWeight = "Bold"
        $NewLabel.Foreground = $_.Value
        $NewLabel.Background = "Transparent"
        $NewLabel.Opacity = "100"

        # Add an event to close the window when clicked
        $NewWindow.Add_MouseDown({
            $This.Close()
        })

        # Add an event to change the cursor to a hand when the mouse goes over the window
        $NewWindow.Add_MouseEnter({
        $this.Cursor = "Hand"
        })

        # Display the window
        $NewWindow.Content = $NewLabel
        $NewWindow.ShowDialog()
    }
})

# Show the window
if (!$psISE)
{
    # Hide PS console window
    $windowcode = '[DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);'
    $asyncwindow = Add-Type -MemberDefinition $windowcode -name Win32ShowWindowAsync -namespace Win32Functions -PassThru
    $null = $asyncwindow::ShowWindowAsync((Get-Process -PID $pid).MainWindowHandle, 0) 

    # Run as an application in it's own context
    $app = [Windows.Application]::new()
    $app.Run($Window)
}
Else
{
    [void]$window.ShowDialog()
}

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
Part I – Creating PowerShell GUIs in Minutes using Visual Studio – A New Hope
PowerShell and WPF: Writing Data to a UI From a Different Runspace
Using Background Runspaces Instead of PSJobs For Better Performance