Part V – Building Responsive PowerShell Apps with Progress bars

series_PowerShellGUI

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.

ISE_poop_the_bed

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.

SayNo

 

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.
 
unnamed

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.

unnamed (1)

 

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:

Synchash
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.
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:

unnamed (2)

 

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.

differentowner
Exception setting Text The calling thread cannot access this object because a different thread owns it.

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!

GIF_better
Enter a caption

 

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.

 

Advertisements

37 thoughts on “Part V – Building Responsive PowerShell Apps with Progress bars

  1. mattsky May 17, 2016 / 6:01 pm

    Very nice Stephen! if you have to do this in C# with VS2015 is it simpler?

    • FoxDeploy May 17, 2016 / 8:57 pm

      That’s a great question… BRB studying c#

  2. anmagnussen May 18, 2016 / 1:25 am

    Would be really cool to see you implement C# and PowerShell together, keep up with the great articles.

  3. EM May 18, 2016 / 7:54 pm

    Absolutely awesome Stephen. I’ve read countless articles of Boe, and his knowledge on the topic is great.
    I’ll be using these techniques soon for one of my long running scripts. However, it’s going to be runspace inception.

    • FoxDeploy May 18, 2016 / 8:23 pm

      Aw, thanks for the kind words! I used Boe’s writings to help me understand this and then tried to share what I learned from him.

  4. Andrew Z June 16, 2016 / 10:21 am

    This is a great post (and series)! This has been a huge help on a project I have been working on. I do have a quick question which is how to access input from a user when using this multi-thread setup. I have removed a lot of the code to make it shorter but here is what I’m trying but it’s not working. https://codeshare.io/q0u18
    Thank you in advance!

  5. sodawillow June 26, 2016 / 2:10 am

    Hi, the article is great but I noticed the quotes in the code examples get replaced with HTML entities, which makes the code a bit less readable. Don’t know if you can do much about it though?

    • FoxDeploy June 26, 2016 / 8:19 am

      Sometimes WordPress messes with my code because of its syntax highlighting. There’s a link to a github post in this post for just that reason! Thanks for the comment, I’ll fix the code when I can get on a pc !

  6. Jarko June 30, 2016 / 8:52 am

    Great info Fox!

    Am I the only one getting “Main Window(Not Responding)” when clicking the “Start” button from the ISE Snippet exampelcode on github?

    I thought this was what we should get rid of with runspaces?

    • sodawillow July 1, 2016 / 12:05 pm

      I get the same behavior. You can reduce the value for the “intentionally slow operation” to avoid this, or make it quicker.

  7. FoxDeploy July 1, 2016 / 5:02 pm

    Hey guys I’ll check this tonight. I don’t see any delay but sometimes I might be using a slightly different version

    • Jesse Paxson December 16, 2016 / 3:20 pm

      The line where you are updating the textblock with the values of $x is what is causing the hang. It’s listing 1-15,000,000, so it causes update-window to hang until $x can be completely parsed. I tried a foreach loop on that line and it eliminated the “Not responding…” message, but the window is still somewhat unresponsive.

  8. Eirikr July 30, 2016 / 10:57 pm

    This is really great.

    If you have multiple buttons, would you need to include the “function Update-Window { .. }” in each of them to be able to call the function?

    • FoxDeploy July 31, 2016 / 8:29 am

      You only need to include the function in a button or script block if you plan on running the script block within its own runspace. If not (like if the operation is really fast) then feel free to just call the function instead.

  9. Mahendran August 31, 2016 / 2:58 pm

    Hi Stephen,

    want to get current value of the gui text box from my second runscape (the one that is in the $synchash.button.add_click)is there any possibility.

  10. Mahendran September 1, 2016 / 4:52 am

    Hi Stephen,

    Great share, i have tried this in my tool but am not able to get the value of text box in another thread its empty.

    is there any possibility pass the value of the text box to other threads, pasted my script in pastebin please take a look

    http://pastebin.com/JXmUvBQU

    • FoxDeploy September 1, 2016 / 1:22 pm

      For one, this tool looks VERY good. Quite sophisticated, well done!

      Give me some help, what text box are you trying to access, and from which part of the UI?

      • Mahendran September 1, 2016 / 1:39 pm

        i am trying get the value of text box filename_txtbox inside the Function Get-UPTime

        • FoxDeploy September 1, 2016 / 2:08 pm

          Store the value that you want to get inside of the sinkhash as a property.

        • Mahendran September 8, 2016 / 1:37 pm

          Thanks Stephen.

        • Jordan September 9, 2016 / 9:28 am

          Found something cool in one of the comments from Scriptabit here: https://learn-powershell.net/2012/10/14/powershell-and-wpf-writing-data-to-a-ui-from-a-different-runspace/

          Function Read-Textbox(){
          write-host “`nBegin Read-Textbox [$(get-date)]”
          $SyncHash.TXT_Output.Dispatcher.Invoke($Normal,[action]{$Global:Return = $SyncHash.TXT_Output.Text})
          $Global:Return
          remove-variable -Name Return -scope Global
          write-host “End Read-Textbox [$(get-date)]”
          }

          I tweaked it slightly:

          function Read-Textbox {
          param($arg)
          $Normal = [System.Windows.Threading.DispatcherPriority]::Normal
          $SyncHash.txtVMName.Dispatcher.Invoke($Normal,[action]{$Global:Return = $SyncHash.$arg.Text})
          $Global:Return
          remove-variable -Name Return -scope Global
          }

          $value = Read-Textbox -Arg txtBoxName

        • Jordan September 9, 2016 / 1:04 pm

          I responded a little bit earlier with a Read-Textbox function I found, not sure if my comment went through, Stephen might have to approve it for it to show up. However, in the meantime, I have discovered that adding the value to $synchash is definitely way easier. I’ve had luck so far just doing it like this:

          $syncHash.Add(“valueName”,$value)

        • FoxDeploy September 9, 2016 / 1:12 pm

          Approved! And yes, the syncHash is the way to go to communicate with the threads of your app

  11. Daniel Petcher October 21, 2016 / 5:45 pm

    I’m just starting to study Runspaces. I’ve read the stuff you cited from Boe Prox that compares them so favorably to Start-Job. Boe’s article doesn’t mention Windows Workflow, though. (I think it was not yet available when he wrote his article.)

    How would using a Workflow compare to using a Runspace for handling a bunch of parallel tasks?

    • FoxDeploy October 21, 2016 / 9:14 pm

      I like where you’re going with this! Workflows are great when you want to code a structure to run forever, or to be able to survive through a reboot.

      If you were building a serious project, you might break it into a few workflows.

      But they are slow and hard to write on top of it.

      You definitely could break your code into multiple threaded workflows but to do so, you will end up working on a proverbial island with little assistance,as few people have great love for workflows.

  12. Samuel Owens January 11, 2017 / 9:37 am

    Hi Stephen,

    This blog series has been really useful. I have used it to build a batch printing tool . At the moment, the tool looks great and technically does what it needs to. The area I am trying to improve is around threads.

    At the moment, the script will use a third party product to render the PDF documents and print them, but this process can take some time. Currently I have popped up a “please wait” box to the user, which, as you say will become (not responding) if the user tries to interact with it. Obviously this is not ideal. Also, I would like to provide the stdout information into the GUI as the print job is taking place, but at the moment it seems like an all or nothing approach.

    This blogpost looked like it could answer my prayers, but I have hit a snag with trying to copy my code into XAML portion. When I try to hit run, and then return $synchhash I get a “cannot call a method on a null valued expression”.

    Name : Error
    Value : {You cannot call a method on a null-valued expression., You cannot call a method on a null-valued expression., You cannot call a method on a null-valued expression., Multiple ambiguous overloads found for
    “Load” and the argument count: “1”….}

    Am I doing something stupid here?

    Cheers
    Sam

    • Samuel Owens January 11, 2017 / 9:49 am

      Scratch this, I seem to have got over that hurdle. I will keep working through this.

      Sam

  13. TheMadTechnician January 19, 2017 / 12:30 pm

    Since you are talking about breaking out runspaces into their own threads, and you reference the ISE, you may want to make mention that simply launching the ISE via double click runs it in STA (Single Thread Apartment) mode.

    To avoid potential issues you might want to make a shortcut that launches the ISE with the -mta switch, which makes it run in Multi Thread Apartment mode. As a bonus, this also makes the ISE run in the same apartment model as the console, so you won’t see any different behavior if you, for example, run a script as a scheduled task. Not that it causes issues very often, but it can happen.

    • FoxDeploy January 25, 2017 / 10:29 am

      Thanks for taking the time to leave this comment, that actually resolved a big question I’ve had about difficulties getting multi-threading to work within the ISE.

  14. rnhamre March 6, 2017 / 8:22 am

    I am adding an output textbox to my GUI, I am able to get everything in to the textbox, but I would want the textbox to autoscroll down to the last line each time I add something to the textbox. Are you able to see what I am doing wrong with the following example code? It is in the second line I am trying to force it to scroll down after each update to the window:

    Update-Window -Control out_textBox -Property text -AppendContent (Get-Service | Sort Status -Desc| out-string)
    $syncHash.out_textBox.Dispatcher.Invoke([action]{$syncHash.out_textBox.ScrollToEnd()},”Normanl”)

    • rnhamre March 6, 2017 / 8:31 am

      I can’t edit my post, but shortly after posting I saw my typo: Normanl instead of Normal
      Fun times 🙂

  15. Allen March 10, 2017 / 2:54 pm

    Great series. Many thanks. Adapting now to meet my needs.

    In this lesson, you removed the try/catch when loading the WPF XML because everything is loaded in $pscmd and run in the default runspace. I would like to add the try/catch, but post the error messages back to the command-prompt if it could not load the WPF. Any idea how the initial runspace can talk back to the main process, i.e., command-line ?

    $Reader = (New-Object System.Xml.XmlNodeReader $XAML)
    try{ $SyncHash.Window = [Windows.Markup.XamlReader]::Load( $Reader ) }
    catch [System.Management.Automation.MethodInvocationException] {
    Write-Host ‘We ran into a problem with the XAML code. Check the syntax for this control…’ -ForegroundColor Red
    Write-Host $error[0].Exception.Message -ForegroundColor Red
    if( $error[0].Exception.Message -like ‘*Button*’ ) { Write-Warning ‘Ensure your Button does NOT have a Click=ButtonClick property. PS cannot handle this.’ }
    if( $error[0].Exception.Message -like ‘*TextBox*’ ) { Write-Warning ‘Ensure your TextBox does NOT have a TextChanged=”…_TextChanged” property.’ }
    if( $error[0].Exception.Message -like ‘*MenuItem*’ ) { Write-Warning ‘Ensure your MenuItem does NOT have a Click=”MenuItem_Help_About_Click” property.’ }
    Exit 1
    }
    catch{
    Write-Host ‘Unable to load Windows.Markup.XamlReader. Double-check syntax and ensure .NET is installed.’ -ForegroundColor Red
    Exit 1
    }

    Unfortunately, the try/catch code exists in the $pscmd within the initial runspace, so any errors are displayed out of sight within the runspace (I assume). I know there are errors because my WPF never displayed.

    Were you ever able to figure out how to provide feedback back to the parent process that spawned the runspace in the first place.

    Thank you.

    P.S.

    Also noticed that you edited the default WPF XML to get it to load in this lesson… specifically…

    <Window x:Name="Window_Main" x:Class="WpfApplication1.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008&quot;
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
    xmlns:local="clr-namespace:WpfApplication1"
    mc:Ignorable="d"

    becomes… in order to load application…

    <Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008&quot;
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006&quot;
    xmlns:local="clr-namespace:WpfApplication1"

    … took me an hour to figure this out since I was used to your other lessons that extracted this for me.

    Allen

    • FoxDeploy March 12, 2017 / 12:36 pm

      Good points man, and this highlights that I need to revisit this post and revise it for clarity. I agree, me switching the xaml header makes it incompatible with my previous walk through.

      I’ll add that to the to do list. Also, good point about nesting try catch in with the runspaces. I am still trying to work through a good approach for threading and error handling. I plan to look into how this is handled in traditional app development, because I think there will be some good lessons I can learn and apply to PowerShell from there.

  16. Philip P. June 8, 2017 / 3:54 pm

    Best thing ever, just saved me a ton of coding by giving me the ability to not have to create some weird workaround to get my GUI forms to update.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s