1

我正在尝试学习如何使用运行空间将值传递给 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 错误的情况下使用脚本的最后一行,并且不会附加文本。但是,如果我Update-Window -Title ("Services on {0}" -f $Env:Computername)在 Dispatcher.invoke 行的正上方添加,我仍然会收到错误,但文本框包含附加的文本。

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

4

2 回答 2

2

您的代码有几个问题可能会导致不稳定的错误。首先,您是从 Powershell_ISE 还是从 powershell 控制台运行代码?此外,您是在窗口打开后从控制台进行调度程序调用的两部分运行脚本,还是作为包括调度程序调用的单个脚本运行?如果您将代码作为单个脚本运行,那么问题在于“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,这是一个 Powershell 模块,它为 WPF/MSForms 和普通控制台 Powershell 脚本提供后台工作程序。

于 2015-12-23T22:14:25.010 回答
1

控制台还是 ISE 对实际的“多线程”没有真正的影响。不同之处在于 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并且脚本将在文本框中显示“您的值”。

在控制台会话中,既$myFirstValue不能访问也不能$mySecondValue访问,但“您的价值”会显示在文本框中。

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

ISE 发生了什么,它是否违反了一般规则?可能出于调试原因,ISE 中的所有窗格共享相同的范围。如果您从文件菜单中打开“新的 powershell 选项卡”,则会创建一个单独的运行空间,并且该变量将不再可用。

您可以使用get-help about_scope.

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

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

于 2016-01-05T17:21:11.720 回答