There comes a time when we must build PowerShell tools with a GUI for ourselves and others and embrace the mouse click. The best way to do this today is with Windows Presentation Framework (WPF). WPF is an XML-based language that PowerShell can read using .NET's Extensible Application Markup Language (XAML) reader.

PowerShell comes with assemblies that allow it to understand many .NET classes natively, but WPF isn't one of them. To work with WPF in PowerShell, you must first add the assembly into your current session using the Add-Type command.

Add-Type -AssemblyName PresentationFramework

Once you've loaded the assembly, you can now begin creating a basic window. WPF represents Windows in XML, so you'll have all of the familiar XML properties like namespaces. To create the most basic window in WPF, I'm going to create an XML string and cast it to an XML object using [xml]:

[xml]$xaml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="Window"
/>
"@

After I have created the XAML string, I need to create an object the XamlReader class understands. To do that, I will pass it to the XmlNodeReader class as an argument. This will create an XmlNodeReader object for me.

$reader = (New-Object System.Xml.XmlNodeReader $xaml)

I'll then pass the XmlNodeReader object to the Load() static method on the XamlReader class to create our window and then use the ShowDialog() method to display the window on the screen. This should display an extremely simple window.

$window = [Windows.Markup.XamlReader]::Load($reader)
$window.ShowDialog()

Next, I can add an element to this window. I've chosen to use a grid element. I will add the grid element inside the XAML and specify a few row and column definitions.

[xml]$xaml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="Window">
    <Grid x:Name="Grid">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
    </Grid>
</Window>
"@
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
$window.ShowDialog()

When you run this, you'll notice nothing looks different. Trust me. There is a grid in the window, but there are no elements inside of the grid. Let's fix that by adding a text box in the first column and first row of the grid and display the window again.

[xml]$xaml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="Window">
    <Grid x:Name="Grid">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name = "PathTextBox"
            Width="150"
            Grid.Column="0"
            Grid.Row="0"
        />
    </Grid>
</Window>
"@
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
$window.ShowDialog()

At this point, you should now see a window with a single text box inside it.

Blank WPF form with text box

Blank WPF form with text box

At this point, we can begin adding elements to the window. Notice this time instead of using a TextBox element, I'm using Button elements referencing the Content properties to display the label and referencing the grid column and row positions like before.

[xml]$xaml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="Window">
    <Grid x:Name="Grid">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name = "PathTextBox"
            Width="150"
            Grid.Column="0"
            Grid.Row="0"
        />
        <Button x:Name = "ValidateButton"
            Content="Validate"
            Grid.Column="1"
            Grid.Row="0"
        />
        <Button x:Name = "RemoveButton"
            Content="Remove"
            Grid.Column="0"
            Grid.Row="1"
        />
    </Grid>
</Window>
"@
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)
$window.ShowDialog()
WPF Window with buttons

WPF Window with buttons

Notice that the buttons snapped automagically to the grid position we have defined.

But WPF GUIs are not just about displaying elements. We can also attach actions to various elements on our form. For example, perhaps I'm using the text box to specify a file path. When I click Validate, I want to run some PowerShell code to test whether that file exists. To do that, I have to associate my PowerShell code to the WPF element somehow.

Notice in the example I've been using, each element has a Name. I'll use the validation button I created called ValidateButton. Knowing the name of the element, I can use the FindName() method on the $window object to get a reference to that individual element.

For example, to get a reference to the ValidateButton element, I would use $validateButton = $window.FindName("ValidateButton"). This then creates an object that allows me to define various methods the user may perform. For example, the user can click a button, and the button element has an Add_Click event we can use to tell the element to execute some code when this event fires.

Adding the below code tells WPF to clear the text box only if the Test-Path command returns $false.

$validateButton = $window.FindName("ValidateButton")
$pathTextBox = $window.FindName("PathTextBox")
$ValidateButton.Add_Click({
    If(-not (Test-Path $pathTextBox.Text)){
        $pathTextBox.Text = ""
    }
})

We can also perform other actions like removing the file from the file system only if it exists when clicking on the Remove button.

$removeButton = $window.FindName("RemoveButton")
$removeButton.Add_Click({
    If($pathTextBox.Text){
        If(Test-Path $pathTextBox.Text){
            Remove-Item $pathTextBox.Text
        }
    }
})

You can now add all of this code to your script, which ends up looking like the code below:

[xml]$xaml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name="Window">
    <Grid x:Name="Grid">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <TextBox x:Name = "PathTextBox"
            Width="150"
            Grid.Column="0"
            Grid.Row="0"
        />
        <Button x:Name = "ValidateButton"
            Content="Validate"
            Grid.Column="1"
            Grid.Row="0"
        />
        <Button x:Name = "RemoveButton"
            Content="Remove"
            Grid.Column="0"
            Grid.Row="1"
        />
    </Grid>
</Window>
"@
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$window = [Windows.Markup.XamlReader]::Load($reader)

$validateButton = $window.FindName("ValidateButton")
$removeButton = $window.FindName("RemoveButton")
$pathTextBox = $window.FindName("PathTextBox")

$ValidateButton.Add_Click({
    If(-not (Test-Path $pathTextBox.Text)){
        $pathTextBox.Text = ""
    }
})

$removeButton.Add_Click({
    If($pathTextBox.Text){
        If(Test-Path $pathTextBox.Text){
            Remove-Item $pathTextBox.Text
        }
    }
})

$window.ShowDialog()

After running this code, you now have a form with a text box, a Validate button and a Remove button. If you press the Validate button, PowerShell will test to see whether the value inside of the text box exists as a file. If not, the text value will disappear. If you press the Remove button and the file exists, it will remove the file!

Subscribe to 4sysops newsletter!

We've just scratched the surface with what you can do with WPF and PowerShell. If you'd like to learn more about WPF and PowerShell, I encourage you to check out the Pluralsight course Building PowerShell GUIs in WPF for Free. It covers the techniques we've gone over here as well as many other tasks you can perform with WPF and PowerShell.

avatar
4 Comments
  1. Clayton (Rank 2) 5 years ago

    By creating GUIs with PowerShell in this manner, is it reliant on the .NET framework version a user is using?

    • mike 4 years ago

      There may be Updates. So why not just update to the latest version?

  2. Klaus 9 months ago

    Hi Adam,

    thanks a lot for this explanation!

    I have already read another good post describing how to create the GUIs for Powershell using Visual Studio (https://adamtheautomator.com/powershell-gui). But now I understand the process behind it much better!

    I still have one problem though:
    As long as the control.Add_Click block is processed, the form is not updated 🙁
    I like to use a log function to put information about what is happening into a label and/or a listbox.
    When I call this function inside the Add_Click block, nothing is displayed until the block is done.
    When I use a write-host in the log-function, I see it working in the console, but nothing on the form.
    Is there anything I can do to force the form or the label or listbox to refresh?

    Thanks!
    Klaus

    • Klaus 9 months ago

      As is often the case, as soon as I wrote this post, I found the solution.

      With this function I can, for example, write a label at runtime:

      $Text = "Hello World"
          
          $window.Dispatcher.Invoke(
              [action]{$StatusLabel.content = "$($Text)"},
              "Render"
          )
      
      or a text box:
      
          $window.Dispatcher.Invoke(
              [action]{$StatusLabel.AddText("$($Text)")},
             "Render"
          )

      etc.

      Thanks for your patience 😉

Leave a reply

Your email address will not be published.

*

© 4sysops 2006 - 2023

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account