Stacking Up with WPF
Let's continue looking at how to create simple Windows Presentation Foundation (WPF) tools in PowerShell. This is a handy way to give users a graphical interface to run your scripts. WPF can be daunting as it is a complicated .NET developer topic. However, I have found that I can get by with a simple WPF form in many cases. It may not be the fanciest looking, but it is relatively easy to code. I want to show you how to create a stacked panel WPF form.
As I mentioned last time, WPF is based on the idea of layers. You need a base window to begin with.
$window = [System.Windows.Window]@{
Title = 'WPF Window Demo'
Height = 350
Width = 500
WindowStartupLocation = 'CenterScreen'
}
Don't forget you need to load the required assemblies first.
if ($IsWindows -OR $PSEdition -eq 'desktop') {
Try {
Add-Type -AssemblyName PresentationFramework -ErrorAction Stop
Add-Type -AssemblyName PresentationCore -ErrorAction Stop
}
Catch {
#Failsafe error handling
Throw $_
Return
}
} else {
Write-Warning 'This requires Windows PowerShell or PowerShell Core on Windows.'
#Bail out
Return
}
Stack Panel
Instead of adding controls to the window, we're going to add them to a stack panel.
$stack = New-Object System.Windows.Controls.StackPanel
Let's see how to use this stack panel to create a simple form we can display using PowerShell. Using Get-TypeMember
as I did in the last article, I can discover the control's methods and properties.

To make it easier to see the stack, I'm going to give it a color.
$stack.Background = 'LightBlue'
I will eventually add the stackpanel to the window but first I want to add controls to the stackpanel. As the name implies, each control will be layered on top of the other like a tiered cake or a stack of Lego blocks.
Labels
I'll define a label control.
$lblName = [System.Windows.Controls.Label]@{
Content = 'Computername'
FontSize = 16
Foreground = 'OrangeRed'
}
I'm using the alternate syntax to define the object. I can set other properties later, but I know I want to create the label with these properties to begin. You could also use Get-TypeMember
or Get-Member
to discover more about this control.
Adding the control to the stack is very simple.
$stack.AddChild($lblName)
Text Box
I want to add one more control, a text box.
$txtComputerName = [System.Windows.Controls.TextBox]@{
Width = $window.Width - 200
HorizontalAlignment = 'Left'
Text = $env:COMPUTERNAME
ToolTip = 'Enter the computer name'
#adjust spacing "left, top, right, bottom"
Margin = '5,2,0,0'
}
Notice how I'm using the other control's properties set properties. I recommend that you create meaningful variable names for your controls. I typically use a short prefix to indicate the type of control such as lbl
for label and txt
for text box. Followed by an identifier. This makes it easier to remember what the control is for. It is hard to remember what $TextBox1
is for. The variable $txtComputerName
is much more descriptive.
When using a TextBox
you'll often refer to the Text
property to get or set the value. In this example, I'm setting a default value.
Many controls have a ToolTip
property. This is a small pop-up window that appears when the user hovers over the control. It is a good way to provide additional information or instructions. I'll set a value for this control.
Finally, I'll add this control to the stack.
$stack.AddChild($txtComputerName)
I want to see what I have so far, so I'll add the stack to the window and show it.
$window.AddChild($stack)
[void]$window.ShowDialog()
I saved all of the code snippets into a single script file that I can run.
PS C:\Users...\wpf-stack\> .\demo-stack-1.ps1

