Я создаю сценарий powershell с графическим интерфейсом, который копирует профили пользователей с выбранного исходного диска на целевой диск. Я создал графический интерфейс в XAML с помощью VS Community 2019. Сценарий работает следующим образом: вы выбираете исходный диск, диск назначения, профиль пользователя и папки, которые вы хотите скопировать. Когда вы нажимаете кнопку "Start", вызывается функция Backup_data, где создается пространство выполнения. В этом пространстве есть только маленький Copy-Item, с аргументами, которые вы выбрали.

Сценарий работает нормально, все нужные элементы корректно копируются. Проблема в том, что GUI замирает во время копирования (нет сообщения "не отвечает" или чего-то подобного, он просто полностью замирает; нельзя нигде щелкнуть, нельзя переместить окно). Я видел, что использование runspaces может решить эту проблему, но мне это не помогает. Может, я что-то упускаю?

Здесь функция Backup_Data:

Функция BackupData {
  ##СОЗДАТЬ ПРОСТРАНСТВО ВЫПОЛНЕНИЯ
  $PowerShell = [powershell]::Create()
  [void]$PowerShell.AddScript( {
      Param ($global:ReturnedDiskSource, $global:SelectedUser, $global:SelectedFolders, $global:ReturnedDiskDestination)
      ##SCRIPT BLOCK
      foreach ($item в $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)
  #Вызов команды
  $PowerShell.Invoke()
  $PowerShell.Dispose()
}

Ответы (2)

Метод PowerShell SDK PowerShell.Invoke() является синхронным и поэтому по своей конструкции блокирует выполнение сценария в другом пространстве выполнения (потоке).

Вы должны использовать асинхронный PowerShell.BeginInvoke() метод вместо этого.

Простой пример без WPF на картинке (решение на WPF см. в нижнем разделе):

.
$ps = [powershell]::Create()

# Добавьте скрипт и вызовите его *асинхронно*.
$asyncResult = $ps.AddScript({ Start-Sleep 3; 'done' }).BeginInvoke()

# Ждите в цикле и периодически проверяйте, завершился ли скрипт.
Write-Host -NoNewline 'Занимаюсь другими делами...'.
while (-not $asyncResult.IsCompleted) {
  Write-Host -NoNewline .
  Start-Sleep 1
}
Write-Host

# Получите результат успешной работы скрипта.
"result: " + $ps.EndInvoke($asyncResult)

$ps.Dispose()

Отметим, что существует более простая альтернатива использованию PowerShell SDK: команда Start-ThreadJob модуля ThreadJob, альтернатива потоку на основе детского процесса обычным фоновым заданиям, запускаемым с помощью Start-Job, которая совместима со всеми другими командами *-Job.

Start-ThreadJob поставляется с PowerShell [Core] 7+, и может быть установлен из галереи PowerShell в Windows PowerShell (Install-Module ThreadJob).

# Требуется модуль ThreadJob (предустановлен в v6+)

# Запуск потокового задания, всегда асинхронно.
$threadJob = Start-ThreadJob { Start-Sleep 3; 'done' }

# Ждать в цикле и периодически проверять, завершилось ли задание.
Write-Host -NoNewline 'Занимаюсь другими делами...'.
while ($threadJob.State -notin 'Completed', 'Failed') {
  Write-Host -NoNewline .
  Start-Sleep 1
}
Write-Host

# Получите результат успешного выполнения задания.
"result: " + ($threadJob | Receive-Job -Wait -AutoRemoveJob)

Полный пример с WPF:

Если, как в вашем случае, код должен запускаться из обработчика событий, прикрепленного к элементу управления в окне WPF, потребуется дополнительная работа, поскольку Start-Sleep может не использоваться, поскольку он блокирует обработку событий GUI и поэтому замораживает окно.

.

В отличие от WinForms, который имеет встроенный метод для обработки ожидающих событий GUI по требованию ([System.Windows.Forms.Application]::DoEvents(), в WPF нет эквивалентного метода, но его можно добавить ручно, как показано в DispatcherFrame документации.

.

Следующий пример:

  • Создает окно с двумя кнопками запуска фоновой операции и соответствующими текстовыми полями состояния.

  • Использует обработчики событий нажатия кнопки для запуска фоновых операций через Start-ThreadJob:

    • Примечание: Start-Job тоже подойдет, но в этом случае код запускается в дочернем процессе, а не в потоке, что гораздо медленнее и имеет другие важные последствия.

    • "Запуск потока".
    • Также было бы несложно адаптировать пример для использования PowerShell SDK ([powershell]), но потоковые задания более идиоматичны для PowerShell и ими легче управлять с помощью обычных командлетов *-Job.

  • Отображение окна WPF не модально и вход в настраиваемый цикл событий:

    • В каждой операции цикла для обработки событий GUI вызывается заказная DoEvents()-подобная функция DoWpfEvents, адаптированная из DispatcherFrame документации.

      • Примечание: Для кода WinForms можно просто вызвать [System.Windows.Forms.Application]::DoEvents().
      • .
    • Кроме того, отслеживается ход выполнения заданий фонового потока, а полученные результаты добавляются в текстовое поле статуса конкретного задания. Завершенные задания очищаются.

Примечание: Точно так же, как если бы вы вызвали окно модально (с помощью .ShowModal()), поток переднего плана и, следовательно, консольная сессия блокируется на время отображения окна. Самый простой способ избежать этого - запустить код в скрытом дочернем процессе; предположим, что код находится в сценарии wpfDemo.ps1:

# В PowerShell [Core] 7+ используйте `pwsh` вместо `powershell`.
Start-Process -WindowStyle Hidden powershell '-noprofile -file wpfDemo.ps1'

Вы также можете сделать это через SDK, что будет быстрее, но это гораздо более многословный и громоздкий процесс:
$runspace = [runspacefactory]::CreateRunspace() $runspace.ApartmentState = 'STA'; $runspace.Open(); $ps = [powershell]::Create(); $ps.Runspace = $runspace; $null = $ps.AddScript((Get-Content -Raw wpfDemo.ps1)).BeginInvoke()

Скриншот:

.

На этом снимке экрана показана одна завершенная фоновая операция и одна текущая (их выполнение параллельно поддерживается); обратите внимание, что кнопка, запустившая текущую операцию, отключена на время выполнения операции, чтобы предотвратить повторный вход:

.

Снимок экрана фоновых операций WPF

Источник кода:

использование пространства имен System.Windows
используя пространство имен System.Windows.Threading

# Загрузите сборки WPF.
Add-Type -AssemblyName PresentationCore, PresentationFramework

# Определите XAML-документ, содержащий пару запускающих фоновую операцию
# кнопки и связанные с ними текстовые поля состояния.
[xml] $xaml = @""

    
        
        
        

Я искал решение весь день и наконец нашел его, поэтому я собираюсь опубликовать его здесь для тех, у кого такая же проблема.

Во-первых, ознакомьтесь с этой статьей : https://smsagent.blog/2015/09/07/powershell-tip-utilizing-runspaces-for-responsive-wpf-gui-applications/. Она хорошо объясняет и показывает, как правильно использовать runspaces с графическим интерфейсом WPF. Вам просто нужно заменить переменную $Window на $Synchhash.Window :

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

Вставьте функцию runspace в свой код :

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()
}

И вызовите его в нужном вам обработчике событий с указанными вами параметрами :

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

Не забудьте завершить runspace :

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

2022 WebDevInsider