Posey's Tips & Tricks

Why PowerShell Timers Cause Problems in GUI Environments, Part 2

A practical workaround shows how PowerShell developers can keep Windows Forms GUIs responsive by moving timer-driven processing into background jobs and using a second timer to update the interface only after the work is complete.

In my first post in this series, I explained that timers really don’t work very well in PowerShell scripts that use a Windows Forms based GUI. Normal PowerShell timers are unable to update the GUI interface, while Windows Forms timers can potentially become overloaded, thereby causing the GUI to lock up. Fortunately, I figured out a creative solution to this seemingly impossible problem.

As you will recall from my previous post, the reason why the Windows Forms timer caused the GUI to lock up was because there is only a single thread for handling all of the GUI related tasks. Therefore, if you try to perform anything beyond the most basic tasks within the timer event handler, it overwhelms the thread, thereby rendering the GUI unresponsive.

The trick to running the required task without overwhelming the GUI thread is to move all of the heavy lifting to a different process. Thankfully, this is easier than it sounds.

So let's suppose that I have created a Windows Forms timer called Timer and that the tasks that I want to perform every few seconds exist in the $Timer.Add_Tick event handler. Let’s also assume that my code is performing some sort of computation. What I can do in a situation like that is to add a line like this one to the event handler:

$Script:Result = Start-Job -ScriptBlock{
#My computational code
return
}

This code snippet would run the computational code in a separate process and then write the results to a variable called $Result, Notice that I am using the script level scope for this variable to ensure that the variable is valid throughout the entire script.

OK, so we have moved the heavy lifting to a different process, but there are actually a couple of additional things that we need to do in order to make sure that the code will behave as intended.

The first thing that we need to do is to put a mechanism in place to keep jobs from piling up. If the job takes longer to complete than the timer allows, then that is a problem. The way that we can handle that is by using a variable to keep track of whether the job is running or not. As such, we might do something like this:

$Script:Running  =  $False
$Timer.Add_Tick({
If ($Script:Running -eq $True) {Return}
$Script:Running = $True
$Script:Result = Start-Job -ScriptBlock{
#My computational code
return
}

In this example, we are creating a variable called Running. This variable is used to keep track of whether or not our computational job is currently running or not. We initially set the value to $False. When the timer tick executes, the first thing that we do is check to see if $Running is true, which would mean that the last job is still running. If the job is running, then we use the Return command to exit the loop. Otherwise, we set $Running to True and then create the job that handles the processing.

As you look at the sample code above, you might notice that something is missing. We never set $Running back to False. The reason for this is that because the job is running in another thread, we can’t just set $Running to False at the end of the loop because the job may very well still be running.

The way that I handled this problem was to create a second Windows Forms timer. This one runs more frequently than the original timer (such as once per second). The tick action for this timer contains an If statement that checks to see if the job has completed. The if statement looks something like this:

If ($Script:Result – and $Script:Result.State -eq  ‘Completed){

In other words, we are checking to see if the job is in a completed state. If the job has completed, then we update the GUI and we set the $Running variable back to False. If the job has not completed, then we don’t take any action aside from checking the job status again the next time that the timer event runs.

While there are a lot of moving parts involved in this solution, it solved my problem beautifully. The GUI updates as intended and the system remains responsive.

About the Author

Brien Posey is a 22-time Microsoft MVP with decades of IT experience. As a freelance writer, Posey has written thousands of articles and contributed to several dozen books on a wide variety of IT topics. Prior to going freelance, Posey was a CIO for a national chain of hospitals and health care facilities. He has also served as a network administrator for some of the country's largest insurance companies and for the Department of Defense at Fort Knox. In addition to his continued work in IT, Posey has spent the last several years actively training as a commercial scientist-astronaut candidate in preparation to fly on a mission to study polar mesospheric clouds from space. You can follow his spaceflight training on his Web site.

Featured

comments powered by Disqus

Subscribe on YouTube