WPF PowerShell Applications
In the last article, I demonstrated how to use common WPF controls in your script. There is a lot of code to write to create a WPF-based PowerShell script, and there is a lot of trial and error to get everything positioned where you want it. I will show you an alternative way to handle the visuals in a future article. But today, I want to show a few more WPF features you might want to incorporate. I also need to discuss some of the challenges you face when running a WPF-based PowerShell script.
WPF is a developer tool for creating graphical applications complete with menus, toolbars, and buttons. As PowerShell scripters we rarely need to create a full-blown WPF application. If you do, you would be better off creating a native .NET application. Our scripting needs tend to be a bit more modest. A WPF-based script is a great way to create an interface for a user to input data for your PowerShell function or script. Although, the WPF script can also be a complete application that you happen to create with PowerShell.
One additional element you might want to include is a status bar. This is a common GUI feature that can provide feedback to the user. Let me show you how to create one.
Status Bar
The status bar control is derived from a slightly different class than the other controls we have been working with. The class name that defines a status bar is System.Windows.Controls.Primitives.StatusBar
. As an alternative, you could use a TextBox
control and format it to look like a traditional status bar. It depends on what information you want to show and what you might need to update.
Here's simple code to create a status bar:
$statusBar = [System.Windows.Controls.Primitives.StatusBar]@{
Background = 'LightGray'
FontSize = 10
FontFamily = 'Tahoma' #<-- this will be inherited by status bar items
HorizontalAlignment = 'Stretch' #<-- this is the default
}
Next, I need to add items to it. THe control has an Items
property which is a collection. I can use the Add()
method to add items to the collection. The items can technically be any WPF control. Although you will most likely use labels or text blocks.
[void]$statusBar.Items.Add([System.Windows.Controls.Label]@{
Content = "v1.0"
VerticalAlignment = 'Center'
})
The method will write an index number to the pipeline, which is why I am setting the output to [void]
. You might need to experiment with aligning and positioning the control.
If you intend to add multiple items, you might want to include a separator.
[void]$statusBar.Items.Add([System.Windows.Controls.Separator]@{Height = 20; Width = 1 })
The separator will be the vertical pipe (|
). If you want it to stand out, you might want to set the background color property.
For my demonstration, I'm going to add a few more elements to the status bar.
[void]$statusBar.Items.Add([System.Windows.Controls.Label]@{
Content = $ENV:Computername
VerticalAlignment = 'Center'
})
[void]$statusBar.Items.Add([System.Windows.Controls.Separator]@{Height = 20; Width = 1 })
Progress Bar
Let's take a side quest and look at another control that might be useful in your scripts. The progress bar is a common control that can provide feedback to the user about the progress of a task. The progress bar is a System.Windows.Controls.ProgressBar
control. I'm going to add one to the status bar that will reflect how much free memory is available.
$progressBar = [System.Windows.Controls.ProgressBar]@{
Width = 100
Value = GetMemPercent
Height = 20
Tooltip = '{0}% free' -f (GetMemPercent)
VerticalAlignment = 'Center'
}
[void]$statusBar.Items.Add($progressBar)
The Value
property is a percentage. I'm using a private function to calculate the percentage of free memory.
function GetMemPercent {
$mem = Get-CimInstance -ClassName Win32_OperatingSystem
$v = ($mem.FreePhysicalMemory / $mem.TotalVisibleMemorySize) * 100
#return the percentage as a rounded number to 2 decimal places
[math]::Round($v, 2)
}
The Tooltip
property will display the percentage when you hover over the control. For the most part I am using the default properties for the control. You can use Get-Member
or Get-TypeMember
to discover what properties are available.
I'm going to add one more element to the status bar
[void]$statusBar.Items.Add([System.Windows.Controls.Separator]@{Height = 20; Width = 1 })
[void]$statusBar.Items.Add(
[System.Windows.Controls.Label]@{
Content = "Last Updated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
VerticalAlignment = 'Center'
}
)
Don't forget to add the status bar to the main window. I'm using a stack panel for this demo.
$stack.AddChild($statusBar)
Updating the Status bar
The StatusBar items are an array based on the order they were added. You can modify the item by referencing the index number.
$btnUpdate.Add_Click({
$mem = GetMemPercent
$statusBar.Items[5].Value = $mem
$statusBar.Items[5].Tooltip = '{0}% free' -f $mem
$statusBar.Items[-1].Content = "Last Updated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
})
Don't forget that separators count as status bar items.
Here's my sample script.
#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 GetMemPercent {
$mem = Get-CimInstance -ClassName Win32_OperatingSystem
$v = ($mem.FreePhysicalMemory / $mem.TotalVisibleMemorySize) * 100
#return the percentage as a rounded number to 2 decimal places
[math]::Round($v, 2)
}
#this will be displayed in the status bar
$ScriptVersion = '0.4.0'
$window = [System.Windows.Window]@{
Title = 'Status Bar Demo'
Height = 325
Width = 600
WindowStartupLocation = 'CenterScreen'
WindowStyle = 'ThreeDBorderWindow'
#prevent the user from resizing the window
ResizeMode = 'NoResize'
}
$stack = [System.Windows.Controls.StackPanel]@{
Orientation = 'Vertical'
}
$txtBox = [System.Windows.Controls.TextBox]@{
TextWrapping = 'Wrap'
AcceptsReturn = $true
FontFamily = 'Consolas'
VerticalScrollBarVisibility = 'Auto'
HorizontalScrollBarVisibility = 'Auto'
Height = 200
Width = 400
Margin = '5,5,5,5'
}
$txtBox.Text = $PSVersionTable | Out-String
$stack.AddChild($txtBox)
$btnUpdate = [System.Windows.Controls.Button]@{
Content = 'Update'
Margin = '5,5,5,5'
Width = 75
HorizontalAlignment = 'Center'
}
$btnUpdate.Add_Click({
$mem = GetMemPercent
$statusBar.Items[5].Value = $mem
$statusBar.Items[5].Tooltip = '{0}% free' -f $mem
$statusBar.Items[-1].Content = "Last Updated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
})
$stack.AddChild($btnUpdate)
$statusBar = [System.Windows.Controls.Primitives.StatusBar]@{
Background = 'LightGray'
FontSize = 10
FontFamily = 'Tahoma' #<-- this will be inherited by status bar items
HorizontalAlignment = 'Stretch' #<-- this is the default
}
[void]$statusBar.Items.Add([System.Windows.Controls.Label]@{
Content = "v$ScriptVersion"
VerticalAlignment = 'Center'
})
[void]$statusBar.Items.Add([System.Windows.Controls.Separator]@{Height = 20; Width = 1 })
[void]$statusBar.Items.Add([System.Windows.Controls.Label]@{
Content = $ENV:Computername
VerticalAlignment = 'Center'
})
[void]$statusBar.Items.Add([System.Windows.Controls.Separator]@{Height = 20; Width = 1 })
[void]$statusBar.Items.Add([System.Windows.Controls.Label]@{
Content = 'Free memory: '
VerticalAlignment = 'Center'
})
$progressBar = [System.Windows.Controls.ProgressBar]@{
Width = 100
Value = GetMemPercent
Height = 20
Tooltip = '{0}% free' -f (GetMemPercent)
VerticalAlignment = 'Center'
}
[void]$statusBar.Items.Add($progressBar)
[void]$statusBar.Items.Add([System.Windows.Controls.Separator]@{Height = 20; Width = 1 })
[void]$statusBar.Items.Add(
[System.Windows.Controls.Label]@{
Content = "Last Updated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
VerticalAlignment = 'Center'
}
)
$stack.AddChild($statusBar)
$window.AddChild($stack)
[void]$window.ShowDialog()

