Part V - Building Responsive PowerShell Apps with Progress bars
This post is part of the Learning GUI Toolmaking Series, here on FoxDeploy. Click the banner to return to the series jump page!
Where we left off
If youâve followed this series, you should know how to make some really cool applications, using WPF for the front-end and PowerShell for the code-behind.
What will now probably happen is youâll make a cool app and go to show it off to someone with a three letter title and theyâll do something you never imagined, like drag a CSV file into the text boxâŚ.(true story).  Then this happens.
We donât want this to happen. Â We want our apps to stay responsive!
In this post weâll be covering how to implement progress bars in your own application, and how to multi-thread your PowerShell applications so they donât hang when background operations take place. The goal here is to ensure that our applications DONâT do this.
Â
Do you even thread, bro?
Hereâs why this is happening to usâŚ
if we are running all operations in the same thread, from rendering the UI to code behind tasks like waiting for something slow to finish, eventually our app will get stuck in the coding tasks, and the UI freezes while weâre waiting. Â This is bad.
Windows will notice that we are not responding to the userâs needs and that weâre staying late at the office too often and put a nasty âNot Respondingâ in the title. This is not to mention the passive-aggressive texts she will leave us!
If thingâs donât improve, Windows will then gray out our whole application window to show the world what a bad boyfriend we are.
Should we still blunder ahead, ignoring the end user, Windows will publicly dump us, by displaying a âkill processâ dialog to the user.  Uh, I may have been transferring my emotions there a bitâŚ
All of this makes our cool code look WAY less cool.
To keep this from happening and to make it easy, Iâve got a template available here which is pretty much plug-and-play for keeping your app responsive. And it has a progress bar too!
The full code is here PowerShell_GUI_template.ps1. Â If youâd like the Visual Studio Solution to merge into your own project, thatâs here. Â Letâs work through what had to happen to support this.
 A little different, a lot the same
Starting at the top of the code, youâll see something neat in these first few lines: weâre setting up a variable called $syncHash which allow us to interact with the separate threads of our app.
$Global:syncHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState ="STA"
$newRunspace.ThreadOptions ="ReuseThread"
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
After defining a synchronized variable, we then proceed to create a runspace for the first thread of our app.
 Whatâs a runspace?
