I am creating a powershell script with a GUI, that copies user profiles from a selected source disk to a destination disk. I've created the GUI in XAML, with VS Community 2019. The script works like this : you select the source disk, the destination disk, the user profile and the folders you want to copy. When you press the button "Start", it calls a function called Backup_data, where a runspace is created. In this runspace, there's just a litte Copy-Item, with as arguments what you've selected.
The script works fine, all the wanted items are correctly copied. The problem is that the GUI is freezing during the copy (no "not responding" message or whatever, it's just completly freezed ; can't click anywhere, can't move the window). I've seen that using runspaces would fix this problem, but it doesn't to me. Am I missing something ?
Here's the function Backup_Data:
Function BackupData {
##CREATE RUNSPACE
$PowerShell = [powershell]::Create()
[void]$PowerShell.AddScript( {
Param ($global:ReturnedDiskSource, $global:SelectedUser, $global:SelectedFolders, $global:ReturnedDiskDestination)
##SCRIPT BLOCK
foreach ($item in $global:SelectedFolders) {
Copy-Item -Path "$global:ReturnedDiskSource\Users\$global:SelectedUser\$item" -Destination "$global:ReturnedDiskDestination\Users\$global:SelectedUser\$item" -Force -Recurse
}
}).AddArgument($global:ReturnedDiskSource).AddArgument($global:SelectedUser).AddArgument($global:SelectedFolders).AddArgument($global:ReturnedDiskDestination)
#Invoke the command
$PowerShell.Invoke()
$PowerShell.Dispose()
}
The PowerShell SDK's
PowerShell.Invoke()method is synchronous and therefore by design blocks while the script in the other runspace (thread) runs.You must use the asynchronous
PowerShell.BeginInvoke()method instead.Simple example without WPF in the picture (see the bottom section for a WPF solution):
Note that there's a simpler alternative to using the PowerShell SDK: the
ThreadJobmodule'sStart-ThreadJobcmdlet, a thread-based alternative to the child-process-based regular background jobs started withStart-Job, that is compatible with all the other*-Jobcmdlets.Start-ThreadJobcomes with PowerShell [Core] 7+, and can be installed from the PowerShell Gallery in Windows PowerShell (Install-Module ThreadJob).Microsoft.PowerShell.ThreadJob. As of PowerShell 7.3.4, it is still the old module that is bundled; clarifying the relationship between these two modules and the best way forward is the subject of GitHub issue #19742.Complete example with WPF:
If, as in your case, the code needs to run from an event handler attached to a control in a WPF window, more work is needed, because
Start-Sleepcan not be used, since it blocks processing of GUI events and therefore freezes the window.Unlike WinForms, which has a built-in method for processing pending GUI events on demand (
[System.Windows.Forms.Application]::DoEvents(), WPF has no equivalent method, but it can be added manually, as shown in theDispatcherFramedocumentation.The following example:
Creates a window with two background-operation-launching buttons and corresponding status text boxes.
Uses the button-click event handlers to launch the background operations via
Start-ThreadJob:Note:
Start-Jobwould work too, but that would run the code in a child process rather than a thread, which is much slower and has other important ramifications.It also wouldn't be hard to adapt the example to use of the PowerShell SDK (
[powershell]), but thread jobs are more PowerShell-idiomatic and are easier to manage, via the regular*-Jobcmdlets.Displays the WPF window non-modally and enters a custom event loop:
A custom
DoEvents()-like function,DoWpfEvents, adapted from theDispatcherFramedocumentation is called in each loop operation for GUI event processing.[System.Windows.Forms.Application]::DoEvents()- and, in fact, you can use the latter directly, if you don't mind loading both the WPF and WinForms assemblies.Additionally, the progress of the background thread jobs is monitored and output received is appended to the job-specific status text box. Completed jobs are cleaned up.
Note: Just as it would if you invoked the window modally (with
.ShowModal()), the foreground thread and therefore the console session is blocked while the window is being displayed. The simplest way to avoid this is to run the code in a hidden child process instead; assuming that the code is in scriptwpfDemo.ps1:You could also do this via the SDK, which would be faster, but it's much more verbose and cumbersome:
$runspace = [runspacefactory]::CreateRunspace() $runspace.ApartmentState = 'STA'; $runspace.Open(); $ps = [powershell]::Create(); $ps.Runspace = $runspace; $null = $ps.AddScript((Get-Content -Raw wpfDemo.ps1)).BeginInvoke()Screenshot:
This sample screen shot shows one completed background operation, and one ongoing one (running them in parallel is supported); note how the button that launched the ongoing operation is disabled for the duration of the operation, to prevent re-entry:
Source code: