在 Powershell 中将 Dispatcher Invoke 与运行空间结合使用

Using Dispatcher Invoke with a Runspace in Powershell

我正在尝试学习如何使用运行空间将值传递给 GUI。我一直在调整 Boe Prox 编写的脚本,试图了解 Dispatcher.Invoke 如何与运行空间一起工作并遇到一个非常奇怪的问题

$uiHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"          
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("uiHash",$uiHash)


          
$psCmd = [PowerShell]::Create().AddScript({   
    $uiHash.Error = $Error
    [xml]$xaml = @"
    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Name="Window" Title="Initial Window" WindowStartupLocation = "CenterScreen"
        Width = "650" Height = "800" ShowInTaskbar = "True">
        <TextBox x:Name = "textbox" Height = "400" Width = "600"/>
    </Window>
"@
    $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $uiHash.Window=[Windows.Markup.XamlReader]::Load( $reader )
    $uiHash.TextBox = $uiHash.window.FindName("textbox")
    $uiHash.Window.ShowDialog() | Out-Null
})
$psCmd.Runspace = $newRunspace
$handle = $psCmd.BeginInvoke()

#-----------------------------------------

#Using the Dispatcher to send data from another thread to UI thread
Update-Window -Title ("Services on {0}" -f $Env:Computername)
$uiHash.Window.Dispatcher.invoke("Normal",[action]{$uiHash.TextBox.AppendText('test')})

如果我使用没有 Update-Window -Title ("Services on {0}" -f $Env:Computername) 的脚本的最后一行,我会收到 you cannot call a method on a null-valued expression. InvokeMethodOnNull 错误并且不会附加文本。但是,如果我在 Dispatcher.invoke 行的正上方添加 Update-Window -Title ("Services on {0}" -f $Env:Computername),我仍然会收到错误消息,但文本框包含附加文本。

出现这种情况的原因是什么?我已经尝试了很多方法来使用 Dispatcher.Invoke 将内容添加到文本框,但总是以 cannot call a method method on null 错误结束,但没有任何成功,但现在添加了一些引用 UI 的行并调用Dispatcher.Invoke 似乎可以正常工作。

您的代码存在一些问题,可能会导致不稳定的错误。 首先,您是 运行 从 Powershell_ISE 还是从 powershell 控制台获取代码?此外,您是 运行 将脚本分为两部分,在 window 打开后从控制台进行调度程序调用,还是作为包含调度程序调用的单个脚本? 如果您将代码 运行 设置为单个脚本,那么问题是 "BeginInvoke" 运行 脚本位于单独线程中其自己的 运行 空间内。 在 window 被该线程正确创建之前,主线程已经在尝试设置标题和文本框的值。

如果您将代码分成两部分,即在一个脚本中调用 begininvoke 之前的所有内容,然后在主脚本中调用调度程序,那么代码也会出现问题,因为您需要将哈希表设为全局。

我已经修改了您的原始代码,因此它将 运行 放在一个脚本中。 注意添加 start-sleep 以延迟调度程序调用。 结果显示线程 ID 以及调用前后的时间(以滴答为单位),您可以清楚地看到开始调用之后的时间是在设置文本框时间之前。

$Global:uiHash = [hashtable]::Synchronized(@{})
$newRunspace =[runspacefactory]::CreateRunspace()
$newRunspace.ApartmentState = "STA"
$newRunspace.ThreadOptions = "ReuseThread"          
$newRunspace.Open()
$newRunspace.SessionStateProxy.SetVariable("uiHash",$Global:uiHash)



