简体   繁体   English

Powershell GUI 冻结,即使有运行空间

[英]Powershell GUI Freezing, even with runspace

I am creating a powershell script with a GUI, that copies user profiles from a selected source disk to a destination disk.我正在创建一个带有 GUI 的 powershell 脚本,它将用户配置文件从选定的源磁盘复制到目标磁盘。 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.我已经在 XAML 和 VS Community 2019 中创建了 GUI。脚本的工作方式如下:select 源磁盘、目标磁盘、用户配置文件和要复制的文件夹。 When you press the button "Start", it calls a function called Backup_data, where a runspace is created.当您按下“开始”按钮时,它会调用一个名为 Backup_data 的 function,其中会创建一个运行空间。 In this runspace, there's just a litte Copy-Item, with as arguments what you've selected.在这个运行空间中,只有一个很小的Copy-Item,其中包含您选择的 arguments。

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).问题是 GUI 在复制期间冻结(没有“不响应”消息或其他任何信息,它只是完全冻结;无法单击任何位置,无法移动窗口)。 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 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. PowerShell SDK 的PowerShell.Invoke()方法是同步的,因此在其他运行空间(线程)中的脚本运行时设计

You must use the asynchronous PowerShell.BeginInvoke() method instead.您必须改用异步PowerShell.BeginInvoke()方法

Simple example without WPF in the picture (see the bottom section for a WPF solution):图片中没有 WPF 的简单示例(请参阅 WPF 解决方案的底部部分):

$ps = [powershell]::Create()

# Add the script and invoke it *asynchronously*
$asyncResult = $ps.AddScript({ Start-Sleep 3; 'done' }).BeginInvoke()

# Wait in a loop and check periodically if the script has completed.
Write-Host -NoNewline 'Doing other things..'
while (-not $asyncResult.IsCompleted) {
  Write-Host -NoNewline .
  Start-Sleep 1
}
Write-Host

# Get the script's success output.
"result: " + $ps.EndInvoke($asyncResult)

$ps.Dispose()

Note that there's a simpler alternative to using the PowerShell SDK : the ThreadJob module's Start-ThreadJob cmdlet, a thread -based alternative to the child-process -based regular background jobs started with Start-Job , that is compatible with all the other *-Job cmdlets.请注意,使用 PowerShell SDK 有一个更简单的替代方法ThreadJob模块的Start-ThreadJob Start-Job ,这是一个基于线程的替代基于子进程的常规后台作业,它与所有其他*-Job兼容*-Job cmdlet。

Start-ThreadJob comes with PowerShell [Core] 7+ , and can be installed from the PowerShell Gallery in Windows PowerShell ( Install-Module ThreadJob ). Start-ThreadJob comes with PowerShell [Core] 7+ , and can be installed from the PowerShell Gallery in Windows PowerShell ( Install-Module ThreadJob ).

# Requires module ThreadJob (preinstalled in v6+)

# Start the thread job, always asynchronously.
$threadJob = Start-ThreadJob { Start-Sleep 3; 'done' }

# Wait in a loop and check periodically if the job has terminated.
Write-Host -NoNewline 'Doing other things..'
while ($threadJob.State -notin 'Completed', 'Failed') {
  Write-Host -NoNewline .
  Start-Sleep 1
}
Write-Host

# Get the job's success output.
"result: " + ($threadJob | Receive-Job -Wait -AutoRemoveJob)