If I click the Update
button, the status bar will update the free memory percentage and the last updated time.
Blocking and Timers
Which brings me to another point. When you run a WPF script, you already know that it blocks the console. You don't get your prompt back until the script finishes. I have a solution for that in another future article. But the other challenge is when you want to update the form with new information in reaction to something like a button click. Due to the nature of the way WPF handles threads, which is way beyond the scope of this article, refreshing the form can be a bit tricky. Unless you have a developer background, researching this topic will make your head spin.
Let's keep it simple and use a thread timer.
$timer = [System.Windows.Threading.DispatcherTimer]@{
#create an interval of 1 second
Interval = New-TimeSpan -Seconds 1
IsEnabled = $True
}
The timer object will fire the Tick
event on every interval. In my case, that is every second. I'll add an event handler for the timer.
$timer.Add_Tick({
#update the time every second
$statusBar.Items[-1].Content = Get-Date -Format 'g'
#update memory usage every 30 seconds
#I could also have created a separate timer for this task
if ($script:c -ge 30) {
UpdateMemoryDisplay
$script:c = 0
}
else {
$script:c++
}
})
I'm adding this to a revised copy of my status bar demo. The last time in the status bar is a time stamp, which gets updated every second. I'm also using a secondary counter to invoke a private function to update the memory display every 30 seconds.
Function UpdateMemoryDisplay {
$script:MemPercent = GetMemPercent
#use for testing
#Write-Host $script:MemPercent
$progressBar.Value = $script:MemPercent
$progressBar.Tooltip = "{0}% free" -f $script:MemPercent
}
I need to start the timer, which I'll do by adding an event handler for the window.
$window.Add_Loaded({
#get the initial memory usage
UpdateMemoryDisplay
#Start the timer when the window is loaded
$timer.Start()
})
These commands will run when the windows is loaded. The timer will start and the memory display will be updated. The timer will continue to run until the window is closed. It is a good practice to stop the timer when the window is closed. I'll do that with another event handler.
$window.Add_Closing({
#clean up the timer
$timer.Stop()
})

Summary
The service info code is too long to display here. You can download all the demos from this article at https://www.dropbox.com/scl/fi/hsp77kdsjtja0c1kxacqo/wpf-apps.zip?rlkey=ozbl4jve99xfz6s510ykr7fm0&
I hope you'll try the code and attempt to build a simple WPF-based PowerShell script. I think I've overwhelmed you with enough WPF content for now. I'll take a little break from this topic. But we will return to it because there are a few more key concepts and techniques I want to share with you.
In the mean time, feel free to leave comments and questions. Enjoy your weekend.