WPF Prism extra: 2 ~ TreeViewItem を MVVM パターンで選択する ~

前回記事「いつだって Prism の画面遷移は RequestNavigation だった。【episode: 6.5 WPF Prism】」

 

本編の流れからは少し外れる小ネタを紹介する extra シリーズの 2 回目は TreeView の TreeViewItem を MVVM パターンで選択する方法を紹介します。
このエントリは元々 episode: 7 に書いていた内容を少し修正しただけなので、Prism 入門本編の episode シリーズと同じサンプルを使用します。

尚、この記事は Visual Studio 2017 Community Edition で .NET Framework 4.7.2 以上 と C# + Prism 7.1 + ReactiveProperty を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。

MVVM パターンで TreeView の TreeViewItem を選択する

Prism 入門本編で紹介しているサンプルアプリの TreeView は現状 fig. 1 のように選択項目無しの状態で起動しています。

fig.1 MainWindow 起動直後

TreeView の項目をマウスクリック等で選択すれば画面左側に編集 View が表示されますが、手動で選択しないと画面左側に何も表示されないのはアプリの動作としては有り得ないので、画面起動時に TreeView のルート(新しい生徒)を選択して生徒情報編集 View も表示されるように修正します。

WPF の TreeViewItem.IsSelected プロパティで項目を選択する

Windows Form 場合、コードから TreeView の項目を選択するには TreeView.SelectedNode プロパティを設定することが多いと思いますが、WPF の TreeView.SelectedItem は読み取り専用プロパティなので値は設定できません。

そのため検索すると『TreeView を継承して SelectedItem をバインド可能にする』とか『Behavior を作る』等の方法が上位に出てきますが、episode: 5 で紹介したように TreeView の各項目ごとに VM とバインドする構成で作成していれば SelectedItem をバインドしなくても VM から設定できます
と言うより TreeView や ListView 等の List 系コントロールを MVVM パターンでバインドする場合は、各項目ごとに VM をバインドするのが基本です。

TreeView の SelectedItem をバインドするのではなく、src. 1 のように TreeViewItem.IsSelected をバインドするだけで、SelectedItemChanged イベントも発生しますし TreeViewItem も選択状態になります。

XAML 側は extra: 1 で IsExpanded をバインドした時と同様に IsSelected を Setter Property で追加して、IsExpanded と同じく『Mode=TwoWay』を指定することで View の状態を VM 側でも取得できるようにしています。(双方向バインディング)

これで VM から IsSelected に値を設定できる準備はできたので、後は画面起動時(Loaded イベント)に IsSelected を設定する処理を追加します。



UserControl.Loaded イベントを ReactiveCommand とバインドする

サンプルアプリ起動時に TreeViewItem を選択するために UserControl.Loaded イベントをハンドルする EventTrigger を src. 2 のように追加します

Loaded イベントは Command として定義されていない為、episode: 6 で紹介した EventToReactiveCommand を使用して VM とバインドします

episode: 6 では TreeView のイベントをハンドルしましたが、今回は UserControl のイベントをハンドルするので Interaction.Triggers を UserControl に追加している程度の違いしかありません。
詳しく知りたい場合や、Prism の DelegateCommand とバインドしたい場合は episode: 6 を参考にしてください。

UserControl.Loaded イベントと ReactiveCommand をバインドするには VM を src. 3 のように実装します。

episode: 6 と違い、今回はイベントパラメータの値を使用しないので ReactiveCommand を型パラメータ無しで宣言しています。(16 行目)

42 ~ 44 行目は ReactiveCommand の初期化部ですが、38 行目の SelectedItemChanged イベントと異なり Delegate 先を指定するのではなくラムダ式で処理を直接記述しています。

イベントで実行するのは一文だけですが、予め 31 行目の TreeViewItemCreator.Create メソッドの戻り値であるルートノードをフィールド(rootNode)へ退避しておくことで、rootNode の IsSelected.Value に true をセットするだけでルートノードが選択状態になり、38 行目の SelectedItemChanged イベントも併せて実行され、生徒情報編集 Viewも表示されるようになります。
このようなイベントの伝播は Windows Form と同じですね。

今回選択するのはルートノードですが、Linq 等で TreeNodes プロパティ(12 行目)から取得した TreeViewItemViewModel を操作すれば任意の Item に対してノードの選択や子項目の展開も可能です

src. 4 は TreeView の各項目とバインドしている VM で、src. 4 のように ReactivePropertySlim で宣言している IsSelected に値を設定する時は、src. 3 の 44 行目のように【.Value】を指定する必要があります。
(これは指定しないとコンパイルエラーになるので忘れることはあまり無いと思います)

