在Powershell中使用Dispatcher Invoke和Runspace

时间:2015-12-23 17:10:24

标签: wpf powershell runspace

我正在尝试学习如何使用Runspace将值传递给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的行似乎让它发挥作用。

2 个答案:

答案 0 :(得分:1)

您的代码存在一些问题,可能导致错误的错误。 首先,您是从Powershell_ISE还是从PowerShell控制台运行代码?另外,您是在两个部分中运行脚本,在窗口打开后从控制台调用调度程序,还是作为包含调度程序调用的单个脚本? 如果您像单个脚本一样运行代码,那么问题是&#34; BeginInvoke&#34;在单独的线程中在自己的运行空间中运行脚本。 在该线程正确创建窗口之前,主线程已经在尝试设置title和文本框的值。

如果你要将代码拆分为两部分,即在一个脚本中调用所有内容以开始调用,然后在主脚本中调度调度程序,则代码也会出现问题,因为您需要使哈希表全局化。

我修改了您的原始代码,以便它可以在一个脚本中运行。 注意启动睡眠的附加项以延迟调度程序调用。 结果显示了线程ID以及调用之前和之后的时间(以刻度表示),您可以清楚地看到开始调用之后的时间是在设置文本框时间之前。

&#13;
&#13;
$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")
&#13;
&#13;
&#13;

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

答案 1 :(得分:1)

控制台或ISE是否对实际的“多线程”没有实际影响。 不同之处在于ISE使用的范围。 通常,除非使用范围修饰符或子脚本的点源,否则父权限范围不能访问子变量,但子范围将继承父变量的任何变量。

在AddScript块之前添加变量$ myFirstValue =“第一个值”,第二个变量$ mySecondValue =“第二个值” 在示例脚本中的AddScript块中。然后在脚本的最后一行添加 $全球:uiHash.Window.Dispatcher.Invoke([动作] {$全球:uiHash.TextBox.AppendText($ myThirdValue)}, “正常”)

在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行改变打开的窗口。这样做意味着您不再需要启动 - 睡眠,因为当您在键盘上输入第一行时,窗口已经打开(除非您真的非常快)。