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!!!!!

0 件のコメント:

コメントを投稿