This is a really good question. Â A runspace is a stripped down instance of the PowerShell environment. Â It basically tacks an additional thread onto your current PowerShell process, and away it goes.
Similar to a PowerShell Job, but theyâre much, much quicker to spawn and execute.
However, where PSJobs are built-in and have tools like get-job and the like, nothing like that exists for runspaces. We have to do a bit of work to manage and control Runspaces, as youâll see below.
Short version: a runspace is a super streamlined PowerShell tangent process with very quick spin up and spin down.  Great for scaling a wide task.
So, back to the code, we begin by defining a variable, $syncHash which will by synchonized from our local session to the runspace thread weâre about to make. Â We then describe $newRunSpace, which will compartmentalize and pop-out the code for our app, letting it run on itâs own away from our session. Â This will let us keep using the PowerShell or ISE window while our UI is running. Â This is a big change from the way we were doing things before, which would lockup the PowerShell window while a UI was being displayed.
If we collapse the rest of the code, weâll see this.
The entire remainder of our code is going into this variable called $pscmd.  This big boy holds the whole script, and is the first thread which gets âpopped outâ.
The code ends on line 171, triggering this runspace to launch off into its own world with beginInvoke(). Â This allows our PowerShell window to be reused for other things, and puts the App in its own memory land, more or less.
Within the Runspace
Letâs look inside $pscmd to see whatâs happening there.
Finally, something familiar! Â Within $pscmd on lines 10-47, we begin with our XAML, laying out the UI. Â Using this great tip from Boe, we have a new and nicer approach to scraping the XAML and search for everything with a name and mount it as a variable.
This time, instead of exposing the UI elements as $WPFControlName, we instead add them as members within $syncHash.  This means our Console can get to the values, and the UI can also reference them.  For example:
Even though the UI is running in itâs own thread, I can still interact with it using this $syncHash variable from the console
Thread Count: Two and climbing
Now weâve got the UI in itâs own memory land and threadâŚand weâre going to make another thread as well for our code to execute within.  In this next block of code, we use a coding structure Boe laid out to help us work across the many runspaces that can get created here.  Note that this time, our synchronized variable is called $jobs.
This code structure sets up an additional runspace to do memory management for us.
For the most part, we can leave this as a âblackboxâ. Â It is efficient and practical code which quietly runs for as long as our app is running. Â This coding structure becomes invoked and then watchs for new runspaces being created. Â When they are, it organizes them and tracks them to make sure that we are memory efficient and not sprawling threads all over the system. Â I did not create this logic, by the way. Â The heavy lifting has already been done for us, thanks to some excellent work by Joel Bennett and Boe Prox.
So weâre up to thread two. Â Thread 1 contains all of our code, Thread 2 is within that and manages the other runspaces and jobs weâll be doing.
Now, things should start to look a little more familiar as we finally see an event listener:
Â
Weâre finally interacting with the UI again. Â on line 85, we register an event handler using the Add_Click() method and embed a scriptblock. Â Within the button, weâve got another runspace!
This multi threading is key though to making our app stay responsive like a good boyfriend and keep the app from hanging.
Updating the Progress Bar
When the button is clicked, weâre going to run the code in its own thread. Â This is important, because the UI will still be rendered in its own thread, so if there is slowness off in âbuttonlandâ, we donât care, the UI will still stay fresh and responsive.
Now, this introduces a bit of a complication here. Â Since weâve got the UI components in their own thread, we canât just reach over to them like we did in the previous example. Â Imagine if we had a variable called $WPFTextBox. Â Previously, weâd change the $WPFTextBox.Text member to change the text of the box.
However, if we try that now, we can see that we get an error because of a different owner.
We actually created this problem for ourselves by pushing the UI into its own memory space. Have no fear, Boe is once again to the rescue here. Â He created a function Called-Update window, which makes it easy to reach across threads. Â (link)
The key to this structure is its usage of the Systems.Windows.Threading.Dispatcher class.  This nifty little guy appears when a threaded UI is created, and then sits, waiting  for update requests via its Invoke() method.  Simply provide the name of a control youâd like to change, and the updated value.
Function Update-Window {
Param (
$Control,
$Property,
$Value,
[switch]$AppendContent
)
# This is kind of a hack, there may be a better way to do this
If ($Property -eq"Close") {
$syncHash.Window.Dispatcher.invoke([action]{$syncHash.Window.Close()},"Normal")
Return
}
# This updates the control based on the parameters passed to the function
$syncHash.$Control.Dispatcher.Invoke([action]{
# This bit is only really meaningful for the TextBox control, which might be useful for logging progress steps
If ($PSBoundParameters['AppendContent']) {
$syncHash.$Control.AppendText($Value)
} Else {
$syncHash.$Control.$Property = $Value
}
},"Normal")
}
Weâre defining this function within the button clickâs runspace, since that is where weâll be reaching back to the form to update values. When I load this function from within the console, look what I can do!
With all of these tools in place, it is now very easy to update the progress bar as we progress through our logic. Â In my case, I read a big file, sleep for a bit to indicate a slow operation, then update a text box, and away it goes.
If youâre looking to drag and drop some logic into your code, this is where you should put all of your slow operations.
Update-Window -Control StarttextBlock -Property ForeGround -Value White
start-sleep -Milliseconds 850
$x += 1..15000000 #intentionally slow operation
update-window -Control ProgressBar -Property Value -Value 25
update-window -Control TextBox -property text -value "Loaded File..." -AppendContent
Update-Window -Control ProcesstextBlock -Property ForeGround -Value White
start-sleep -Milliseconds 850
update-window -Control ProgressBar -Property Value -Value 50
Update-Window -Control FiltertextBlock -Property ForeGround -Value White
start-sleep -Milliseconds 500
update-window -Control ProgressBar -Property Value -Value 75
Update-Window -Control DonetextBlock -Property ForeGround -Value White
start-sleep -Milliseconds 200
update-window -Control ProgressBar -Property Value -Value 100
Sources
Thatâs all there is to it! The hard part here was containing our app into separate threads, but hopefully with the template involved you can easily see where to drop you XAML and how to make your application hum along swimmingly!
I could not have done this post without the many examples provided by Boe Prox on his blog:
Writing WPF Across Runspaces PowerShell WPF Radio Buttons Multi-runspace Event Handling Asynchronous event handling in PowerShell
Additionally, I had help from Joel Bennett (JayKul) of HuddledMasses.org.
I learned a lot from reading over Micah Rairdonâs New-ProgressBar cmdlet from his blog, so check that out too.  Finally, Rhys W Edwards has a great cmdlet also on TechNet, with some more good demos if youâre looking for help or inspiration.