Complete example with WPF: 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-Sleep can not be used, since it blocks processing of GUI events and therefore freezes the window.如果在您的情况下,代码需要从附加到 WPF window 中的控件的事件处理程序运行,则需要做更多的工作,因为不能使用Start-Sleep ,因为它会阻止 GUI 事件的处理并因此冻结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 the DispatcherFrame documentation .WinForms不同,WinForms 具有用于按需处理待处理 GUI 事件的内置方法( [System.Windows.Forms.Application]::DoEvents() , Z3055DD731D79EAA7189A62F36720 手动添加,但它可以手动添加 9F 方法中显示的等效项,但它不能像所示的方法一样添加DispatcherFrame文档

The following example:下面的例子:

  • Creates a window with two background-operation-launching buttons and corresponding status text boxes.创建一个带有两个后台操作启动按钮和相应状态文本框的 window。

  • Uses the button-click event handlers to launch the background operations via Start-ThreadJob :使用按钮单击事件处理程序通过Start-ThreadJob启动后台操作:

    • Note: Start-Job would 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.注意: Start-Job也可以工作,但它会在子进程而不是线程中运行代码,这会慢得多并且有其他重要的后果。

    • 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 *-Job cmdlets.调整示例以使用 PowerShell SDK ( [powershell] ) 也不难,但线程作业更符合 PowerShell 习惯,并且更易于管理,通过常规*-Job cmdlet。

  • Displays the WPF window non -modally and enters a custom event loop:模态显示 WPF window 并进入自定义事件循环:

    • A custom DoEvents() -like function, DoWpfEvents , adapted from the DispatcherFrame documentation is called in each loop operation for GUI event processing.在每个循环操作中调用自定义DoEvents() -like function, DoWpfEvents ,改编自DispatcherFrame文档以进行 GUI 事件处理。

      • Note: For WinForms code, you could simply call [System.Windows.Forms.Application]::DoEvents() .注意:对于WinForms代码,您可以简单地调用[System.Windows.Forms.Application]::DoEvents()
    • Additionally, the progress of the background thread jobs is monitored and output received is appended to the job-specific status text box.此外,后台线程作业的进度受到监控,收到的 output 将附加到作业特定的状态文本框中。 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.注意:就像您以模态方式调用 window (使用.ShowModal() )一样,前台线程和控制台 session 在显示 Z05B8C74CBD96FBF2DE4C1A352ZFFB 时被阻止 The simplest way to avoid this is to run the code in a hidden child process instead;避免这种情况的最简单方法是在隐藏的子进程中运行代码; assuming that the code is in script wpfDemo.ps1 :假设代码在脚本wpfDemo.ps1中:

# In PowerShell [Core] 7+, use `pwsh` instead of `powershell`
Start-Process -WindowStyle Hidden powershell '-noprofile -file wpfDemo.ps1'

You could also do this via the SDK, which would be faster, but it's much more verbose and cumbersome:您也可以通过 SDK 执行此操作,这会更快,但更冗长和麻烦:
$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:注意启动正在进行的操作的按钮在操作期间是如何被禁用的,以防止重新进入:

WPF后台操作截图

Source code:源代码:

using namespace System.Windows
using namespace System.Windows.Threading

# Load WPF assemblies.
Add-Type -AssemblyName PresentationCore, PresentationFramework

# Define the XAML document, containing a pair of background-operation-launching
# buttons plus associated status text boxes.
[xml] $xaml = @"
<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Test"
        Title="MainWindow" Height="220" Width="600">
    <Grid>
        <TextBox x:Name="Status1" Height="140" Width="280" Margin="10,10" TextWrapping="Wrap" VerticalAlignment="Top" HorizontalAlignment="Left" AcceptsReturn="True" AcceptsTab="True" Padding="4" VerticalScrollBarVisibility="Auto" />
        <TextBox x:Name="Status2" Height="140" Width="280" Margin="10,10" TextWrapping="Wrap" VerticalAlignment="Top" HorizontalAlignment="Right" AcceptsReturn="True" AcceptsTab="True" Padding="4" VerticalScrollBarVisibility="Auto" />
        <Button x:Name="DoThing1" Content="Do Thing 1" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="100" Height="22" Margin="10,5" IsDefault="True" />
        <Button x:Name="DoThing2" Content="Do Thing 2" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="100" Height="22" Margin="10,5" />
    </Grid>
</Window>
"@

# Parse the XAML, which returns a [System.Windows.Window] instance.
$Window = [Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $xaml))

# Save the window's relevant controls in PowerShell variables.
# Background-operation-launching buttons.
$btns = $Window.FindName('DoThing1'), $Window.FindName('DoThing2')

# Use a [hashtable] to map the buttons to the associated status text boxes.
$txtBoxes = @{
  $btns[0] = $Window.FindName('Status1')
  $btns[1] = $Window.FindName('Status2')
}
# Use a [hashtable] to map the buttons to the associated background
# operations, defined as script blocks to be passed to Start-ThreadJob later.
# The sample operations here run for a few seconds, 
# emitting '.' every second and a message on completion.
$scriptBlocks = @{
  $btns[0] = 
    {
      1..3 | ForEach-Object { '.'; Start-Sleep 1 }
      'Thing 1 is done.'
    }
  $btns[1] = 
    {
      1..2 | ForEach-Object { '.'; Start-Sleep 1 }
      'Thing 2 is done.'
    }
}

# Attach the button-click event handlers that
# launch the background operations (thread jobs).
foreach ($btn in $btns) {

  $btn.Add_Click({

    # Temporarily disable this button to prevent re-entry.
    $this.IsEnabled = $false

    # Show a status message in the associated text box.
    $txtBoxes[$this].Text = "Started thing $($this.Name -replace '\D') at $(Get-Date -Format T)."

    # Asynchronously start a background thread job named for this button.
    # Note: Would work with Start-Job too, but that runs the code in *child process*, 
    #       which is much slower and has other implications.
    $null = Start-ThreadJob -Name $this.Name $scriptBlocks[$this]

  })

}