$psCmd = [PowerShell]::Create().AddScript({   
    $Global:uiHash.Error = $Error
    Add-Type -AssemblyName PresentationFramework,PresentationCore,WindowsBase
    $xaml = @"
    <Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Name="Window" Title="Initial Window" WindowStartupLocation = "CenterScreen"
        Width = "650" Height = "800" ShowInTaskbar = "True">
        <Grid>
        <TextBox x:Name = "textbox" Height = "400" Width = "600" TextWrapping="Wrap"/>
        </Grid>
    </Window>
"@
   # $reader=(New-Object System.Xml.XmlNodeReader $xaml)
    $Global:uiHash.Window=[Windows.Markup.XamlReader]::Parse($xaml )
    $Global:uiHash.TextBox = $Global:uiHash.window.FindName("textbox")
    $Global:uiHash.TextBox.Text = "Window Creation Thread Id: $([System.Threading.Thread]::CurrentThread.ManagedThreadId.ToString()) Time: $([System.Diagnostics.Stopwatch]::GetTimestamp()) `r`n" 
    $Global:uiHash.Window.ShowDialog() | out-null
})
$psCmd.Runspace = $newRunspace
$time1 = " Time before beginInvoke: $([System.Diagnostics.Stopwatch]::GetTimestamp()) `r`n"
$handle = $psCmd.BeginInvoke()

#-----------------------------------------
$time2 = " Time after beginInvoke: $([System.Diagnostics.Stopwatch]::GetTimestamp()) `r`n"
#Using the Dispatcher to send data from another thread to UI thread
Start-Sleep -Milliseconds 100
#Update-Window -Title ("Services on {0}" -f $Env:Computername)
$threadId = " Dispatcher Call Thread Id: $([System.Threading.Thread]::CurrentThread.ManagedThreadId.ToString())  Time: $([System.Diagnostics.Stopwatch]::GetTimestamp())`r`n "

$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText($time1)},"Normal")
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText($time2)},"Normal")
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.TextBox.AppendText($threadId)},"Normal")
$Global:uiHash.Window.Dispatcher.Invoke([action]{$Global:uiHash.Window.Title = "$($env:ComputerName)"},"Normal")

您可能还想下载 WPFRunspace,一个为 WPF/MSForms 和普通控制台 Powershell 脚本提供后台程序的 Powershell 模块。

控制台或 ISE 对实际 "multithreading" 没有真正的影响。 不同之处在于 ISE 使用的范围。 通常,除非您使用子脚本的作用域修饰符或点源,否则父 powershell 作用域无法访问子变量,但子作用域将继承父作用域的任何变量。

添加一个变量

$myFirstValue = "The first value"

在 AddScript 块和第二个变量之前

$mySecondValue = "The second value" 

在示例脚本的 AddScript 块中。然后在脚本的最后一行添加

$Global:uiHash.Window.Dispatcher.Invoke(
    [action]{$Global:uiHash.TextBox.AppendText($myThirdValue)},"Normal")

在 ISE 控制台中设置

的值
$myThirdValue = "your value"

在打开的 Powershell 控制台会话中执行相同的操作。 运行 ISE 和 powershell 控制台会话中的脚本。

在脚本为 运行 之后的 ISE 中,您将能够访问 $myFirstValue 但不能访问 $mySecondValue 的值,脚本将在 "your value"文本框。

在控制台会话中,$myFirstValue$mySecondValue 都不可访问,但 "your value" 会显示在文本框中。

为了解释正在发生的事情,子脚本作用域继承了 $myThirdValue 的值,因此它在所有情况下都会显示。 $mySecondValue 显然位于单独的 运行 空间中,因此在任何情况下都无法访问。 $myFirstValue 由于上面的一般范围规则,控制台会话无法访问。

ISE 是怎么回事,是否违反了一般规则?可能出于调试原因,ISE 中的所有窗格共享相同的范围。如果您从文件菜单打开 "new powershell tab",则会创建一个单独的 运行 空间,该变量将不再可用。

您可以使用 get-help about_scope 获得更多帮助。

所有这一切的要点是,如果您正在编写分布在多个文件中的脚本,您可能需要使用 global/script 修饰符来确保它在从 ISE 迁移到控制台会话时正常运行。 我将使用 ISE 编写我的脚本,但会 运行 在同时打开的控制台会话中编写它们,因为 ISE 始终有可能保留控制台会话可能无法使用的值。

我提到这个是因为在示例脚本中你可以(正如我认为 BoeProx 的意图)从脚本中删除最后四行然后再次 运行 脚本,同时 window 打开您可以在控制台中输入4行并更改open window。这样做意味着您不再需要 start-sleep 因为当您在键盘上输入第一行时 window 已经打开(除非您真的非常快)。