I'm going to continue building this form to provide a list of services and display details based on a selected service. I'll begin by adding another label to the stack.
$lblService = [System.Windows.Controls.Label]@{
Content = 'Select a Service'
FontSize = 16
Foreground = 'OrangeRed'
}
$stack.AddChild($lblService)
Radio Button
I want to give the user the option to select running or stopped services, so I'll provide two radio buttons.
$radioRunning = [System.Windows.Controls.RadioButton]@{
Content = 'Running'
GroupName = 'ServiceStatus'
IsChecked = $true
Margin = '5,0,0,2'
}
$radioStopped = [System.Windows.Controls.RadioButton]@{
Content = 'Stopped'
GroupName = 'ServiceStatus'
IsChecked = $False
Margin = '5,0,0,2'
}
These buttons are intended to be mutually exclusive so I've assigned them to the same group. I've also set the IsChecked
property to $true
for the first button. This will make it the default selection. I've also set a margin to give the buttons a little space in the stack.
I'll add these to the stack.
$stack.AddChild($radioRunning)
$stack.AddChild($radioStopped)
List Box
I want to display a list of services. I'll use a list box control.
$listDisplayName = [System.Windows.Controls.ListBox]@{
Width = $window.Width - 150
Height = $window.Height - 200
HorizontalAlignment = 'Left'
ToolTip = 'Select a service'
Margin = '5,2,0,0'
}
You can control the height to limit how many items are displayed. Scrollbars will be added dynamically as needed.
But now, I need to populate the list box with some data. I'm going to define a helper function to handle this since I may need to re-populate the list depending on what radio button is selected.
function populateServiceList {
$listDisplayName.Items.Clear()
if ($radioRunning.IsChecked) {
$filter = "State = 'Running'"
} else {
$filter = "State = 'Stopped'"
}
$cimParams = @{
ClassName = "Win32_Service"
ComputerName = $txtComputerName.Text
Filter = $filter
Property = 'DisplayName'
}
Get-CimInstance @cimParams | Sort-Object -Property DisplayName |
ForEach-Object { [void]$listDisplayName.Items.Add($_.DisplayName) }
}
In the function, I can reference the controls. I'm using the IsChecked
property to test if the radio button is checked or not and dynamically building a filter. The meat of the function is using the Add()
method on the Items
property.
[void]$listDisplayName.Items.Add($_.DisplayName)
The method will write the index number to the pipeline, which I don't need to see. That's why I am using [void]
.
I can use this function for event handlers on the radio buttons.
$radioRunning.Add_checked({populateServiceList })
$radioStopped.Add_checked({populateServiceList})
If the user clicks a radio button, the list box will be repopulated based on the selection.
In my script, I can invoke the function to initially populate the list.
populateServiceList
Button
The last thing I need is a button to initiate the primary action.
$btnGetDetail = [System.Windows.Controls.Button]@{
Content = 'Get Detail'
Width = 75
Height = 25
HorizontalAlignment = 'Center'
Margin = '0,5,0,0'
}
I won't bother with a Quit
button since I can use the window's menu bar to close the form. But I will need code to handle the button click.
$btnGetDetail.Add_Click({
$splat = @{
ComputerName = $txtComputerName.Text
ClassName = 'Win32_Service'
Filter = "DisplayName = '$($listDisplayName.SelectedItem)'"
}
Get-CimInstance @splat | Select-Object -Property * -ExcludeProperty CIM* | Out-String | Write-Host
})
The code will build a filter using the selected item from the list. Remember, the pipeline is blocked but I can use Write-Host
to display the output in the hosting application. I could have displayed the information in the form, but I'll save that for next time.
A Practical StackPanel Example
Here's the complete code.
#requires -version 5.1
if ($IsWindows -OR $PSEdition -eq 'desktop') {
Try {
Add-Type -AssemblyName PresentationFramework -ErrorAction Stop
Add-Type -AssemblyName PresentationCore -ErrorAction Stop
}
Catch {
#Failsafe error handling
Throw $_
Return
}
} else {
Write-Warning 'This requires Windows PowerShell or PowerShell Core on Windows.'
#Bail out
Return
}
#helper function
function populateServiceList {
$listDisplayName.Items.Clear()
if ($radioRunning.IsChecked) {
$filter = "State = 'Running'"
}
else {
$filter = "State = 'Stopped'"
}
$cimParams = @{
ClassName = 'Win32_Service'
ComputerName = $txtComputerName.Text
Filter = $filter
Property = 'DisplayName'
}
Get-CimInstance @cimParams | Sort-Object -Property DisplayName |
ForEach-Object { [void]$listDisplayName.Items.Add($_.DisplayName) }
}
$window = [System.Windows.Window]@{
Title = 'Service Detail'
Height = 350
Width = 500
WindowStartupLocation = 'CenterScreen'
}
#stack panels
$stack = New-Object System.Windows.Controls.StackPanel
$stack.Background = 'LightBlue'
#add a label
$lblName = [System.Windows.Controls.Label]@{
Content = 'Computername'
FontSize = 16
Foreground = 'OrangeRed'
}
$stack.AddChild($lblName)
$txtComputerName = [System.Windows.Controls.TextBox]@{
Width = $window.Width - 300
HorizontalAlignment = 'Left'
Text = $env:COMPUTERNAME
ToolTip = 'Enter the computer name'
#adjust spacing "left, top, right, bottom"
Margin = '25,2,0,0'
}
$stack.AddChild($txtComputerName)
$lblService = [System.Windows.Controls.Label]@{
Content = 'Select a Service'
FontSize = 16
Foreground = 'OrangeRed'
}
$stack.AddChild($lblService)
#add a radio button
$radioRunning = [System.Windows.Controls.RadioButton]@{
Content = 'Running'
GroupName = 'ServiceStatus'
IsChecked = $true
Margin = '25,0,0,2'
}
$radioStopped = [System.Windows.Controls.RadioButton]@{
Content = 'Stopped'
GroupName = 'ServiceStatus'
IsChecked = $False
Margin = '25,0,0,2'
}
$radioRunning.Add_checked({ populateServiceList })
$radioStopped.Add_checked({ populateServiceList })
$stack.AddChild($radioRunning)
$stack.AddChild($radioStopped)
#add a list box
$listDisplayName = [System.Windows.Controls.ListBox]@{
Width = $window.Width - 150
Height = $window.Height - 200
HorizontalAlignment = 'Left'
ToolTip = 'Select a service'
Margin = '25,2,0,0'
}
#Populate the listbox
populateServiceList
$stack.AddChild($listDisplayName)
#add a button
$btnGetDetail = [System.Windows.Controls.Button]@{
Content = 'Get Detail'
Width = 75
Height = 25
HorizontalAlignment = 'Center'
Margin = '0,5,0,0'
}
$btnGetDetail.Add_Click({
$splat = @{
ComputerName = $txtComputerName.Text
ClassName = 'Win32_Service'
Filter = "DisplayName = '$($listDisplayName.SelectedItem)'"
}
Get-CimInstance @splat | Select-Object -Property * -ExcludeProperty CIM* | Out-String | Write-Host
})
$stack.AddChild($btnGetDetail)
#add the stack to the window
$window.AddChild($stack)
#show the window
[void]$window.ShowDialog()

I can select a service and click the button to display details. I can also change the radio button to toggle between running and stopped services.
I adjusted the margins to give the controls a little more space and I could probably adjust other dimensions, but that is pretty easy to do.
Summary
Using a stack panel is the fastest way, I think, to create a WPF-based script. Experienced readers will recognize there are more efficient ways to achieve the same results. Consider this example an educational example and not a candidate for production use.
We're just getting warmed up on WPF, so please leave your comments and questions.