# Define a custom DoEvents()-like function that processes GUI WPF events and can be 
# called in a custom event loop in the foreground thread.
# Adapted from: https://docs.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcherframe
function DoWpfEvents {
  [DispatcherFrame] $frame = [DispatcherFrame]::new($True)
  $null = [Dispatcher]::CurrentDispatcher.BeginInvoke(
    'Background', 
    [DispatcherOperationCallback] {
      param([object] $f)
      ($f -as [DispatcherFrame]).Continue = $false
      return $null
    }, 
    $frame)
  [Dispatcher]::PushFrame($frame)
}


# Finally, display the window NON-modally...
$Window.Show() 
$null = $Windows.Activate() # Ensures that the window gets the focus.
# ... and enter a custom event loop based on calling the custom .DoEvents() method
while ($Window.IsVisible) {

  # Process GUI events.
  DoWpfEvents

  # Process pending background (thread) jobs, if any.
  Get-Job | ForEach-Object {
    
    # Get the originating button via the job name.
    $btn = $Window.FindName($_.Name)
    # Get the corresponding status text box.
    $txtBox = $txtBoxes[$btn]

    # Test if the job has terminated.
    $completed = $_.State -in 'Completed', 'Failed', 'Stopped'

    # Append any new results to the respective status text boxes.
    # Note the use of redirection *>&1 to capture ALL streams, notably including the error stream.
    if ($data = Receive-Job $_ *>&1) {
      $txtBox.Text += "`n" + ($data -join "`n")
    }

    # Clean up, if the job is completed.
    if ($completed) {
      Remove-Job $_
      $btn.IsEnabled = $true # re-enable the button.
      $txtBox.Text += "`nJob terminated on: $(Get-Date -Format T); status: $($_.State)."
    }

  }

  # Note: If there are no GUI events pending, this loop will cycle very rapidly.
  #       To mitigate this, we *also* sleep a little, but short enough to still keep
  #       the GUI responsive.
  Start-Sleep -Milliseconds 50

}

# Window was closed; clean up:
# If the window was closed before all jobs completed, 
# get the incomplete jobs' remaining output, wait for them to finish, and delete them.
Get-Job | Receive-Job -Wait -AutoRemoveJob


I've been searching for a solution all day and I've finally found one, so I'm gonna post it there for those who have the same problem.我整天都在寻找解决方案,我终于找到了一个,所以我会把它贴在那里给那些有同样问题的人。

First, check this article: https://smsagent.blog/2015/09/07/powershell-tip-utilizing-runspaces-for-responsive-wpf-gui-applications/ It's well explained and shows you how to correctly use runspaces with a WPF GUI.首先,查看这篇文章: https://smsagent.blog/2015/09/07/powershell-tip-utilizing-runspaces-for-responsive-wpf-gui-applications/很好地解释并展示了如何正确使用运行空间WPF GUI。 You just have to replace your $Window variable by $Synchhash.Window:您只需将 $Window 变量替换为 $Synchhash.Window:

$syncHash = [hashtable]::Synchronized(@{})
$reader = (New-Object System.Xml.XmlNodeReader $xaml)
$syncHash.window = [Windows.Markup.XamlReader]::Load( $reader )

Insert a runspace function with your code:使用您的代码插入运行空间 function:

function RunspaceBackupData {
$Runspace = [runspacefactory]::CreateRunspace()
$Runspace.ApartmentState = "STA"
$Runspace.ThreadOptions = "ReuseThread"
$Runspace.Open()
$Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash)
$Runspace.SessionStateProxy.SetVariable("SelectedFolders",$global:SelectedFolders)
$Runspace.SessionStateProxy.SetVariable("SelectedUser",$global:SelectedUser)
$Runspace.SessionStateProxy.SetVariable("ReturnedDiskSource",$global:ReturnedDiskSource)
$Runspace.SessionStateProxy.SetVariable("ReturnedDiskDestination",$global:ReturnedDiskDestination)
$code = {
    foreach ($item in $global:SelectedFolders) {
        copy-item -Path "$global:ReturnedDiskSource\Users\$global:SelectedUser\$item" -Destination "$global:ReturnedDiskDestination\Users\$global:SelectedUser\$item" -Force -Recurse
        }
}
$PSinstance = [powershell]::Create().AddScript($Code)
$PSinstance.Runspace = $Runspace
$job = $PSinstance.BeginInvoke()
}

And call it in the event-handler you want with the parameters you've indicated:并使用您指定的参数在所需的事件处理程序中调用它:

$var_btnStart.Add_Click( {
    RunspaceBackupData -syncHash $syncHash -SelectedFolders $global:SelectedFolders -SelectedUser $global:SelectedUser -ReturnedDiskSource $global:ReturnedDiskSource -ReturnedDiskDestination $global:ReturnedDiskDestination 
})

Don't forget to end your runspace:不要忘记结束你的运行空间:

$syncHash.window.ShowDialog()
$Runspace.Close()
$Runspace.Dispose()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM