Building Cleaner, Responsive WPF Forms

In my first two posts on this subject, I was just getting started with learning responsive WPF form building. I’m here today to show you a better way to build a responsive WPF using runspaces that will do the exact same thing as my previous uploads showed. Just better.

This time, I won’t be putting the form into its own runspace. As I learnt you didn’t need to from JRV over on the TechNet forums. This has some benefits that I will very helpfully, briefly and probably incorrectly list below:

  • Don’t have to use syncHash when updating the form
  • One less runspace for the form
  • Having a form in its own runspace creates additional overhead and possible errors

So to display the form I would use something like the below:

[xml]$xml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="Counter" Height="119" Width="351.5" ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
        <Label Name="Label" Content="0" HorizontalAlignment="Left" Margin="16.666,9.333,0,0" VerticalAlignment="Top" FontSize="18"/>
        <Button Name="Button" Content="Start" HorizontalAlignment="Center" VerticalAlignment="Top" Width="75" Margin="123.25,63,123.25,0"/>
    </Grid>
</Window>
"@

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

$Label = $Window.FindName("Label")
$Button = $Window.FindName("Button")

$Window.ShowDialog() | Out-Null

Which will give us the below form:

But what if I want the button press to make the label number increase? I would use something like this on the button press:

[xml]$xml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="Counter" Height="119" Width="351.5" ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
        <Label Name="Label" Content="0" HorizontalAlignment="Left" Margin="16.666,9.333,0,0" VerticalAlignment="Top" FontSize="18"/>
        <Button Name="Button" Content="Start" HorizontalAlignment="Center" VerticalAlignment="Top" Width="75" Margin="123.25,63,123.25,0"/>
    </Grid>
</Window>
"@

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

$Label = $Window.FindName("Label")
$Button = $Window.FindName("Button")

$Button.Add_Click({
    $counter = 1 

    do{
        Start-Sleep -Milliseconds 5
        $label.content = $counter
        [System.Windows.Forms.Application]::DoEvents()
        $counter += 1
    }while ($counter -le 5000)

})

$Window.ShowDialog() | Out-Null

This produces a form which increases the label up to 5000 when the button is pressed. You can see this below:

But what if I want to actually run something in a runspace. For example, test the connection to google.com? Then I would use the below code:

[xml]$xml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="Counter" Height="119" Width="351.5" ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
        <Label Name="Label" Content="0" HorizontalAlignment="Left" Margin="16.666,9.333,0,0" VerticalAlignment="Top" FontSize="18"/>
        <Button Name="Button" Content="Start" HorizontalAlignment="Center" VerticalAlignment="Top" Width="75" Margin="123.25,63,123.25,0"/>
    </Grid>
</Window>
"@

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

$Label = $Window.FindName("Label")
$Button = $Window.FindName("Button")

$Button.Add_Click({
    $syncHash = [hashtable]::Synchronized(@{})
    $Runspace = [runspacefactory]::CreateRunspace()
    $Runspace.ApartmentState = "STA"
    $Runspace.ThreadOptions = "ReuseThread"
    $Runspace.Open()
    $Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash)

    $powershell = [powershell]::Create().AddScript({
        $connection = Test-Connection -ComputerName google.com -Count 5
        $syncHash.output = [math]::Round(($connection.ResponseTime | Measure-Object -Average).Average)
    })

    $powershell.Runspace = $Runspace

    $Object = $powershell.BeginInvoke()

    do {
        Start-Sleep -Milliseconds 50
        [System.Windows.Forms.Application]::DoEvents()
    }while(!$Object.IsCompleted)

    $powershell.EndInvoke($Object)
    $powershell.Dispose()

    $label.Content = $syncHash.output
})

$Window.ShowDialog() | Out-Null

All the above examples will stay responsive whilst the action is performed. There are a couple of different methods to do this as you can see above, for anything that takes some time to complete, a runspace is needed. But when you are updating the form quickly, like the counter, then no runspace is needed. 

Enjoy!

Responsive PowerShell WPF Form Introduction #2

Following on from my last post, I’m going to show you how to update a textbox using a button on the same form. I will be adding the following code starting on line 38:

#BUTTON LOGIC
$syncHash.Button.Add_Click({

    $syncHash.Window.Dispatcher.Invoke(
        [action]{
            $syncHash.TextBox.AppendText("This is a test")
        }
    )
})

This is fairly basic in what it does. It just adds “This is a test” to the textbox. Say if I want the button to run a task and then update the textbox with the results, but the results took a long time to come, the form would freeze. This is because whatever command you run in the same runspace as the GUI, takes controls and stops the GUI being responsive.

So, what I’m going to do is ping google 5 times, get the average from all of those and then update the textbox without the GUI becoming unresponsive. To do this, I’m going to create a new runspace and add the code I want to run. You can see this below:

#BUTTON LOGIC
$syncHash.Button.Add_Click({
    #ASSIGNING HOST VARIABLE
    $syncHash.host = $Host
    #CREATING NEW RUNSPACE
    $pingrunspace = [runspacefactory]::CreateRunspace()
    $pingrunspace.ApartmentState = "STA"
    $pingrunspace.ThreadOptions = "ReuseThread"
    $pingrunspace.Open()
    #PUTTING THE SYNCHASH VARIABLE INSIDE THE NEW RUNSPACE
    $pingrunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)

    #THIS IS THE CODE THAT WILL BE EXECUTED IN THE NEW RUNSPACE
    $code = {

        #CONNECTION TO GOOGLE AND CALCULATING AVERAGE IN NEW RUNSPACE
        $connection = Test-Connection -ComputerName google.co.uk -Count 5
        $average = [math]::Round(($connection.responsetime | Measure-Object -Average).Average)
        #UPDATING THE TEXTBOX WITH CONNECTION AVERAGE IN NEW RUNSPACE
        $syncHash.Window.Dispatcher.Invoke(
            [action]{
                $syncHash.TextBox.AppendText($average)
            }
        )

    }
        
    #ADDING AND RUNNING THE CODE IN THE NEW RUNSPACE
    $PSInstance = [powershell]::Create().AddScript($code)
    $PSinstance.Runspace = $pingrunspace
    $job = $PSinstance.BeginInvoke()
    
})

This will run the code in a separate runspace to the GUI and allow you to interact with it whilst the commands complete in the background.

Just in case you want the entire this, this is what the whole file looks like 🙂

#CREATE HASHTABLE AND RUNSPACE FOR GUI
$syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"         
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)      
#BUILD GUI AND ADD TO RUNSPACE CODE
$psCmd = [PowerShell]::Create().AddScript({   
    [xml]$xaml = @"
    <Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Name="Window" Height="400" Width="600">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <Button Name="Button" Content="Press" Height="200" Width="580" Grid.Row="0" Grid.Column="0" />
        <TextBox Name="Textbox" Height="200" Width="580" Grid.Row="1" Grid.Column="0" />
    </Grid>
</Window>
"@
  
    #INTERPRET AND LOAD THE GUI
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )

    #EXTRACT THE CONTROLS FROM THE GUI
    $syncHash.TextBox = $syncHash.window.FindName("Textbox")
    $syncHash.Button = $syncHash.Window.FindName("Button")

    #BUTTON LOGIC
    $syncHash.Button.Add_Click({

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

        $code = {

            $connection = Test-Connection -ComputerName google.co.uk -Count 5
            $average = [math]::Round(($connection.responsetime | Measure-Object -Average).Average)
            $syncHash.Window.Dispatcher.Invoke(
                [action]{
                    $syncHash.TextBox.AppendText($average)
                }
            )

        }

        $PSInstance = [powershell]::Create().AddScript($code)
        $PSinstance.Runspace = $pingrunspace
        $job = $PSinstance.BeginInvoke()
    })


    #FINALISE AND CLOSE GUI RUNSPACE UPON EXITING
    $syncHash.Window.ShowDialog() | Out-Null
    $syncHash.Error = $Error
    $Runspace.Close()
    $Runspace.Dispose()
    
})
#LOAD RUNSPACE WITH GUI IN
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

Enjoy!

Responsive PowerShell WPF Form Introduction #1

Hooooly jebus chwist! This took a LONG time for me to get my head around and an even longer time to implement and get working (still breaking it every minute!). I used this website and this website to help me learn the basics.

Today, I’m going to show you how to create a responsive WPF from using PowerShell. This utilises runspaces and a synchronised hashta… never mind the technical stuff!

This is the code that I used:

#CREATE HASHTABLE AND RUNSPACE FOR GUI
$syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"         
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)      
#BUILD GUI AND ADD TO RUNSPACE CODE
$psCmd = [PowerShell]::Create().AddScript({   
    [xml]$xaml = @"
    <Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Name="Window" Height="400" Width="600">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>

        <Button Name="Button" Content="Press" Height="200" Width="580" Grid.Row="0" Grid.Column="0" />
        <TextBox Name="Textbox" Height="200" Width="580" Grid.Row="1" Grid.Column="0" />
    </Grid>
</Window>
"@
  
    #INTERPRET AND LOAD THE GUI
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $syncHash.Window=[Windows.Markup.XamlReader]::Load( $reader )

    #EXTRACT THE CONTROLS FROM THE GUI
    $syncHash.TextBox = $syncHash.window.FindName("Textbox")
    $syncHash.Button = $syncHash.Window.FindName("Button")

    #FINALISE AND CLOSE GUI RUNSPACE UPON EXITING
    $syncHash.Window.ShowDialog() | Out-Null
    $syncHash.Error = $Error
    $Runspace.Close()
    $Runspace.Dispose()
    
})
#LOAD RUNSPACE WITH GUI IN
$psCmd.Runspace = $newRunspace
$data = $psCmd.BeginInvoke()

Using this, you can then use the same command prompt used to launch the script to change the form. E.g. to change the text in the textbox we would use:

$syncHash.Window.Dispatcher.Invoke(
    [action]{$syncHash.TextBox.Text = "Updated text here"}
)

In another post, I’ll show you how to update the textbox using a button on the same form. Exciting stuff, right?

Leave a comment if you have any questions or issues. Enjoy!