ここまでで実行すると fig. 2 のようにルートノードが選択され MainWindow 右側に編集 View も表示された状態で起動するようになります。
(今回から MainWindow のサイズを 800×600 に変更しました)

fig.2 起動直後のサンプルアプリ MainWindow

WPF の TreeView は Windows Form の TreeView と異なり MultiSelect プロパティは定義されていないため、常に単一の項目しか選択できません
そのため、選択された TreeViewItem 以外の IsSelected プロパティは全て false に設定されます

試しに SelectedItemChanged イベントへブレークポイントを張って TreeView の選択項目を変更すると、VM の TreeNodes プロパティの中身は、選択された TreeViewItem の IsSelected プロパティのみ true に設定され、それ以外は全て false に設定されるのが確認できます。

起動直後のフォーカス設定

アプリ起動時に TreeView のルートノードが選択され、MainWindow 左側に編集 View が表示されるようになりましたが、キャプチャを見て分かる通り起動直後はフォーカスがどこにも設定されていないので TreeView にフォーカスをセットするよう修正します。

FocusManager で Load 直後のフォーカスを設定

Windows Form の場合であれば、TabIndex を振るか、Form.Load イベントの最後で、
this.ActiveControl = hogeControl;
等とすればアプリ起動時に任意のコントロールへフォーカスをセットする事が出来ましたが、WPF で初期フォーカスをセットするには『FocusManager 添付プロパティ』を XAML に設定するのがセオリーのようです。

アプリ起動時のフォーカスを TreeView にセットするために NavigationTree.xaml を src. 5 のように変更します。

MVVM パターンで開発する場合、コントロールに名前を付ける必要はありませんが、FocusManager でフォーカスを設定するにはコントロールの名前が必要なので、フォーカスを設定するコントロールへ『x:Name=”hoge”』を追加します。(ここでは TreeView に「mainTree」を設定)

他のブログでは「Window クラスへ FocusManager プロパティを添付すればおk!」と書いてある所がほとんどでしたが、サンプルアプリの場合はフォーカスがセットされませんでした。

Prism で UserControl を動的にロードしているのが原因なのか、Grid をネストさせているのが原因なのかはよく分かりませんが、src. 5 のように TreeView を配置している Grid に FocusManager を添付すると fig. 3 のように、TreeView へフォーカスがセットされるようになりました

fig.3 起動時にフォーカスをセットした MainWindow

Prism で View を動的に表示した後の Tab キーでのフォーカス移動

又、起動後に Tab キーでフォーカスを移動するとフォーカスが消失する箇所があることに気付くかもしれませんが、これは MainWindow に配置している ItemControl がフォーカスを受け取るのが原因なので、src. 6 のように ItemControl.IsTabStop = “false” を設定するとフォーカスが消失しなくなります

フォーカスのハンドリングについては今の所、最適な方法を決めかねていることと、フォーカス制御のネタは当面の本題からは外れてしまうので今回は初期フォーカスを設定する方法だけ紹介します。



TreeViewItem レイアウトの微調整

これでアプリ起動時のフォーカスは TreeView へセットされ、ルートノードが初期選択項目となり、生徒情報編集 View も表示されるようになりました。
ただ、管理人個人的には TreeView の選択枠等が窮屈そうに見えたので src. 7 のように TreeViewItem のレイアウトを微調整しています。

scr. 7 のようにレイアウトを調整すると分かりにくいかもしれませんが、fig. 4 → fig. 5 のように変わります。

fig.4 レイアウト調整前

fig.5 レイアウト調整後

特に説明することはありませんが、上記のようにパディングやマージンを設定するだけで TreeViewItem の上下間隔や画像との距離等が微調整出来るのは XAML の利点だと思います。
見た目は個人の好みなので、調整したい人は参考程度に。

ここで紹介したソースコードも他の Prism 入門の episode と同じく GitHub リポジトリ に上げていますが、このエントリの内容は元々 episode: 7 の一部だったため episode: 7 のソリューション に含まれています。

 

次回も Prism 入門本編 episode シリーズではなく extra シリーズで TreeViewItem へコンテキストメニューを追加する方法を紹介します。

 

次回記事「とある TreeView の状況一覧 (Context menu)【extra: 3 WPF Prism】」

 

 

おすすめ

3件のフィードバック

  1. 匿名 より:

    あれ、消されてしまった。
    ×:Style TargetType=”TreeViewItemViewModel”
    〇:Style TargetType=”TreeViewItem”
    じゃないでしょうか。

    • 沖田玲朗 より:

      返信遅くなって申し訳ありません。
      リポジトリは【TreeViewItem】になっていましたが、エントリ側を修正していなかったようなので修正しました!
      ご指摘ありがとうございました。

  2. 通りすがり より:

    1: NavigationTree.xaml の

    じゃないでしょうか。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください