2013年12月25日水曜日

PowerShell で DSL - Create the DSL for the template engine that generates automatically the C++ library code that hooks the JIT of a program written by C# -

"……君のような勘のいいガキは嫌いだよ"


PowerShell Advent Calendar 2013、25 日目!昨年に引き続き、今年も PowerShell Advent Calendar に参加させていただきました、杉浦と申します。
昨日は @oota_ken さんの『PesterのMock機能をもう少し詳しく│ソフトウェアテストラボ|アプリテスト|スマートフォンテスト|株式会社シフト』でした。単体テスト周りの話は、私自身、テストダブル生成フレームワークを作っていく過程で、今回題材にさせていただいているような DSL を実装するに至ったこともあり、非常にタメになるところです。お疲れ様でした!

さて、直近で 12/21 にありました 第1回 PowerShell勉強会で登壇させていただいたこともあり、今回はそのフォローアップ記事になればと。

発表に使った資料はこちらに公開してあります:

私はこのような場所でセッションを行うのは初めてでしたので、お聞き苦しい点やわかりにくい点もあったかと思いますが、いかがでしたでしょうか?
PowerShell が持つ脅威の柔軟性の、ほんの一端でも伝われば幸いに思います。

では、早速行ってみましょう!!

※今回の記事で扱うのは、PowerShell v2 の情報となります。PowerShell v3/v4 での情報は、また次の機会に・・・。
※まとめる内に、基本的な部分の解説と応用的な部分の解説とでは分けたほうが良さそうに思えてきましたので、今回は基本的な部分を。続きはまた後日執筆させていただければと思います・・・。


こちらの情報を参考にさせていただいています。もしご自分で何かしらの DSL を実装される場合は、何かと参考になるかもしれません! (゚∀゚)
PowerShellでプロトタイプベースのオブジェクト指向を記述する方法 - 趣味の無い人生は虚しい
Writing your own PowerShell Hosting App (Part 1. Introduction) PowerShell Station
JavaScrip のクラスの多重継承(の、ようなもの):みみちゃんblog - プログラムの園:So-netブログ
Rubyist Magazine - Refinementsとは何だったのか
Adding New Type Accelerators - Power Tips - PowerShell.com – PowerShell Scripts, Tips, Forums, and Resources
Detouring Win32 Function Calls in PowerShell Adam Driscoll's Blog
Generating Fakes Assemblies with PowerShell Adam Driscoll's Blog
Inside PowerShell 3 The New Parser and Compiler Adam Driscoll's Blog
http://ps2exe.codeplex.com/
c# - Programmatic equivalent of default(Type) - Stack Overflow
c# - alternative for using slow DynamicInvoke on muticast delegate - Stack Overflow
dot net figured out How to get PowerShell current runspace from C#
c# - How to prevent blank xmlns attributes in output from .NET's XmlDocument - Stack Overflow
powershell Adding the Using Statement - The Technical Adventures of Adam Weigert
オブジェクト指向 - アンサイクロペディア





目次

PSAnonym.Prototype 全体像
資料の中にありますPSAnonym.Linq は、昨年の Advent Calendar で紹介させていただいていますので、今回は PSAnonym.Prototype のほうを。
PSAnonym.Prototype は、多重継承をサポートしたプロトタイプベースのオブジェクト指向言語です。PowerShell には通常存在しない、いわゆるクラス(というか、この場合プロトタイプですね)定義構文を導入するモジュールになります。
勉強会でやったものより、若干大き目のサンプルで、全体像を見てみることにしましょう:
# キャラを表すプロトタイプ
$キャラ = 
    Prototype キャラ {
        # フィールド、プロパティ
        Field 今日は話した $false -Hidden
        Field 好感度 0 -Hidden
        Field 難易度 10 -Hidden
        Field 状態確認回数 10 -Hidden
        Field 名前 ([string].default) -Hidden
        Property 状態 {
            if (0 -lt $Me.状態確認回数-- - $Me.難易度) {
                '好感度: ' + $Me.好感度
            }
        }

        # コンストラクタ
        New {
            if ($null -eq $Params -or 10 -lt $Params[0]) {
                $難易度 = 10
            } else {
                $難易度 = $Params[0]
            }
            $Me.難易度 = $難易度
            $Me.名前 = $Params[1]
        }
        
        # メソッド
        Method 会う {
            $Me.今日は話した = $false
            $Me.会う中身()
        }
        AbstractMethod 会う中身
        
        Method 話す {
            if ($Me.今日は話した) {
                $Me.好感度--
                $Me.話す中身_好感度下げ()
            } else {
                $Me.今日は話した = $true
                $Me.好感度++
                $Me.話す中身_好感度上げ()
            }
        }
        AbstractMethod 話す中身_好感度下げ
        AbstractMethod 話す中身_好感度上げ
        
        AbstractMethod 誘う
    }

# ツンキャラを表すプロトタイプ
$ツンキャラ = 
    $キャラ | 
        Prototype ツンキャラ |
            OverrideMethod 会う中身 { 'な、なによ' } | 
            OverrideMethod 話す中身_好感度下げ { 'バカ、しつこい' } | 
            OverrideMethod 話す中身_好感度上げ { 'べ、別に' } | 
            OverrideMethod 誘う { $Me.好感度--; 'バカ、なに考えてるのよ' }

# デレキャラを表すプロトタイプ
$デレキャラ = 
    $キャラ | 
        Prototype デレキャラ |
            OverrideMethod 会う中身 { '....おはよう' } | 
            OverrideMethod 話す中身_好感度下げ { 'しつこいわよ' } | 
            OverrideMethod 話す中身_好感度上げ { 'そうね' } | 
            OverrideMethod 誘う { $Me.好感度++; 'ありがとうっ、楽しみね' }

# ツンデレキャラを表すプロトタイプ
$ツンデレキャラ = 
    $ツンキャラ, $デレキャラ | 
        Prototype ツンデレキャラ {
            OverrideMethod 会う中身 { 
                if ($Me.好感度 % 3 -eq 0) {
                    $ツンキャラ.会う中身()
                } else {
                    $デレキャラ.会う中身()
                }
            }
            
            OverrideMethod 話す中身_好感度下げ { 
                if ($Me.好感度 % 3 -eq 0) {
                    $ツンキャラ.話す中身_好感度下げ()
                } else {
                    $デレキャラ.話す中身_好感度下げ()
                }
            }
            
            OverrideMethod 話す中身_好感度上げ { 
                if ($Me.好感度 % 3 -eq 0) {
                    $ツンキャラ.話す中身_好感度上げ()
                } else {
                    $デレキャラ.話す中身_好感度上げ()
                }
            }
            
            OverrideMethod 誘う { 
                if ($Me.好感度 % 3 -eq 0) {
                    $ツンキャラ.誘う()
                } else {
                    $デレキャラ.誘う()
                }
            }
        } -Force

$舞 = $ツンデレキャラ.New((2, '舞'))

'1 日目: {0} ---' -f $舞.名前
$舞.会う()
$舞.話す()
$舞.話す()
$舞.誘う()

'2 日目: {0} ---' -f $舞.名前
$舞.会う()
$舞.話す()
$舞.誘う()

'3 日目: {0} ---' -f $舞.名前
$舞.会う()
$舞.話す()
$舞.誘う()
$舞.誘う()

# 結果------------------------
# 1 日目: 舞 ---
# な、なによ
# そうね
# バカ、しつこい
# バカ、なに考えてるのよ
# 2 日目: 舞 ---
# ....おはよう
# べ、別に
# バカ、なに考えてるのよ
# 3 日目: 舞 ---
# ....おはよう
# べ、別に
# バカ、なに考えてるのよ
# ありがとうっ、楽しみね
私の Blog には、ちょっと似つかわしくない題材なのですが、大き目のサンプルで複数の生き物を合成するのは、どこかの錬金術師的な流れになる危険がありましたので。。。(^_^;)
もう少し柔らかめな表現しやすいものとして、定番のツンデレキャラを作ってみました。罵倒されても、挫けずに誘い続けることが大切です!・・・みたいな教訓めいたものは何もなく、適当に組んだらそれっぽい結果になったので驚いています。これがクリスマスの魔法でしょうか。

さて、コマンドと宣言構文で、何をしたいかは大体想像がついてしまうかもしれませんが、次節から、各項目の簡単な説明をさせていただければと思います。





Prototype(New-Prototype) 関数
プロトタイプは、そのまんまではありますが Prototype という宣言文で始まります。これは、New-Prototype 関数のエイリアスになっています。コマンドのシグニチャはこんな感じ(※共通のスイッチは…で省略しています):
PS C:\> New-Prototype -?
New-Prototype [-Name] <String> [[-Declaration] <ScriptBlock>] [-InputObject <PSObject[]>] [-Force] …
 
[-Name] はプロトタイプの名前で、多重継承の時などに、親プロトタイプを明示的に指定する時にも使います。必須の項目はこれだけですので、空のプロトタイプを作り、後からメソッドやプロパティを足すことも可能です。
次の [-Declaration] は、スクリプトブロックを用いた構文をサポートするために使うものです。上記のサンプルですと、$キャラ や $ツンデレキャラ を定義するのに、この構文を使っていますね。逆に、$ツンキャラ、$デレキャラ の宣言はパイプラインを使って定義されていることも見て取れると思います。

このようなスクリプトブロックを用いた汎用言語に近い構文と、パイプラインを使った構文の両方をサポートするのは、若干手間ではあるのですが、個人的には外せなかった要件でした。スクリプトブロックを用いたほうですと、処理がある程度の大きさになった場合にコメントを挟むなどして説明が入れやすくなるという利点があります。パイプラインでも、途中経過を変数に入れればできないことは無いのですが、どうしても宣言と処理がごっちゃになりやすい・・・。逆にある程度の大きさになるまでは、パイプラインで繋ぐほうが簡潔で読みやすく、また今回の勉強会でやったようなデモをやる場合もお手軽にできるという利点があります。
どちらも一長一短があり、状況に応じて使い分けたかったので、ハイブリッド構文を採用するに至った次第です。DSL を自分で実装する場合、この辺りのさじ加減を自分で決められるのは良い点ですね!

はい、説明に戻ります。最後の [-Force] スイッチは、重複するメンバーが存在する場合に、無理やり上書きするかどうかを指定するフラグです。私が思うに、本来、多重継承がうまく働くのは、状態を持ち、かつ重複しない概念を混ぜ合わせて、新たな概念を作りたくなった場合のはずですので、このフラグが必要になるようなものは、本当にキレイな設計では無い可能性があります。注意して見直しが必要でしょう。
※ただ、実際はそうそううまく分かれることもなかったり・・・。目的であるテンプレートエンジンへの適用という意味では、実益はあったので良いのですが・・・。多重継承の是非みたいな話は、個人的にはこの辺りを辿ると、わからないなりに納得できたように思いましたが、まだ勉強の必要がありそうです。。。





Field(Add-Field) 関数
プロトタイプには Field という宣言文でフィールドの定義が可能です。これも実際は Add-Field 関数のエイリアスですね。コマンドのシグニチャはこんな感じ:
PS C:\> Add-Field -?
Add-Field [-Name] <String> [-Value] <Object> [-InputObject <PSObject>] [-Hidden] …
 
[-Name] はフィールド名です。ここに付けた名前に対し、同じプロトタイプのメンバー(コンストラクタ、メソッドやプロパティ)からであれば、$Me.フィールド名 でアクセスが可能になります。
[-Value] で初期値を指定します。初期値は必須で、かつ最初に与えられた値で型が決まるようになっています。こういう書き方は、はじくようになっているということですね:
PS C:\> $a = Prototype A | Field Value 10
PS C:\> $a.Value = 'aiueo'
. : Cannot convert value "aiueo" to type "System.Int32". Error: "Input string was not in a correct format."

At line:1 char:4
+ $a. <<<< Value = 'aiueo'
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : PropertyAssignmentException
 
 
自分が静的言語に関わっている時間が長いこともあり、ちょっとした型注釈は、できればシステム側で見守っていてほしい派だったりします。
このため、フィールドには、型の決定が行えるよう、$null は直接指定できないようにしています。初期値として $null を指定したい場合は、上記の例のように($キャラ を定義している 9 行目)、([型名].default) で初期値を指定するようにします。

後は、[-Hidden] スイッチを付けることで、Get-Member の結果に現れなくなります。そのプロトタイプ内部で使うだけの属性であれば、このスイッチで隠してしまったほうが扱いやすくなると思います。
・・・おっと、[-InputObject <PSObject>] を忘れていました。それもそのはずで、これは通常、意識する必要はありません。パイプラインを使った構文でプロトタイプを宣言する場合に自動的に利用されるもので、プロトタイプ以外のものを指定すると、エラーになるようになっています。





Property(Add-Property) 関数
プロパティの宣言文は Property です。例によって、Add-Property 関数のエイリアスになっています。コマンドのシグニチャを見てみましょう:
PS C:\> Add-Property -?
Add-Property [-Name] <String> [[-Getter] <ScriptBlock>] [[-Setter] <ScriptBlock>] [-InputObject <PSObject>] [-Hidden] …
 
Getter と Setter で、それぞれスクリプトブロックを指定する必要がありますので、若干大き目ですが、内容に特筆すべきところは無いですね。
[-Name] にプロパティ名、[[-Getter] <ScriptBlock>] に取得処理のスクリプトブロック、[[-Setter] <ScriptBlock>] に設定処理のスクリプトブロック、となっています。あ・・・、今見ると [-Hidden] スイッチは、Override が絡むとうまく動いていないようですね・・・直さねば (-_-;)

ところで、標準の Add-Member ScriptProperty もそうなのですが、PowerShell のプロパティは、基本的に例外を握りつぶす仕様になっているようです:
PS C:\> $a = New-Object psobject | Add-Member ScriptProperty Value { throw New-Object NotImplementedException } -PassThru
PS C:\> $a.Value

# ここでは何も出ない
PS C:\> $a.psobject.properties.match('value')[0].getterscript.invoke()
invoke : Exception calling "Invoke" with "1" argument(s): "The method or operation is not implemented."

At line:1 char:61
+ $a.psobject.properties.match('value')[0].getterscript.invoke <<<< ()
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException
 
# Getter 部分を直接メソッドとして呼び出すと例外が発生していることがわかる
 
PSAnonym.Prototype でも、内部的には ScriptProperty を使っていますので、この動きに習う形になります。ただ、プロパティ内でエラーが発生していることに気づかないと、スクリプト実行後の $Error.Count がどえらいことになっていたりするので、時々調べてみるのをオススメしますです ヽ(;▽;)ノ





New(Set-New) 関数
コンストラクタの宣言文です。これも Set-New 関数のエイリアスとして、New が指定してあります。コマンドのシグニチャは一番単純ですね:
PS C:\> Set-New -?
Set-New [-Body] <ScriptBlock> [-InputObject <PSObject>] …
 
[-Body] でコンストラクタの処理を指定するだけです。構文自体も簡単ですが、その呼び出され方も極力簡略化されたものになっています。
サンプルで $キャラ に定義されたコンストラクタ(17 行目)ですが、その派生プロトタイプのいずれも、明示的に呼んではいないことに気づかれたでしょうか?
標準的な PowerShell の引数の引き回し方($Args 配列によるもの)に習い、PSAnonym.Prototype でも $Params という配列に一連の引数を詰めて引き回します。派生プロトタイプが途中で引数を加工しない限り、この引数はそのまま基底プロトタイプまで引き渡されますので、このような簡略化が可能になっているというわけですね。
ちなみに、自分自身を表す変数が Me だったり、コンストラクタが New だったりするのは、Visual Basic みたいだなと思われた方もいらっしゃるかもしれませんが、間違いなく VB の影響だということをここで告白しておきます。タイプ数が少なく、他と被りにくいキーワードとして優秀なのですよー! ゚ .(・∀・)゚ .゚





Method(Add-Method) 関数
基本的な構文の最後はメソッドの宣言です。Add-Method 関数のエイリアスとして Method が定義してあります。コマンドのシグニチャはこの通り:
PS C:\> Add-Method -?
Add-Method [-Name] <String> [-Body] <ScriptBlock> [-InputObject <PSObject>] [-Hidden] …
 
[-Body] <ScriptBlock> で例外が発生した場合、ちゃんと外側までスローされるのがプロパティとの大きな違いです。
それ以外は特に無いのですが、中心になる機能ということもあり、中身は色々やっています。パフォーマンス向上の工夫、多重継承の実現方法やモジュール性の確保の方法など・・・後日執筆予定の応用編では、それらに触れられればと思いますですね・・・(>_<)。





続く...?
PowerShell Advent Calendar 2013 の 25 日目を担当させていただきましたが・・・うーん、すみません、まとめきれませんでした・・・orz。
この PowerShell の、使えば使うほどできることが広がっていく感じは、実用性だけでなく、単純にプログラミング自体を楽しめるということにも繋がるのでは、なんて思っていたりするのですが、まだまだその域には達していない感じです。精進せねば!
また機会があれば、PowerShell が持つ力を広めるお手伝いができればと思います。それでは、良いお年を!!



2013年12月18日水曜日

Snoop 大作戦 - Mission: in PowerShellable -

"なお、この記事は自動的に消滅する。"


初めましての方は初めまして!XAML Advent Calendar 2013、18 日目を担当させていただきます、杉浦と申します。

個人的には .NET(CLR)の仕組みや開発基盤のことに関心があり、Twitter ではいつもそんなことばかり呟いていますが、お仕事ではそうそう低レイヤーなことばかりできるはずもなく、実際は WPF を使ったデスクトップアプリを開発してる時間のほうが多かったりするのです。
そんなわけで、XAML Advent Calendar1 日目の ぐらばく(@Grabacr07)さんの記事を見た瞬間、これはどんぴしゃりだと。私も何か貢献できればと参加させていただいた次第です。

さて、今回私が取り上げるのは、WPF 向けの開発ツールとして有名な Snoop
ここで言う開発ツールとは、実行中のアプリのオブジェクトの状態を確認したり、デバッガではできても難しい、もしくは手間という操作をできるようにしてくれたりする補助的なツールのことを指しています。

WPF が出た当時は様々なものがあった開発ツールも、すっかり淘汰されて、今や残っているのは一握り。
逆に言うと、XAML プラットフォームでも最初のほうに出た WPF 開発はだいぶ枯れてきているのでしょう。

今さらな感じはあるのですが、改めて Snoop にスポットを当て、情報共有の場にさせていただければと思いますです。



こちらの情報を参考にさせていただきました。自分ももっと参考にされるようにならねば!(`・ω・´)
c# - Useful WPF utilities - Stack Overflow
XAML Wonderland » Blog Archive » Shazzam – WPF Pixel Shader Effect Testing Tool now available
XamlPadX 4.0 - Lester's WPF\Silverlight Blog - Site Home - MSDN Blogs
Mole | Enterprise Touch This and Karl on WPF
Mole For Visual Studio - With Editing - Visualize All Project Types - CodeProject
Kaxaml
Announcing Pistachio – “WPF Resource Visualizer” - Grant Hinkson Blog
ZAM 3D - 3D XAML 3D WPF 3ds to XAML and dxf to XAML converter Tool for Windows Vista and WinFX
XAML Exporter for Blender - Home
AB4D - Paste2Xaml application can convert clipboard and metafiles into XAML for WPF and Silverlight
.NET Reflector Add-Ins - Home
Crack.NET - Home
Bindingの状況をTraceする | 泥庭





目次

Mission1: 最初の指令
Snoop は、CodePlex からダウンロードできます。2013/12/18 現在の最新版は、2012/10/04 付けでリリースされている 2.8.0 となっています。
zip ファイルにはインストーラが同梱されていますので、それを実行します。特にデフォルト値から変更する必要はないと思います。インストール後、起動すると、こんな感じの棒状のアプリが起動するはずです。


これだけだとなんじゃらほいですので、説明のために以下のようなアプリを書いてみました(ソースコード一式はこちらに):
MainWindow.xaml
<Window x:Class="SnoopWithPowerShell.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        Title="MainWindow" Name="MainWindow1" Height="160" Width="350">
    <Window.Resources>
        <!-- デフォルトの枠スタイル -->
        <Style TargetType="{x:Type Border}" x:Key="DefaultBorderStyleKey">
            <Setter Property="Width" Value="300" />
            <Setter Property="BorderThickness" Value="0.1" />
            <Setter Property="BorderBrush" Value="Black" />
            <Setter Property="CornerRadius" Value="5" />
            <Setter Property="Padding" Value="5" />
            <Setter Property="HorizontalAlignment" Value="Left" />
        </Style>
        
        <!-- int 型向けのデータテンプレート -->
        <DataTemplate DataType="{x:Type sys:Int32}">
            <Border Style="{StaticResource DefaultBorderStyleKey}">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Column="0" Text="{Binding}" />
                    <Button Grid.Column="1" Content="押してね" Click="Button_Click" />
                </Grid>
            </Border>
        </DataTemplate>

        <!-- DateTime 型向けのデータテンプレート -->
        <DataTemplate DataType="{x:Type sys:DateTime}">
            <Border Style="{StaticResource DefaultBorderStyleKey}">
                <StackPanel Orientation="Horizontal">
                    <TextBlock>
                        <Run FontWeight="Bold" Text="{Binding Year, Mode=OneWay}" /> 年も、もうすぐ終わりだよ
                    </TextBlock>
                </StackPanel>
            </Border>
        </DataTemplate>

        <!-- Decimal 型向けのデータテンプレート -->
        <DataTemplate DataType="{x:Type sys:Decimal}">
            <Border Style="{StaticResource DefaultBorderStyleKey}">
                <TextBlock Text="{Binding StringFormat={}{0:C}}" />
            </Border>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <!-- ItemsControl と ComboBox に内容を並べてみる -->
        <ItemsControl Grid.Row="0" ItemsSource="{Binding}" />
        <ComboBox Grid.Row="1" Name="ComboBox1" ItemsSource="{Binding DataContxet, ElementName=MainWindow1}" 
                  SelectionChanged="ComboBox1_SelectionChanged" />

    </Grid>
</Window>

MainWindow.xaml.cs
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;

namespace SnoopWithPowerShell
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext =
                new ObservableCollection<object>() 
                {
                    42, 
                    new DateTime(2013, 12, 18), 
                    10000m
                };
        }

        void ComboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            throw new NotImplementedException();
        }

        void Button_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show(((Button)sender).DataContext + "");    
        }
    }
}

ビルドし、実行しますと、以下のような画面が立ち上がります。


先ほどの棒状のアプリの双眼鏡アイコンの右にあるターゲットマークをドラッグし、Snoop したいアプリ上でドロップすると・・・



WPF アプリケーションの描画要素、いわゆる Visual Tree が俯瞰できるようになります!




Mission2: 各項目値を奪え
感の良い方ならすでにお気づきかもしれませんが、今回調査対象にしているアプリは Binding がうまくいっていないところや、未実装の部分をわざと作ってあります。基本的な操作をおさらいしながら、実際に問題がある箇所を調査してみることにしましょう。
動かして気づくのは、ComboBox の中身が全然入っていないところです。


ComboBox には、ItemsSource にコレクションを Binding しているはずですね。Visual Tree を辿り、ItemsSource を確認してみると・・・


なにやらセルが赤くなっており、ただならぬ雰囲気を醸し出してます。まあ、実際エラーなのですが (^_^;)
この場合、大抵は右クリック - [Display Binding Errors] でエラーの内容を確認することができます。

"・・・BindingExpression path error: 'DataContxet' property not found on 'object' ''MainWindow' (Name='MainWindow1')'.・・・" というわけで、DataContxet プロパティは MainWindow に存在しないよ、とのエラーメッセージががが。
よくよく見れば、DataContxet は DataContext の typo ですね!これを修正して、リビルドし、実行すると・・・

MainWindow.xaml
        <ComboBox Grid.Row="1" Name="ComboBox1" ItemsSource="{Binding DataContext, ElementName=MainWindow1}" 
                  SelectionChanged="ComboBox1_SelectionChanged" />



ComboBox にも、上に配置していた ItemsControl と同じものが表示されるようになりました!!
ちなみに Binding エラーは、PresentationTraceSources の TraceLevel レベルを指定することにより、デバッグ中の Visual Studio コンソールにもっと詳細な情報を出すこともできます。Snoop を使ったカジュアルな方法で確認しきれない場合は、そちらも試すと良いでしょう。





Mission3: 戦慄のスクリプター養成所
すでにここまででも、エラー箇所の直観的な把握や、実行中のプロパティ変更、DataTemplate 適用先の要素の型調査などができるようになるわけで、相当 WPF アプリ開発が捗るようになるわけですが、実は Snoop 2.7.1 → 2.8.0 のバージョンアップの際、さらにすばらしい機能の拡張が行われました。
そう、我らが Windows 標準搭載にして脅威の柔軟性を持つスクリプト言語、PowerShell の組み込みです!!!


"To get started, try using the $root and $selected variables." とありますので、とりあえずコンソールに $root と入力し、Enter キーを押下してみます。
snoop:> $root


MainVisual          : SnoopWithPowerShell.MainWindow
Target              : SnoopWithPowerShell.App
Parent              : 
Depth               : 0
Children            : {[001] MainWindow1 (MainWindow) 33}
IsSelected          : True
IsExpanded          : False
TreeBackgroundBrush : #FFF0F0F0
VisualBrush         : 
HasBindingError     : False

 

※注※見易さのため、Snoop の PowerShell ペインとは若干見え方を変えてあります。
ここでは、実際に入力するコマンドを、snoop:> の隣に出していますが、実際は何も表示されません (>_<)
また、普通の PowerShell コンソールで実行しているスクリプトは、PS C:\> で始めるようにしています※注※


どうやら、Target プロパティに入っているものが、現在 Visual Tree 上で選択されているもののようですね。
PowerShell を使い慣れている方だと、ここでふと気づき、このコマンドを実行してみるかもしれません。結果は・・・
snoop:> pwd

Path
----
snoop:\

 
おおっ!通ります。やはりカスタムプロバイダーも実装されているようです。Visual Tree の移動も試してみましょう。
snoop:> dir


PSPath              : snoop::MainWindow
PSParentPath        : 
PSChildName         : MainWindow
・・・(略)・・・
Target              : SnoopWithPowerShell.MainWindow
Parent              : [000]  (App) 34
・・・(略)・・・




snoop:> cd main*

snoop:> dir


PSPath              : snoop::MainWindow\ResourceDictionary
PSParentPath        : snoop::MainWindow
PSChildName         : ResourceDictionary
・・・(略)・・・
Target              : {DataTemplateKey(System.DateTime), DataTemplateKey(System.Decimal), DefaultBorderStyleKey, DataTemplateKey(System.Int32)}
Parent              : [001] MainWindow1 (MainWindow) 33
・・・(略)・・・

PSPath              : snoop::MainWindow\Border
PSParentPath        : snoop::MainWindow
PSChildName         : Border
・・・(略)・・・
Target              : System.Windows.Controls.Border
Parent              : [001] MainWindow1 (MainWindow) 33
・・・(略)・・・

 
ただ、さすがに全ての実装はされていない様子 (^^ゞ
snoop:> cls
"2" 個の引数を指定して "SetBufferContents" を呼び出し中に例外が発生しました: "メソッドまたは操作は実装されていません。"発生場所 行:9 文字:1
+ $Host.UI.RawUI.SetBufferContents($rect, $space)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  + CategoryInfo          : NotSpecified: (:) [], ParentContainsErrorRecordException
  + FullyQualifiedErrorId : NotImplementedException


snoop:> dir -r


PSPath              : snoop::MainWindow\Border\AdornerDecorator
PSParentPath        : snoop::MainWindow\Border
PSChildName         : AdornerDecorator
・・・(略)・・・
Target              : System.Windows.Documents.AdornerDecorator
Parent              : [002]  (Border) 32
・・・(略)・・・




cls(Clear-Host) は、キーボードの F12 キーを押下することで代用できるのですが、残念ながら dir(Get-ChildItem) -r(-Recurse) はそうも行きません。
まあ、Snoop は GitHub で運用されていますので、Pull Request を送ってみるのが一つの手かもしれませんね。





Mission4: 極秘?情報を奪回せよ
実は先ほどのカスタムプロバイダーのお話は、本流に Merge される前、こんな機能を付け足してみた、という作者さんのブログ記事で語られているお話なのですが、どうも公式には紹介されていない情報もあるような・・・。
コミットログを眺めていると、PowerShell 関係の修正の中で、度々 "Snoop.psm1" なるモジュールが変更されていることがわかります。
ん?Snoop 専用のモジュールってこと?調べてみましょう:
snoop:> gmo

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     0.0        Snoop                               {Find-By, Find-ByName, Find-ByType, Get-SelectedDataContext}

 
何やら Export されたメンバーが見れますね!ヘルプヘルプ・・・っと φ(..)
snoop:> help Find-By


    
    

    
    
    

    
    
    

    




    
    
    





・・・orz。cls(Clear-Host) などと同様、完全に Host への出力がフックできているわけではないようです。そうすると・・・GitHub に上がっている本流のソースコードを読んでも良いですが、ここは実際にインストールされているものを確認するほうが賢明でしょう。

ちなみに、PowerShell のモジュールについての詳しい説明は、今年の PowerShell Advent Calendar 2 日目の記事として投稿されている、ぎたぱそ(@guitarrapc_tech)さんの記事が詳しいのでそちらも参考にされると良いと思います。

さて、Get-Module の結果を Format-List すると・・・
snoop:> gmo | fl


Name              : Snoop
Path              : C:\Program Files (x86)\Snoop\Scripts\Snoop.psm1
Description       : 
ModuleType        : Script
Version           : 0.0
NestedModules     : {}
ExportedFunctions : {Find-By, Find-ByName, Find-ByType, Get-SelectedDataContext}
ExportedCmdlets   : 
ExportedVariables : 
ExportedAliases   : 

 
なるほど。%Snoop のインストールディレクトリ%\Scripts\Snoop.psm1 に配置されているものということがわかります。普通の PowerShell コンソールで読み込んでみましょう。
PS C:\> ipmo 'C:\Program Files (x86)\Snoop\Scripts\Snoop.psm1'
PS C:\> gmo

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Manifest   3.1.0.0    Microsoft.PowerShell.Management     {Add-Computer, Add-Content, Checkpoint-Computer, Clear-Con...
Manifest   3.1.0.0    Microsoft.PowerShell.Utility        {Add-Member, Add-Type, Clear-Variable, Compare-Object...}
Script     0.0        Snoop                               {Find-By, Find-ByName, Find-ByType, Get-SelectedDataContext}


PS C:\> gcm -Module Snoop

CommandType     Name                                               ModuleName
-----------     ----                                               ----------
Function        Find-By                                            Snoop
Function        Find-ByName                                        Snoop
Function        Find-ByType                                        Snoop
Function        Get-SelectedDataContext                            Snoop

 
さあ、今度こそ!
PS C:\> help Find-By

名前
    Find-By

概要
    Recursively finds an element contained in the visual tree matched using a predicate.


構文
    Find-By [-predicate] <ScriptBlock> [-select] [<CommonParameters>]

 
詳しい説明はありませんが、コマンドレット名と引数も合わせればなんとなく使い方がわかりますね。
Find-By は、引数に Visual Tree の各要素を取り、bool 値を返すスクリプトブロックを指定することで、条件に合致した要素を一発で取得することができます。Snoop 上で使うとこんな感じ。
snoop:> Find-By { $_.Target -match 'Items\.' }


・・・(略)・・・
Target              : System.Windows.Controls.ItemsControl Items.Count:3
Parent              : [005]  (Grid) 28
・・・(略)・・・

・・・(略)・・・
Target              : System.Windows.Controls.ComboBox Items.Count:3
Parent              : [005]  (Grid) 28
・・・(略)・・・

 
対象を ToString() した結果に 'Items\.' の正規表現にマッチする文字列が含まれている要素が列挙できました。お次は、Find-ByName:
PS C:\> help Find-ByName

名前
    Find-ByName

概要
    Recursively finds an element contained in the visual tree matched by name.


構文
    Find-ByName [-name] <String> [-select] [<CommonParameters>]

 
これは Visual Tree の各要素を、その Name プロパティ値で検索するバージョンです。実行するとこんな感じに。
snoop:> Find-ByName 'ComboBox1'


・・・(略)・・・
Target              : System.Windows.Controls.ComboBox Items.Count:3
Parent              : [005]  (Grid) 28
・・・(略)・・・

 
Find-ByType はこれの型名(GetType().Name でのマッチング)バージョンですね。Find-ByName と似たり寄ったりなので、使い方は割愛です (^_^;)
PS C:\> help Find-ByType

名前
    Find-ByType

概要
    Recursively finds an element contained in the visual tree matched by name.


構文
    Find-ByType [-type] <String> [-select] [<CommonParameters>]

 
最後の Get-SelectedDataContext は、その名の通り、選択された要素の DataContext を取得するものです。
PS C:\> help Get-SelectedDataContext

名前
    Get-SelectedDataContext

概要
    Gets the currently selected tree item's data context.


構文
    Get-SelectedDataContext [<CommonParameters>]

 
サンプルアプリの MainWindow に対しての実行結果はこんな感じに。
snoop:> cd snoop:\MainWindow

snoop:> Get-SelectedDataContext
42

2013年12月18日 0:00:00
10000

 






Mission5: プロファイル
ここまで来ると、自作したユーティリティや、いくつかの問題に当たる内に定型となった処理も、Snoop 起動時にいっしょに使えるようにしておきたい!となるのが人の性というものです。
Snoop にはそういう要望に応える形で、プロファイルの読み込み機能が用意されています。最後はこの機能を使ってみましょう。
なお、プロファイルの読み込みの優先順序は以下の通りとなっています:
1. %USERPROFILE% に置かれた SnoopProfile.ps1 ファイル
2. [My Documents] にある WindowsPowerShell ディレクトリに置かれた SnoopProfile.ps1 ファイル
3. mission4 で説明した Snoop.psm1 と同じ場所にある SnoopProfile.ps1 ファイル

さて、サンプルアプリが持っている問題に対処すべく、以下の関数を定義してみました:
function Get-SnoopRoutedEventHandlers {
    param (
        [System.Windows.UIElement]
        $element, 
        
        [System.Windows.RoutedEvent]
        $routedEvent
    )
    
    $eventHandlersStoreProperty = [System.Windows.UIElement].GetProperty("EventHandlersStore", ([System.Reflection.BindingFlags]'Instance, NonPublic'))
    $eventHandlersStore = $eventHandlersStoreProperty.GetValue($element, $null)
    $getRoutedEventHandlers = $eventHandlersStore.GetType().GetMethod("GetRoutedEventHandlers", ([System.Reflection.BindingFlags]'Instance, Public, NonPublic'))
    $routedEventHandlers = $getRoutedEventHandlers.Invoke($eventHandlersStore, $routedEvent)
    $routedEventHandlers
}



function Clear-SnoopRoutedEventHandlers {
    param (
        [System.Windows.UIElement]
        $element, 
        
        [System.Windows.RoutedEvent]
        $routedEvent
    )
    
    $routedEventHandlers = Get-SnoopRoutedEventHandlers $element $routedEvent
    foreach ($routedEventHandler in $routedEventHandlers) {
        $handler = $routedEventHandler.Handler
        $element.RemoveHandler($routedEvent, $handler)
    }
}

 
プロファイルを配置すると、PowerShell で表示されるメッセージがそれを読み込んだ旨のものに変わるようになります(ちなみに、再読み込みは F5 キー押下で可能です)。


Get-Command で確認すると・・・大丈夫そうですね!
snoop:> gcm *snoop*

CommandType     Name                                               ModuleName
-----------     ----                                               ----------
Function        Clear-SnoopRoutedEventHandlers
Function        Get-SnoopRoutedEventHandlers

 
さて、サンプルアプリですが、ComboBox に項目の Binding ができるようになったのは良いものの、選択項目変更時のイベントハンドラが未実装だったことに気づきました。


1 つや 2 つならいったんアプリを落として、修正し、ビルドし直して再度実行すればよいのですが、他にも実行中に Snoop で値を変えたりしていて、いい感じにやり方や挙動がわかってきてあと一歩、というところだったりすると、こういう手間はモチベーションが下がったりするもの。何とかこのままちょっと動きが変えられないかなあ・・・。

そんな場面で、先ほどの関数にある Clear-SnoopRoutedEventHandlers を実行すると、邪魔なイベントハンドラを全て削除することができる、というものです。
さっそく実行してみます・・・
snoop:> (Find-ByName ComboBox1).IsSelected = 1

snoop:> Clear-SnoopRoutedEventHandlers $selected.Target ([System.Windows.Controls.ComboBox]::SelectionChangedEvent)

 
先ほどの Snoop モジュールも活用し、問題の ComboBox へ一発で辿り着いた後、イベントハンドラを全て削除してしまいます。そして、選択項目を変更すると・・・問題が発生しなくなりました!!!


ちなみに、この PowerShell 組み込みをされた作者さんの Blogには、実行中に ICommand 実装クラスを差し替えるなどして、動作を変えてしまうというサンプルが掲載されていたりします。
XAML といえども、通常は静的言語のビルドを通して初めて動きが変わるものですので、こういう動的言語な手法を見ると胸が高鳴りますね!!





終わりの終わり
駆け足になってしまいましたが、XAML Advent Calendar 2013 18 日目として、WPF 開発者向けツールとして有名な Snoop を取り上げてみました!
この記事が少しでも皆さんの XAMLer ライフに貢献できることをお祈りしております!それでは、Happy Merry XAML'mas!!!!!