読者です 読者をやめる 読者になる 読者になる

ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

Friendly.WPFStandardControls.1.3.0をリリースしました。

Friendly(Win32, WinForms, WPF)

WPF系の機能追加です。

x:Name以外でのコントロールの特定

x:Nameを付けるのは、なんら恥じることはありません。テスタビリティーを向上させるという大義名分があります。付けても使わないようにしたらいいだけです。だいたい、x:Name付けなくてもコードビハインドからコントロール取得する手段ありますしね!

でも・・・。

XAML系のガチな人はx:Nameを付けることを良しとしませんw。(ごもっとも)
でも大丈夫です!前述しましたが、x:Name付けなくてもコントロールを取得する手段はあります。で、Friendlyは通常のプログラムでできることなら何でもプロセス越しにできるのです。だからx:Nameにこだわらなくても、WPFの知識のある人ならいくらでも特定できるのですね。

とは言え、VisualTreeやLogicalTreeを走査するのは面倒なので、そのユーティリティーを追加しました。

サンプルです。あ、DataTemplateの例は、かずき (id:okazuki)さんのブログからパクりました。
f:id:ishikawa-tatsuya:20150505180208p:plain

<Window x:Class="Target.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="personViewTemplate">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="{Binding Name}"/>
                    <TextBlock Text="さん " />
                    <TextBlock Text="{Binding Age}"/>
                    <TextBlock Text="歳" />
                </StackPanel>
            </DataTemplate>
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBox Text="{Binding Memo}" Grid.Row="0"/>
        <TextBlock Text="{Binding Memo}" Grid.Row="1"/>
        <Button Content="OK" Command="{Binding CommandOK}" Grid.Row="2"/>
        <ListBox ItemsSource="{Binding Persons}" ItemTemplate="{StaticResource personViewTemplate}" Grid.Row="3"/>
    </Grid>
</Window>

コントロール特定

[TestMethod]
public void コントロール特定サンプル()
{
    AppVar main = _app.Type<Application>().Current.MainWindow;
    var logicalTree = main.LogicalTree();

    var textBox = new WPFTextBox(logicalTree.ByBinding("Memo").ByType<TextBox>().Single());
    var textBlock = new WPFTextBlock(logicalTree.ByBinding("Memo").ByType<TextBlock>().Single());
    var button = new WPFButtonBase(logicalTree.ByBinding("CommandOK").Single());
    var listBox = new WPFListBox(logicalTree.ByBinding("Persons").Single());
}

まずは、画面のロジカルツリーをフラットなコレクションにします。

var logicalTree = main.LogicalTree();

LogicalTree()はAppVarとIAppVarOwnerの拡張メソッドにしています。
戻り値はIWPFDependencyObjectCollection<DependencyObject>です。
ちょっと変な型ですか?
まあvarって書いてください。
ちなみにVisualTree()もあります。

var visualTree = main.VisualTree();

それから、逆方向に手繰ることもできます。

var logicalTree = button.LogicalTree(TreeRunDirection.Ancestors);

次は、Bindingのパスから検索します。純粋に文字列で検索してきます。これも戻り値はIWPFDependencyObjectCollection<DependencyObject>です。
上のXamlを見てもらうと分かるように"Memo"というパスのBindingは2個あります。なので、ここで取得されるコレクションの数は2です。

logicalTree.ByBinding("Memo")

そして、そのコレクションの中からTextBoxで絞り込みます。

.ByType<TextBox>()

そうすると1つに特定できているはずなのでSingleを呼びます。

.Single()

Bindingからの検索時は、どのデータに結びついているのかを指定することができます。

var  button = logicalTree.ByBinding("CommandOK", new ExplicitAppVar(main.Dynamic().DataContext)).Single();

それから、実はGeneric型のByTypeで取得した場合は戻り値のコレクションにその型が反映されます。

IWPFDependencyObjectCollection<TextBox> texs =  collection.ByType<TextBox>();

まだないのですが、将来的には、その型に特化した検索の拡張メソッドとか提供するかもしれないですね。
もちろん、それは誰でも作れるので、それぞれのプロジェクトでも作ってみてください。

ちょっと言い訳

IWPFDependencyObjectCollectionっていう変わったインターフェイスが出てきています。(正確にはCollectionではないのですが、相手プロセス内部のCollectionを操作するので、このサフィックスがついています。)

public interface IWPFDependencyObjectCollection<out T> where T : DependencyObject
{
    int Count { get; }
    AppVar this[int index] { get; }
    AppVar Single();
}

なんで素直にIEnumerableじゃないかと言うと、Friendlyは回す系は非推奨なのです。プロセス越しにループを回すと遅いからですね。だからIEnumerableにして通常のループ操作(Linqも含め)が使いやすくなるのはちょっと問題あるので特殊なのを用意しています。後述しますが相手プロセス内でLogicalTree()とか使う場合はIEnumerableが返ってきますのでご安心を。

ItemsControlの場合

ItemsControlとくくりましたが、これは色々です。これは単純にVisualTreeを手繰ってもアイテムは取得できません。と言うのは非表示のものは取得できないからです。可視状態にしてから取得する必要がありますね。

以下のコントロールにはそれぞれアイテム取得メソッドを追加しました。

  • ListBox
  • ListView
  • DataGrid
  • TreeView

これを使って、アイテムを取得します。そのアイテムはIAppVerOwnerを実装していますので、それからVisualTreeを手繰れば目的の要素を取得できます。

[TestMethod]
public void ListBoxとか()
{
    AppVar main = _app.Type<Application>().Current.MainWindow;
    var logicalTree = main.LogicalTree();
    var listBox = new WPFListBox(logicalTree.ByBinding("Persons").Single());

    //アイテム取得
    //可視状態にしている
    var item = listBox.GetItem(20);

    //NameにバインドされたTextBlockを取得する
    //ListBoxのアイテムはLogicalTree上には現れない
    var textBlock = new WPFTextBlock(item.VisualTree().ByBinding("Name").Single());
    Assert.AreEqual("U", textBlock.Text);
}

これでも取得できない場合は

それは、まあそれぞれで工夫してもらうと言うことで・・・
DLLインジェクションを使って頑張ってみてください。
相手プロセスの内部で実行すれば、もう普通のプログラムですから。
でも、これも少しだけサポートしました。

DependencyObjectに対してLogicalTree()、VisualTree()
IEnumerableに対してByType()、ByBinding()
の拡張メソッドを用意しました。

もちろん相手プロセスでこれらを使う場合は、先にWPFStandardControlsもインジェクションする必要があります。

[TestMethod]
public void 内部からの取得もサポート()
{
    //内部で処理をするための準備
    WPFStandardControls_3_5.Injection(_app);
    WindowsAppExpander.LoadAssembly(_app, GetType().Assembly);

    //相手プロセスで取得ロジック実行
    var layout = _app.Type(GetType()).GetLayout(_app.Type<Application>().Current.MainWindow);

    //戻り値に格納されているのでそれを使う
    var textBox = new WPFTextBox(layout.TextBox);
    var textBlock = new WPFTextBlock(layout.TextBlock);
    var button = new WPFButtonBase(layout.Button);
    var listBox = new WPFListBox(layout.ListBox);
}

class Layout
{
    public TextBox TextBox { get; set; }
    public TextBlock TextBlock { get; set; }
    public Button Button { get; set; }
    public ListBox ListBox { get; set; }
}

static Layout GetLayout(Window main)
{
    //通常のプログラムなんで、普通のLinqとかも使えるし、ご自由にどうぞ
    var logicalTree = main.LogicalTree();
    return new Layout()
    {
        TextBox = (TextBox)logicalTree.ByBinding("Memo").ByType<TextBox>().Single(),
        TextBlock = (TextBlock)logicalTree.ByBinding("Memo").ByType<TextBlock>().Single(),
        Button = (Button)logicalTree.ByType<Button>().Single(),
        ListBox = (ListBox)logicalTree.ByBinding("Persons").Single()
    };
}

サンプルコードを動かした方が分かりやすいですね

はい、こちらに置いておきました。ダウンロードしてテスト実行してもらえば、感覚的にわかると思います。
Ishikawa-Tatsuya/WPFSearch-Sample · GitHub