1

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 エラーが発生し、テキストが追加されません。ただし、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 コンソールからコードを実行していますか? また、スクリプトを 2 つの部分に分けて実行し、ウィンドウを開いた後にコンソールからディスパッチャー呼び出しを行っていますか、それともディスパッチャー呼び出しを含む単一のスクリプトとして実行していますか? コードを単一のスクリプトとして実行している場合、問題は、「BeginInvoke」が別のスレッドの独自の実行空間内でスクリプトを実行することです。このスレッドによってウィンドウが適切に作成される前に、メイン スレッドはすでにタイトルとテキスト ボックスの値を設定しようとしています。

コードを 2 つの部分に分割する場合、つまり、begininvoke までのすべてを 1 つのスクリプトで呼び出し、次にメイン スクリプトでディスパッチャ呼び出しを行う場合、ハッシュテーブルをグローバルにする必要があるため、コードにも問題が生じます。

元のコードを変更して、単一のスクリプトで実行できるようにしました。ディスパッチャーの呼び出しを遅らせるための 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")

また、WPF/MSForms のバックグラウンドワーカーと通常のコンソール Powershell スクリプトを提供する Powershell モジュールであるWPFRunspaceをダウンロードすることもできます。

于 2015-12-23T22:14:25.010 に答える
1

コンソールと ISE のどちらでも、実際の「マルチスレッド」に大きな違いはありません。違いは、ISE で使用されるスコープにあります。一般に、スコープ修飾子または子スクリプトのドット ソースを使用しない限り、親 PowerShell スコープは子変数にアクセスできませんが、子スコープは親の変数を継承します。

変数を追加する

$myFirstValue = "The first value"

AddScript ブロックと 2 番目の変数の前に

$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 に答える