So you’ve got your fancy new GUI created, functions and events all ready to go but seem to be having an issue with the GUI going non-responsive any time you try to do anything? If so you’ve come to the right place. Today we’re going to be integrating runspaces into our PS Event Viewer project to keep everything responsive and create a good user experience when using our new utility.
What are runspaces anyway? When we talk about multi-threading I think a simple way to understand it is that you have a single powershell.exe process with multiple independent threads. The overall concept is that you can do something in parallel with running your GUI while maintaining responsiveness. In our background runspace we do not need do even think about touching our GUI, and I think this is a big key because I have seen people try to keep their code and GUI all together and end up with issues. All we need to do is return an object that you then process as part of your event and update the GUI with outside of the background runspace. What is returned can be a simple success/fail value or it can be an entire collection of objects like we are using here.
Note: If you are a beginner my advice is not to try and create any sort of GUI that updates on it’s own via a timer or anything like that. It is much easier to grasp this concept when you have click events that you can control at will. So this means no continuously monitoring or ping GUIs just yet, however once you have your feet on the ground this will seem much simpler than it does today.
With that said, let’s start building our Start-BackgroundJob function:
Function Start-BackgroundJob { param( [ScriptBlock] $Job = {}, [HashTable] $JobVariables = @{} ) #Create our runspace & a powershell object to run in $Runspace = [runspacefactory]::CreateRunspace() $Runspace.Open() $Powershell = ::Create() $Powershell.Runspace = $Runspace
So as you can see we are going to be building a function that takes 2 parameters: a job and the variables for that job. Then to kick things off we just need to build a runspace and powershell object to have a place to execute our job. Next, lets add our code for our job and make the variables available as well:
#Add code for the function to be run $Powershell.AddScript($Job) | Out-Null #Send variables across pipeline 1 by 1 and make them available for our imported function foreach ($Variable in $JobVariables.GetEnumerator()) { $Powershell.AddParameter($Variable.Name, $Variable.Value) | Out-Null }
Finally, we are ready. Our newly created runspace has all the info it needs to be able to execute whatever code we pass it’s way. To kick off our code in the runspace we just need to run the BeginInvoke method:
#Start job $BackgroundJob = $Powershell.BeginInvoke() #Wait for code to complete and keep GUI responsive do { [System.Windows.Forms.Application]::DoEvents() Start-Sleep -Milliseconds 1 } while (!$BackgroundJob.IsCompleted)
That do/while loop is pretty much where the magic happens. As you can see we are checking every millisecond to see if our code is done running, and during that process we need to keep our GUI responsive. Once everything is done running all we need to do is clean up a few things and return our results:
$Result = $Powershell.EndInvoke($BackgroundJob) #Clean up $Powershell.Dispose() | Out-Null $Runspace.Close() | Out-Null #Return our results to the GUI $Result
We just finished creating the function that allows all of this to happen. Of course you can adapt things to return results immediately if you wanted to kick off multiple background jobs at once and then check the results later. That’s the beauty of Powershell, you really just have to think outside the box and there is most likely a way to do anything you can think of. Now the only thing left to do is slightly alter our events to leverage the new function. Here is what our submit click event looks like now:
Instead of:
$Script:Results = Get-LogResults @Params
Use this:
$Script:Results = Start-BackgroundJob -Job ${Function:Get-LogResults} -JobVariables $Params
What we’re actually doing here is passing in the code for the Get-LogResults function into our background job along with the variables it needs from our GUI, allowing the Start-BackgroundJob function to process all of it as a scriptblock and then waiting on the results to update our GUI with. It’s a pretty neat trick and seems to work well in a ton of different scenarios.
That’s it! We are officially done with our PS Event Viewer project. Download the source code below and see what you can do with this to take things to the next level. Add a custom theme, new functionality, or anything else that would be of value to you. I hope everyone enjoyed this series as much as I enjoyed writing it. If so, drop me a line and let me know what your experience was!