WPF 入門 第 4 回 WPF の リソース スタイル テンプレート を習得しよう 前回説明した依存関係プロパティは ほかの要素の値に依存してプロパティの値を決定する機構 といえる WPF ではこの仕組みを基軸として リソース スタイル コントロール テンプレートなどの高度な機能を提供している 特に WPF の柔軟性を象徴する機能がコントロール テンプレートで この機能を用いることでコントロールの外観を自由自在にカスタマイズ可能となる Windows フォームなどの既存の GUI 作成フレームワークでは コントロールに対して背景色やフォント サイズの変更など 限定的なカスタマイズしかできなかった これに対して WPF のコントロール テンプレートは 外観をほぼ無制限にカスタマイズできるポテンシャルを秘めている 今回は これらの WPF の高度な機能について説明していく リソース WPF では 複数の UI 要素で 1 つのオブジェクトを共有するために リソースという仕組みを持っている 例として 背景色を指定するためのブラシ ( 具体的には <SolidColorBrush> 要素 ) を 2 つの <Button> 要素で共有する XAML コードを List 1 に示す この XAML コードの表示結果は Figure 1 のようになる <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="100" Width="200"> <Window.Resources> <SolidColorBrush x:key="mybrush" Color="Blue"/> </Window.Resources> <StackPanel> <Button Content="Button 1" Background="StaticResource MyBrush"/> <Button Content="Button 2" Background="StaticResource MyBrush"/> </StackPanel> </Window> List 1: リソースによるブラシの共有 -1-
Figure 1: List 1 の XAML コードの表示結果 このように リソースを用いてオブジェクトを共有することで インスタンス生成などのオーバーヘッドを削減できる 上述の例では 青色のブラシを 1 つだけ生成して共有しており 2 つの <Button> 要素でそれぞれ個別にブラシを作るよりも生成されるインスタンスが少ない また リソースは UI 要素に対する設定を一カ所に集めることで 保守を容易にする 上述の例でいうと リソース中で定義されたブラシを変更するだけで これを参照する 2 つの <Button> 要素の背景色を一斉に変更できる 2 種類の リソース リソース という言葉が混乱を招く場合もあるので ここで補足的な説明を入れておく Windows アプリケーションでは 実行可能ファイル (=.EXE ファイル ) などのアセンブリの中に画像などのバイナリ ファイルを埋め込むための リソース 機構を持っている この仕組みは WPF アプリケーションでももちろん利用できる この機構と 本稿で説明する WPF のリソース機構を区別する際には 以下のように呼び変える アセンブリ リソース : アセンブリの中にバイナリ ファイルを埋め込むためのリソース機構 バイナリ リソースなどとも呼ぶ オブジェクト リソース : 本稿で説明する.NET オブジェクトを複数の UI 要素から参照するためのリソース機構 本稿では 単に リソース と呼ぶ場合には こちらを指すものとする リソース定義 List 1 でも示したように リソースは <Window> などの要素 ( 正確には FrameworkElement 型を継承する要素 ) の Resources プロパティ内に定義する Resources プロパティは ResourceDictionary 型 (System.Windows 名前空間 ) で この型はキーも値も object 型の辞書となっている 連載第 2 回で説明したように XAML コード中で IDictionary インターフェイスを実装する要素に子要素を追加する際には x:key XML 属性の指定が必須となる ただし 後述する Style クラス ( System.Windows 名前空間 ) のように ( クラスに対して ) DictionaryKeyProperty 属性 (DictionaryKeyPropertyAttribute) を付与して (x:key XML 属性の ) 代替となるプロパティ (Style クラスの場合は TargetType プロパティ ) を設定しているクラスの場合には x:key XML 属性の指定を省略可能である 例えば List 2 で示している Style クラスでは x:key XML 属性の代替として TargetType プロパティを利用している (<Style> 要素の TargetType プロパティについては 後述のスタイルの節で詳しく説明する ) <Window x:class=" WpfApplication1.MainWindow" -2-
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Window.Resources> <!-- x:key の指定が必須 --> <SolidColorBrush x:key="brush1" Color="Green" /> <!-- TargetType が x:key の代わりに利用される --> <Style TargetType="Button"> <Setter Property="Background" Value="LightBlue" /> </Style> </Window.Resources> <Grid> <Button Content=" ボタン "/> </Grid> </Window> List 2: リソースの定義例 外部リソースの取り込み リソースは <Windows.Resources> タグの中に直接定義する以外に ルート要素が ResourceDictionary 型の XAML ファイルを別途用意して この XAML ファイルを取り込む形で利用することもできる ( 外部リソース ) 外部リソースの定義例を List 3 に示す ここでは この XAML ファイルに Styles.xaml という名前を付けて保存するものとする <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Style TargetType="Button"> <Setter Property="Background" Value="LightBlue" /> </Style> </ResourceDictionary> List 3: 外部リソースの定義例 (Styles.xaml) 利用側が外部リソースを取り込むには List 4 に示すように <ResourceDictionary> 要素に Source 属性を付けることで行う <Window x:class=" WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> -3-
<Window.Resources> <ResourceDictionary Source="Styles.xaml"/> </Window.Resources> <Grid> <Button Content=" ボタン "/> </Grid> </Window> List 4: 外部リソースの取り込み例 また ResourceDictionary.MergedDictionaries プロパティを用いることで 複数のリソース ディクショナリを 1 つに結合することもできる 例えば Styles.xaml に加えてもう 1 つ Brushes.xaml という名前の外部リソースを用意したものとして これらの外部リソース両方を取り込むためには List 5 に示すような XAML コードを記述する この例では 外部リソースに加えて さらにローカルでリソース ディクショナリを結合している <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="Styles.xaml"/> <ResourceDictionary Source="Brushes.xaml"/> <ResourceDictionary> <SolidColorBrush x:key="localresource" Color="Red" /> </ResourceDictionary> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> List 5: 複数のリソース ディクショナリの結合 ちなみに 通常 リソースはインスタンスが 1 つだけ生成され そのただ 1 つのインスタンスが複数の UI 要素から参照されることになる この挙動を変更したい場合 List 6 に示すように x:shared XML 属性を false に設定することで リソースが参照されるたびに別のインスタンスが生成されるようにすることも可能である <Grid.Resources> <SolidColorBrush x:shared="false" x:key="brush1" Color="Blue" /> </Grid.Resources> <Button Content=" ボタン 1" Background="StaticResource Brush1" /> <Button Content=" ボタン 2" Background="StaticResource Brush1" /> List 6: リソースのインスタンスを毎度生成する例 リソース利用 リソースを参照するためには StaticResource マークアップ拡張もしくは DynamicResource マークアップ拡張を利用する 実行時に値を変化させる必要のないものには StaticeResource マークアップ拡張 -4-
を用いる 一方 DynamicResource は 実行時に値が変化し その変化を参照している要素に反映させる必要がある場合に用いる リソースの検索は 親要素を下から上に階層的にたどって行われる List 7 に示すように 同名のキーを持つリソース ディクショナリがあった場合 Figure 2 に示すように 内側のスコープ ( 階層的に下 ) にあるリソースが優先的に利用される <Window x:class=" WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <Window.Resources> <SolidColorBrush x:key="brush1" Color="Red" /> <SolidColorBrush x:key="brush2" Color="Red" /> </Window.Resources> <StackPanel> <StackPanel.Resources> <!-- 内側のリソースを優先的に利用するため ボタン 2 にはこちらのブラシが反映される --> <SolidColorBrush x:key="brush2" Color="Blue" /> </StackPanel.Resources> <Button Content=" ボタン 1" Background="StaticResource Brush1"/> <Button Content=" ボタン 2" Background="StaticResource Brush2"/> </StackPanel> </Window> List 7: リソースの利用例 ( リソース検索の優先度 ) -5-
Figure 2: リソース参照の仕組み システム リソース XAML コードの中で定義したリソースに加えて 個人設定 などで設定されたシステム色やフォントなどの システム リソースも利用できる システム リソースを利用するには以下のクラス ( いずれも System.Windows 名前空間 ) を使用する SystemColors: システム色やブラシを取得する SystemFonts: システム フォントを取得する SystemParameters: ウィンドウの境界線やキャプションの幅などのシステム設定を取得する システム リソースは SystemColors.DesktopBrush などの静的プロパティを通して直接取得することもできるが これを StaticResource マークアップ拡張などでリソース参照するためのキーとして SystemColors.DesktopBrushKey などの静的プロパティが用意されている 例えば List 8 に示すような XAML コードでデスクトップの背景色を利用できる <Window x:class="wpfapplication1.mainwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Width="200" Height="200"> <Grid Background= "DynamicResource x:static SystemColors.DesktopBrushKey"> </Grid> </Window> List 8: システム リソースの利用例 ( デスクトップの背景色の取得 ) この例では DynamicResource マークアップ拡張を利用しているが こうすることで アプリケーション起動中にエンド ユーザーがデスクトップの背景色を変更した際に その変更がアプリケーションに即座に反映される 例えば List 8 の表示結果は Figure 3 のようになる この例では デスクトップの背景色を灰色からピンクに変更している -6-
Figure 3: List 8 の表示結果 ( システム設定の変更を即座に反映 ) スタイル WPF は HTML でいうところの CSS のようなスタイル設定の機構を持っている CSS 同様 WPF でもスタイルを用いることで UI 要素の外観をカスタマイズ可能である スタイルの定義 WPF のスタイルは List 9 に示すように <Setter> 要素 (= プロパティの値を設定するための要素 ) のリストとして定義する <Style> 要素の TargetType プロパティにはスタイルを適用したい型の名前を指定する また <Setter> 要素の Property プロパティおよび Value プロパティに それぞれ対象とするプロパティ名と値を指定する <Style TargetType="Button"> <Setter Property="Background" Value="DarkSeaGreen" /> <Setter Property="Foreground" Value="LightPink" /> </Style> List 9: スタイルの定義例 これまでの XAML エディタでは <Setter> 要素に対する IntelliSense が効かず プロパティ名を覚えていなければスタイル設定ができないという もどかしい状態が続いていた しかし 最新の Silverlight 4 Tools for Visual Studio 2010 をインストールすることで WPF の XAML エディタも同時に更新され Figure 4 に示すように <Style> 要素に対する IntelliSense が有効になる (Visual Studio 2010 から WPF と Silverlight の XAML エディタが共通化された Silverlight Tools の更新で WPF も同時に更新されるのはこのためである ) -7-
Figure 4: <Style> 要素に対する IntelliSense スタイルの適用 FrameworkElement クラス (System.Windows 名前空間 ) には Style プロパティがあり この Style プロパティに値を設定することで 定義したスタイルを適用できる スタイルは FrameworkElement クラスの Style プロパティに対して直接記述することもできるが 通常は 本稿の前半で解説したリソースの中で定義して利用する スタイルとリソースは非常に相性がよく リソースの中でスタイルを定義することで 複数の UI 要素にスタイルを一斉適用可能である 特に リソース中に x:key XML 属性を指定しないスタイルを定義することで TargetType プロパティで指定した型の要素すべてに自動的にスタイルが適用される この 3 種類のスタイル利用 ( 自動適用 明示的なリソース指定 直接記述 ) の例を List 10 に また その表示結果を Figure 5 に示す <StackPanel> <StackPanel.Resources> <!-- x:key なしのスタイルを定義することで TargetType で指定した型すべてにスタイルを適用する --> <Style TargetType="Button"> <Setter Property="Background" Value="LightBlue" /> <Setter Property="Foreground" Value="Red" /> </Style> <!-- x:key の明示 --> <Style x:key="mybuttonstyle" TargetType="Button"> <Setter Property="Background" Value="DarkSeaGreen" /> <Setter Property="Foreground" Value="LightPink" /> </Style> </StackPanel.Resources> <!-- スタイルの自動適用 --> <Button Content=" ボタン 1" /> <!-- x:key を指定して明示的にスタイルを適用 --> <Button Style="StaticResource MyButtonStyle" Content=" ボタン 2" /> <!-- スタイルを直接記述 --> <Button Content=" ボタン 3"> <Button.Style> <Style TargetType="Button"> <Setter Property="Background" Value="Gray" /> </Style> </Button.Style> -8-
</Button> </StackPanel> List 10: スタイルの利用例 Figure 5: List 10 の表示結果 スタイルの継承 <Style> 要素は BasedOn プロパティを指定することで ほかのスタイルを継承できる この仕組みにより スタイルを部分的に書き換えたり <Setter> 要素を追加したりといったことが可能だ List 11 にスタイルの継承例を また Figure 6 にその表示結果を示す この例では 背景色および前景色を指定するスタイルを継承し さらにフォント サイズを指定するスタイルを定義している <StackPanel Width="90"> <StackPanel.Resources> <Style TargetType="Button"> <Setter Property="Background" Value="LightBlue" /> <Setter Property="Foreground" Value="Red" /> </Style> <!-- 自動適用版のスタイルを基にして 新たにスタイルを作成 --> <Style x:key="mybuttonstyle" TargetType="Button" BasedOn="StaticResource x:type Button"> <Setter Property="FontSize" Value="20" /> </Style> </StackPanel.Resources> <Button Content=" ボタン 1" /> <Button Style="StaticResource MyButtonStyle" Content=" ボタン 2" /> </StackPanel> List 11: スタイルの継承の利用例 Figure 6: List 11 の表示結果 トリガー スタイルの作成では 特定の条件下でのみ働くスタイルを定義したいという場面がしばしば出てくる -9-
例えば マウス カーソルが上に乗っているときや クリックされたときだけスタイルを変えたい というような要求は必ずといっていいほど出るだろう このような要求に応えるため WPF のスタイルではトリガーという仕組みを持っている 具体的には <Style> 要素の Triggers プロパティを設定することで 特定の条件下でのみ働くスタイルを定義することができる 例えば List 12 のような XAML コードにより マウス カーソルが上に乗ったときだけ水色に フォーカスを得ているときだけピンク色に変化するテキストボックスが得られる <StackPanel Width="60"> <StackPanel.Resources> <Style TargetType="TextBox"> <Setter Property="Background" Value="LightGray" /> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Background" Value="LightBlue" /> </Trigger> <Trigger Property="IsFocused" Value="True"> <Setter Property="Background" Value="LightPink" /> </Trigger> </Style.Triggers> </Style> </StackPanel.Resources> <TextBox Text=" テキスト " /> </StackPanel> List 12: トリガーを用いた条件付きのスタイル適用例 <Style.Triggers> 要素中に記述できるのは Trigger クラスだけではなく 以下のようなものがある ( いずれも System.Windows 名前空間中のクラス ) Trigger: 特定のプロパティの値の変化をトリガーとして Setter を用いてプロパティ値を変更する MultiTrigger: Trigger を複数条件に対応させたもの 指定したすべての条件が満たされた場合にトリガーがかかる DataTrigger: スタイル適用先の UI 要素だけでなく データ バインディングされたデータを監視する MultiDataTrigger: DataTrigger の複数条件版 EventTrigger: プロパティ値の変化ではなく イベントの発生をトリガーとする また Setter ではなくストーリーボードを使ったアニメーションによりプロパティ値を変化させる データ バインディングやアニメーションに関しては次回以降で説明する EventTrigger に関しても アニメーションの回にあらためて説明を行う また WPF 4 では 状態の変化に応じたスタイルの変更を管理するために 新たに VisualStateManager というクラスが追加された この VisualStateManager クラスは もともとは Silverlight で実装されたものだが.NET Framework 4 で WPF にも輸入されることになった VisualStateManager クラスの利用にはアニメーションの知識が必要なため Trigger クラスを利用す -10-
るより少し難易度は高いが *1 状態の管理が行いやすく 可能ならばこちらを用いた方がいいだろう VisualStateManager クラスに関しても アニメーションの回で説明する予定である *1 Visual Studio の XAML エディタにはアニメーション作成をサポートする機能がなく 複雑なアニメーションの作成が必要な場合には Expression Blend の利用を考えてみるべきであろう コントロール テンプレート スタイルを用いることで コントロールなどの外観をカスタマイズできるが 前節で説明したような単純なプロパティ値の変更だけでは カスタマイズ可能な範囲は極めて限られている 背景色などの変更程度であれば 従来の GUI 作成フレームワークでもある程度可能な範囲である WPF のスタイルには トリガーを用いた 状態の変化に応じたスタイル適用 などの先進的な機能もあるものの まだ驚く範囲ではないだろう Windows フォームなど 従来の GUI 作成フレームワークでは 出来合いのコントロールを用いることで高い生産性を得ていたが その半面 カスタマイズ性は非常に限られていた 結果として 大幅なカスタマイズが必要な場合には 新たなコントロールを 1 から自作する必要があった この場合 外観の新規作成だけでなく 例えば ボタンのクリック や テキストの選択 などの機能面まで含めて作り直す必要があり 非常に負荷の高い作業となる これに対して WPF では コントロール テンプレートという機能を用いることで ボタンのクリック などの機能を残したまま 外観だけを任意に変更可能である コントロール テンプレートの利用 まず 最低限のコントロール テンプレートを見てみよう List 13 に ボタンの外観を水色のだ円形に変えるテンプレートを示す ボタンなどのコントロールの基底となる Control クラス (System.Windows.Controls 名前空間 ) は Template というプロパティを持っていて この Template プロパティに ControlTemplate クラス (System.Windows.Controls 名前空間 ) のインスタンスを設定すればよい <Button> <Button.Template> <ControlTemplate TargetType="Button"> <Ellipse Fill="LightBlue" Width="80" Height="30"/> </ControlTemplate> </Button.Template> </Button> List 13: ボタンの外観を水色のだ円形に変えるテンプレート この程度の例であれば 単純にだ円 (=<Ellipse> 要素 ) に Mouse.MouseDown イベントを追加した方が似たようなことを手早く実現できるかもしれない ただ Button クラスなどのコントロールの場合 次回以降で説明する コマンド という仕組みも持っているため これを利用したければ やはりコントロール テンプレートを使うことになるだろう Button クラスなどのコントロールや Ellipse クラスなどの シェイプ に関する詳細は次回以降で説明する -11-
コントロール テンプレートのリソース化と自動適用 もちろん コントロール テンプレートもリソース化可能である List 14 に コントロール テンプレートをリソース化する例を示す <StackPanel> <StackPanel.Resources> <ControlTemplate x:key="mybuttontemplate" TargetType="Button"> <Ellipse Fill="LightBlue" Width="80" Height="30"/> </ControlTemplate> </StackPanel.Resources> <Button Template="StaticResource MyButtonTemplate" /> </StackPanel> List 14: コントロール テンプレートのリソース化の例 また スタイルと組み合わせることで コントロール テンプレートの自動適用も可能だ List 15 にその例を示す <StackPanel> <StackPanel.Resources> <!-- 自動適用スタイル --> <Style TargetType="Button"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Ellipse Fill="LightBlue" Width="80" Height="30"/> </ControlTemplate> </Setter.Value> </Setter> </Style> </StackPanel.Resources> <!-- 以下のボタンには 自動的にコントロール テンプレートが設定される --> <Button /> </StackPanel> List 15: コントロール テンプレートの自動適用例 ContentPresenter と TemplateBinding それでは もう少し複雑な例を見てみよう 前述の最低限の例では <Button> 要素の Content プロパティを設定してもボタンの中身が表示されなくなってしまうという問題がある また だ円形の色も水色に固定されてしまっている そこで List 16-12-
に示すような修正が必要となる <StackPanel Width="80"> <StackPanel.Resources> <Style TargetType="Button"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Grid> <Ellipse Fill="TemplateBinding Background"/> <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </StackPanel.Resources> <Button Content=" ボタン 1" Background="LightBlue" Height="30" /> <Button Content=" ボタン 2" Background="LightPink" Height="30" /> </StackPanel> List 16: ContentPresenter と TemplateBinding 以下の 2 つがポイントとなる <ContentPresenter> 要素 : この要素が置かれた位置にコントロールの中身 (=Content プロパティに与えた値 ) が配置される TemplateBinding マークアップ拡張 : コントロール テンプレートの適用先のコントロールに与えられたプロパティ値を取得するために利用する 上記のコード例では <ContentPresenter> 要素により <Grid> 要素内の水平 / 垂直方向の中央に <Button> 要素の Content プロパティの値 ( 具体的には ボタン 1 と ボタン 2 ) が表示され さらに TemplateBinding マークアップ拡張によって コントロール テンプレートの適用先である <Button> 要素の Background プロパティの値 ( 具体的には LightBlue と LightPink ) が取得されて用いられる Figure 7 に その表示結果を示す Figure 7: List 16 の表示結果 ルーティング コマンド スクロールバーのように 挙動が少し複雑なコントロールに対してコントロール テンプレートを適用 -13-
する場合 ルーティング コマンド という仕組みを利用することになる ルーティング コマンドの詳細については次回以降で説明することになるが 簡単にいうと ページ アップ や 端までスクロール などといった操作が発生したことを親要素に伝達するための仕組みである ここでは参考程度のコード提示にとどめるが ScrollBar クラスに対してコントロール テンプレートを適用する例を List 17 に示す <Grid Width="150" Height="150"> <Grid.Resources> <ControlTemplate x:key="verticalscrollbartemplate" TargetType="ScrollBar"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="15"/> <RowDefinition Height="*"/> <RowDefinition Height="15"/> </Grid.RowDefinitions> <RepeatButton Command="ScrollBar.LineUpCommand" Background="LightBlue" /> <Track Grid.Row="1" IsDirectionReversed="True"> <Track.DecreaseRepeatButton> <RepeatButton Command="ScrollBar.PageUpCommand" Background="Red" /> </Track.DecreaseRepeatButton> <Track.Thumb> <Thumb Background="Blue" /> </Track.Thumb> <Track.IncreaseRepeatButton> <RepeatButton Command="ScrollBar.PageDownCommand" Background="Green" /> </Track.IncreaseRepeatButton> </Track> <RepeatButton Command="ScrollBar.LineDownCommand" Background="LightPink" Grid.Row="2" /> </Grid> </ControlTemplate> <Style TargetType="ScrollBar"> <Style.Triggers> <Trigger Property="Orientation" Value="Vertical"> <Setter Property="Template" Value="StaticResource VerticalScrollBarTemplate" /> </Trigger> </Style.Triggers> </Style> </Grid.Resources> <ScrollViewer HorizontalScrollBarVisibility="Visible"> -14-
<Ellipse Width="500" Height="500" Fill="Gray" /> </ScrollViewer> </Grid> List 17: ScrollBar へのコントロール テンプレートの適用例 WPF Themes(WPF テーマ ) コントロール テンプレートの仕組みは高機能で 柔軟なカスタマイズが可能ではあるが これをすべてのコントロールに対して 1 から定義するのは非常に大変な作業となる 自作ではなく 出来合いのものをどこかから探してきて利用する方が現実的かもしれない ありがたいことに Codeplex においてフリーの WPF テーマが公開されている コントロール テンプレートの利用の際には参考にしてみるのもいいだろう WPF Themes 次回はデータ バインディングの仕組みや具体的な用途について説明を行っていく 第 5 回 WPF の データ バインディング を理解する 今回および次回の 2 回に渡り ビューとモデルの疎結合を実現するための仕組みとして データ バインディングとコマンドという 2 つの機能について説明する まず今回は これらの機能の背景にある GUI アプリケーションに対する要件と データ バインディングについて説明を行っていく GUI アプリケーションに対する要件 WPF のデータ バインディングやコマンドといった仕組みを説明する前に そもそも GUI アプリケーションに対して どのような要件があるのかを整理してみよう ここでは 実装上で満たすべき要件として ビューとモデルの疎結合 と GUI アプリケーションに求められる機能 ( の中で 今回はデータ バインディングに関係する部分 ) を紹介する ビューとモデルの疎結合 GUI アプリケーション開発においてよくいわれる言葉として ビュー (view) とモデル (model) を疎結合にしろ というものがある すなわち アプリケーションの見た目にかかわる部分 (= ビュー ) と 見た目と関係なく成り立つロジックや表示したいデータ (= モデル ) を分けるのが良いとされる オブジェクト指向言語の場合には ビューとモデルを別クラスとして表現し 互いのクラスに参照関係を作らないことを指す ( そうすることで 再利用性やテスト可能性が向上する ) 単に モデル というと意味が広すぎるが 要は 業務を分析し モデル化して得られたデータ (= ドメイン モデル ) を指す言葉だ アプリケーションの種類によっては モデルという言葉がしっくりこないかもしれないが (GUI であろうと CUI(character-based user interface) であろうと ) 表示や操作の方法にかかわらず変わらない部分を独立させ その部分を指して モデル と呼んでいると思えばいいだろう GUI アプリケーションに求められる機能 -15-
データ バインディングと関連して GUI アプリケーションに求められる機能としては 以下のようなものがある 同じデータを複数のウィンドウやコントロールから参照 通常 モデルの持っているデータをビュー上に表示する方法は 1 通りではない 例えば 同じデータを テーブルに表示したい場合もあれば 折れ線グラフなどで表示したい場合もあるだろう また テーブルとグラフを同時に表示したうえで テーブル上での値の変更が即座にグラフ側に反映されてほしい場合も少なくないだろう Visual Studio を例にとって 1 つ具体例を挙げるなら XAML エディタがまさにこのような挙動になっている Figure 1 に示すように この XAML エディタは マウスを使って視覚的にデザインするための デザイン 画面 ( 画像上部 ) と XAML コードを文字ベースで入力する XAML 画面 ( 画像下部 ) を持っている そして どちらか片方への変更があった場合 もう一方に即座に変更結果が反映される -16-
Figure 1: 同じデータを複数のコントロール上に表示する例 (XAML エディタ ) このような機能を実現するためには データの変更を通知するような仕組みが必要で このために WPF は データ バインディング という仕組みを持っている ( また 次回説明する コマンド も ビューとモデルの疎結合を保つために有用である ) 次節以降では このデータ バインディングについて説明していく データ バインディング WPF では データ ソース (= モデルなどの データの提供元 ) をビュー (=WPF の場合は XAML コード ) 上の UI 要素と簡単に結び付けるために データ バインディングという仕組みを提供している バインディング (binding: 結合 ) や 結び付ける という言い方をしているが これは 値を一度だけ代入するのではなく 1 カ所で値が変化するたびに ほかの個所にもその変化が即座に伝搬される ということを指している 具体的には 以下のような手順でデータ ソースと UI 要素を結び付ける (Figure 2) XAML コード中に このプロパティの値をここに表示する という印だけを入れておく 表示したいデータ ソースは DataContext プロパティを通じてビューに渡す Figure 2: データ バインディングの利用方法 アプリケーションの要件によっては モデルのインスタンスをそのまま DataContext プロパティに渡すだけでよい 例えば モデルが最初から WPF 向けに作られている場合や 後述する INotifyPropertyChanged インターフェイスなどの仕組みを必要としない単純な要件の場合などである ただし実際には このような場合に該当することはあまりなく WPF 向けにモデルをラッピングしたもの (= ビュー モデルと呼ぶ ) を作って これを DataContext プロパティに渡すことになる データ バインディングの使い方 それでは データ バインディングの具体例を見てみよう まず List 1 に示すように XAML コード中に Binding マークアップ拡張を使って このプロパティの値をここに表示する という印を入れてお -17-
く <Window x:class="atmarkit05.minimalsamplewindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="100" SizeToContent="Height" Title=" ウィンドウ名 "> <StackPanel> <TextBlock Text="Binding X" /> <TextBlock Text="Binding Y" /> </StackPanel> </Window> List 1: データ バインディングの利用例 (XAML) <TextBlock> 要素の Text プロパティに対して Binding X と書くことで Text プロパティの値と X プロパティの値を結び付ける という意味になる 次に FrameworkElement クラス (System.Windows 名前空間 )(Window クラスもこのクラスを継承している ) の DataContext プロパティにデータ ソースを渡す 今回は List 2 に示すように 分離コード中のビュー クラス (Window クラスを継承 ) のコンストラクタで DataContext プロパティに匿名クラスを使って渡す ( ただし 実際には データの差し替えやテストのしやすさを考えると ビュー クラスの外でデータ ソースを渡す方が好ましい ) 実行結果は Figure 3 のようになる Visual Basic Namespace atmarkit05 Class MinimalSampleWindow Sub New() InitializeComponent() Me.DataContext = New With.X = 10,.Y = 20 End Sub End Class End Namespace Visual C# using System.Windows; namespace atmarkit05 public partial class MinimalSampleWindow : Window public MinimalSampleWindow() InitializeComponent(); this.datacontext = new X = 10, Y = 20 ; -18-
List 2: データ バインディングの利用例 ( 分離コード ) Figure 3: List 1 の表示結果 このように 単にデータを表示するだけなら何も特殊な実装は必要ない ( 後述するような INotifyPropertyChanged インターフェイスの実装は不要 ) また 同名のプロパティさえ持っていればよく 具体的な型が何かは問われない 従って この例のように匿名クラスであっても構わない ( ただし 匿名クラスが利用できるのはアプリケーションが完全信頼で動作している場合のみ ) ここで DataContext プロパティの値は包含継承 ( 親 UI 要素から値が引き継がれる ) されていて ビュー クラスの DataContext プロパティに渡したデータ ソースは <TextBlock> 要素の DataContext プロパティにも引き継がれている Binding X というマークアップ拡張記述の実体としては その UI 要素の DataContext プロパティに格納されているオブジェクトの X プロパティを参照することになる バインディングのソースとターゲット データ バインディングを行ううえで データの提供元 (= ソース ) となるものと 反映先 (= ターゲット ) となるものができるが 混乱を避けるため Figure 4 に示すように用語を定める バインディング ソース : データの提供元のオブジェクト ソース プロパティ : データの提供元となる バインディング ソースのプロパティ バインディング ターゲット : データの反映先のオブジェクト ターゲット プロパティ : データの反映先となる バインディング ターゲットのプロパティ -19-
Figure 4: データ バインディングのソース / ターゲットに関する用語 まず WPF のデータ バインディングにおいて ターゲット プロパティは依存関係プロパティでなければならない 幸い WPF で XAML コード中に記述する要素のプロパティは 大部分が依存関係プロパティになっていて ターゲット プロパティにすることが可能である ( ただし InputBinding クラス (System.Window.Input 名前空間 ) の Command プロパティのように.NET Framework 3.5 までは依存関係プロパティになっておらず.NET Framework 4 で初めてターゲット プロパティにできるようになったものもある ) 一方で バインディング ソースには以下のようなものが利用可能である CLR オブジェクト : 通常の CLR オブジェクトのプロパティやインデクサをソース プロパティにできる ( アクセス修飾子は public である必要がある また フィールドは利用できない ) プロパティ値の取得や更新はリフレクションを介して行われる あるいは ICustomTypeDescripter インターフェイスを実装する場合や TypeDescriptionProvider クラスを使って型情報を WPF に対して登録している場合 ( いずれのクラスも System.ComponentModel 名前空間 ) には これらを介して値の取得 更新が行われる また 値の変更を通知するためには INotifyPropertyChanged インターフェイス (System.ComponentModel 名前空間 ) を実装する必要がある 依存関係オブジェクト : ソース プロパティに依存関係プロパティを用いる場合 パフォーマンスもよく 標準で値の変更を通知する仕組みも備えている ただし 依存関係プロパティを利用するためには バインディング ソースが依存関係オブジェクトである (DependencyObject を継承している ) 必要がある ( その結果 ほかのクラスを継承できなくなる ) という問題もある XML オブジェクト : Binding マークアップ拡張の指定の仕方によっては (Source プロパティと XPath プロパティを利用 ) XML オブジェクトを直接バインディング ソースにすることができる 動的オブジェクト :.NET Framework 4 からは IDynamicMetaObjectProvider インターフェイスを実装する動的オブジェクトもバインディング ソースに指定可能になった すなわち IronPython や IronRuby などの動的言語を用いて作成したオブジェクトを そのままバインディング ソースにできる 実際のところ CLR オブジェクトをバインディング ソースとして利用する場合が多いだろう そこで重要になるのが INotifyPropertyChanged インターフェイスを利用した値の変更通知である この INotifyPropertyChanged インターフェイスの実装方法については 次回で説明する それでは 次のページからは Binding マークアップ拡張の書き方について詳しく説明していこう Binding マークアップ拡張の書き方 これまでの例では Binding マークアップ拡張を単に Binding X というように記述してきたが Binding マークアップ拡張にはさまざまなプロパティがあり データ バインディングの挙動を細かく設定することができる 以下 主要なものをいくつか紹介していく データ バインディングの向きとタイミング Mode プロパティで データ バインディングの向きとタイミングを指定できる Mode プロパティに対して設定できる値は以下のとおりである OneTime: UI 要素生成時に一度だけ ソース プロパティの値を読み出してターゲット プロパテ -20-
ィに与える OneWay: ソース プロパティが変更された際に ターゲット プロパティに変更を反映させる ( 逆は行わない ) OneWayToSource: ターゲット プロパティが変更された際に ソース プロパティに変更を反映させる TwoWay: ソース プロパティおよびターゲット プロパティのいずれの変更も 他方に反映させる 当然 TwoWay や OneWayToSource を指定するためには ソース プロパティが書き込み可能である (set アクセサを持っている ) 必要がある また TwoWay や OneWayToSource を指定した際には 変更をソース プロパティに反映させるタイミングを UpdateSourceTrigger プロパティで指定できる UpdateSourceTrigger プロパティに設定できる値は以下のとおりである Default: バインディング ターゲットの依存関係プロパティのメタデータに基づいてタイミングを決定する ( 何も指定しない場合 これが設定される ) PropertyChanged: バインディング ターゲットの値が変化するたびに ( 例えば テキストボックスの場合 1 文字入力されるたびに ) 変更を通知する LostFocus: バインディング ターゲットの要素がフォーカスを失うたびに ( 例えば テキストボックスからフォーカスを外した際に ) 変更を通知する Explicit: 明示的に UpdateSource メソッドを呼び出した場合にのみ変更を通知する バインディング ソースのパスの書き方 Path プロパティで バインディング ソースのパスを指定する Path プロパティは省略形で書くことができ これまで用いてきた Binding X という書き方は と同じ意味である Path プロパティでは 以下のような構文でパス指定が可能である Binding X.Y のように. でつなぐことで階層的なパス指定が可能 Binding X[0] のように 角カッコを用いてインデクサを利用可能 データ ソース (=DataContext に渡されたオブジェクト ) そのもののインデクサを利用する場合には Binding [0] というように Path プロパティに直接角カッコを記述する 引数が複数あるインデクサを利用する場合には <Binding Path="[3,4]" /> のように, を利用する ( ただし マークアップ拡張ではこの書き方はできない <Binding> 要素を使う必要がある ) 階層的なパス指定とインデクサは混在可能 ( Binding X[0].Y というような記述も可能 ) 添付プロパティをバインディング ソースにする場合には Binding (Canvas.Left) のように カッコでくくって ( 型名. プロパティ名 ) と記述する 例えば List 3 に示すように 階層的なデータを DataContext プロパティに渡した場合を考えてみよう Visual Basic Me.DataContext = New With -21-
. 管理者 = New With. 姓 = " 岩永 ",. 名 = " 信之 ",. コンテンツ = New With. タイトル = "C# 入門 ",.URL = "csharp", New With. タイトル = " 信号処理 ",.URL = "dsp", New With. タイトル = " 力学 ",.URL = "dynamics" Visual C# this.datacontext = new 管理者 = new 姓 = " 岩永 ", 名 = " 信之 ", コンテンツ = new[] new タイトル = "C# 入門 ", URL = "csharp", new タイトル = " 信号処理 ", URL = "dsp", new タイトル = " 力学 ", URL = "dynamics", ; List 3: 階層的なデータを DataContext に渡す このとき List 4 に示すような XAML コードを書くと Figure 5 に示すような表示結果が得られる 省略 <StackPanel x:name="panel" Canvas.Left="99"> <!-- 階層的なプロパティ指定 --> <TextBlock Text="Binding 管理者. 姓 "/> <!-- インデクサー --> <TextBlock Text="Binding コンテンツ [1].URL"/> <!-- 添付プロパティ --> <TextBlock Text="Binding ElementName=panel, Path=(Canvas.Left)"/> </StackPanel> 省略 List 4: パス指定の例 (XAML) Figure 5: List 4 の表示結果 -22-
コレクション走査パスの指定 Path プロパティでは Binding Path=List/X というように / でつなぐことで リストボックスなどで選択された行のプロパティを表示することができる ( コレクションを走査して 選択中の要素を拾い出してくれる ) 前節と同様に List 3 に示すデータを DataContext プロパティに渡した場合 List 5 に示すような XAML コードを書くと Figure 6 に示すような表示結果が得られる 省略 <StackPanel> <!-- / による選択行の表示を働かせるためには IsSynchronizedWithCurrentItem プロパティの指定が必要 --> <DataGrid ItemsSource="Binding Path= コンテンツ " IsSynchronizedWithCurrentItem="True" /> <!-- DataGrid コントロールで選択されている行の [ タイトル ] プロパティが表示される --> <TextBlock Text="Binding Path= コンテンツ / タイトル " /> </StackPanel> 省略 List 5: 階層的なデータを参照する Binding マークアップ拡張の書き方 (XAML) Figure 6: List 5 の表示結果 DataGrid コントロール内の行を選択すると その行データの [ タイトル ] 列の値が 下部のテキストボックスに表示される バインディング ソースの明示的指定 特に何も指定しない場合 バインディング ソースは UI 要素の DataContext プロパティに与えられたオブジェクトになる 一方で Binding マークアップ拡張の Source プロパティを設定することで バインディング ソースを明示的に指定することもできる 例えば List 6 に示すように 静的プロパティをバインディング ソースに指定するなどの用途で利用する -23-
省略 <StackPanel> <TextBlock Text="Binding Source=x:Static Application.Current, Path=StartupUri" /> </StackPanel> 省略 List 6: 静的プロパティをバインディング ソースに指定する例 (XAML) このコードを実行すると (Application クラス (System.Windows 名前空間 ) の Current 静的プロパティから得られる )Application オブジェクトの StartupUri プロパティ値 (= アプリケーションの起動時に自動的に表示される UI を参照する URI) が TextBlock コントロールに表示される Source プロパティのほかに 後述する ElementName プロパティや RelativeSource プロパティでもバインディング ソースを選択できるが これら 3 つのプロパティを同時に指定することはできない XPath バインディング ソースが XML データの場合 Path プロパティではなく XPath プロパティを利用する XPath プロパティには (XML に付随するパス指定言語である )XPath 言語 (XML Path Language) でクエリを記述する XPath クエリの書き方については本稿の範囲を超えるため 詳細は割愛し 例のみを List 7 に示す 省略 <Grid> <Grid.Resources> <XmlDataProvider x:key="pages" XPath="Pages"> <x:xdata> <Pages xmlns=""> <Page Title="C# 入門 " Url ="csharp" /> <Page Title=" 信号処理 " Url ="dsp" /> <Page Title=" 力学 " Url ="dynamics" /> </Pages> </x:xdata> </XmlDataProvider> </Grid.Resources> <ListBox ItemsSource= "Binding Source=StaticResource Pages, XPath=Page"> <ListBox.ItemTemplate> <DataTemplate> <TextBlock Text="Binding XPath=@Title"/> -24-
</DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> 省略 List 7: XML データをバインディング ソースとして利用する例 (XAML) このコードを実行すると XML データの <Pages> 要素内の各 <Page> 要素の Title 属性値が リストボックスの各行に表示される ほかの UI 要素の参照 場合によっては モデルなどのデータ ソースを介さず UI 要素間で直接プロパティ値の同期を取りたいことがある このような場合に ElementName プロパティを指定することで ほかの UI 要素をバインディング ソースにしてデータ バインディングを行える 例えば List 8 に示すような XAML コードを書くことで テキストボックスに表示されるテキストと スライダーの値を同期できる ( スライダーのつまみを動かすと 即座にテキストが変化する ) 省略 <StackPanel> <TextBox x:name="textvalue" /> <Slider Value="Binding ElementName=textValue, Path=Text" /> </StackPanel> 省略 List 8: UI 要素間でのデータ バインディング (XAML) 自分自身や先祖要素の参照 RelativeSource プロパティを指定することで UI 要素自身や 先祖要素 (= 直接の親だけではなく 階層的にたどれる上位の要素すべて ) のプロパティをデータ ソースに指定することができる 利用例を List 9 に示す 省略 <StackPanel> <!-- 先祖をたどって <Window> 要素を見つけ その Title プロパティとバインディング --> <TextBlock Text="Binding RelativeSource=RelativeSource Mode=FindAncestor, AncestorType=Window, Path=Title" /> <!-- 自分自身の Width プロパティと Height プロパティをバインディング ( 要するに 正方形にする ) --> <Rectangle Fill="Blue" Width="50" Height="Binding RelativeSource=RelativeSource Self, Path=Width" /> </StackPanel> -25-
省略 List 9: RelativeSource の利用例 (XAML) このコードを実行すると TextBlock コントロールに先祖 <Window> 要素の Title プロパティ値が表示され その下に縦 横 50 ピクセルの青い正方形が表示される 値の変換 値そのままではなく 何らかの変換処理を行ってからデータ バインディングしたい場合もあるだろう そのような場合には Binding マークアップ拡張の Converter プロパティに IValueConverter インターフェイス (System.Windows.Data 名前空間 ) を実装するクラスを渡す 例えば 角度の度数 (degree) と弧度数 (radian: ラジアン ) の ( 双方向 ) 変換を考えてみよう まず List 10 に示すように IValueConverter インターフェイス実装クラスを用意する Visual Basic Public Class DegreeToRadianConverter Implements IValueConverter Public Function Convert(ByVal value As Object, ByVal targettype As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.Convert Dim x As Double = CType(value, Double) Return x / 180 * Math.PI End Function Public Function ConvertBack(ByVal value As Object, ByVal targettype As System.Type, ByVal parameter As Object, ByVal culture As System.Globalization.CultureInfo) As Object Implements System.Windows.Data.IValueConverter.ConvertBack Dim x As Double = Double.Parse(CType(value, String)) Return (x / Math.PI * 180).ToString() End Function End Class Visual C# using System; using System.Windows.Data; using System.Globalization; namespace atmarkit05 public class DegreeToRadianConverter : IValueConverter -26-
public object Convert(object value, Type targettype, object parameter, CultureInfo culture) double x = (double)value; return x / 180 * Math.PI; public object ConvertBack(object value, Type targettype, object parameter, culture) double x = double.parse((string)value); return (x / Math.PI * 180).ToString(); List 10: 度と弧度を変換するためのクラス ( 上 :C# 下:VB) CultureInfo なお VB の場合 プロジェクト プロパティで設定した [ ルート名前空間 ] が atmarkit05 なっている必要がある このクラスを利用して List 11 に示すような XAML コードを記述することで 度と弧度の変換が可能になる 省略 <StackPanel xmlns:local="clr-namespace:atmarkit05"> <StackPanel.Resources> <local:degreetoradianconverter x:key="degtorad"/> </StackPanel.Resources> <Slider x:name="slider" Value="0" Minimum="0" Maximum="360" /> <TextBox Text="Binding ElementName=slider, Path=Value" /> <TextBox Text="Binding ElementName=slider, Path=Value, Converter=StaticResource DegToRad" /> </StackPanel> 省略 List 11: Converter プロパティを使った値の変換の例 (XAML) このコードを実行してスライダーを動かすと 上の TextBox コントロールに 0~360 の度数値が表示され その下の TextBox コントロールに ( 度数から変換された ) 弧度数が表示される なお VB の場合 プロジェクト プロパティで設定した [ ルート名前空間 ] が atmarkit05 なっている必要がある データ検証 Binding マークアップ拡張では エンド ユーザーから入力されたデータの検証を行うために 以下の 3 つのプロパティを利用する -27-
ValidationRules: ValidationRule クラス (System.Windows.Controls 名前空間 ) を継承するクラスを使って 明示的に検証ルールを追加する ValidatesOnExceptions: True に設定されている場合 ソース プロパティの更新中に例外が発生していないかを確認する ValidationRules プロパティに ExceptionValidationRule を追加した場合と同様の挙動になる ValidatesOnDataErrors: True に設定されている場合 IDataErrorInfo インターフェイスを介したデータ検証を有効にする ValidationRules プロパティに DataErrorValidationRule を追加した場合と同様の挙動になる データ検証の結果 何らかのエラーがあった場合には Validation.HasError 添付プロパティに True が設定され Validation.Errors 添付プロパティにエラーの一覧が格納される また WPF のコントロールのいくつかは 標準でデータ検証エラーに対応していて Figure 7 に示すように データ検証エラーがある場合にテキストボックスの枠線が赤くなるなどの変化が生じる (Validation.HasError 添付プロパティをトリガーとしたスタイルが定義されている ) Figure 7: データ検証エラー時のスタイル 数値をデータ バインディングしているテキストボックスに対して 不正な文字列を入力することで例外が発生している ValidatesOnException プロパティを True に設定することで このように テキストボックスの枠線が赤く変化する ( ただし この例で表示されているツールチップは 標準ではなく 追加でスタイルを定義している ) IDataErrorInfo インターフェイスの実装方法については 次回で説明する 続いて データ バインディングした単一のデータ / コレクション データの表示方法をカスタマイズできる データ テンプレート について説明する データ テンプレート 文字列や数値などの単純な型だけでなく 任意のデータ型を表示するための仕組みとして データ テンプレートというものがある 前回で説明したコントロール テンプレートと似ているが 名前どおり コントロール テンプレートがコントロールの表示方法をカスタマイズするものであるのに対して データ テンプレートはデータの表示方法をカスタマイズするものである ContentControl クラス : 単一のデータに対するデータ テンプレートの適用 まず 単一の ( コレクションではない ) データに対するテンプレートについて説明しよう 単一データに対するテンプレート指定は ContentControl クラス (Sytem.Windows.Control 名前空間 ) の Content プロパティにデータを ContentTemplate プロパティにデータ テンプレートを渡すことによって行う * * ちなみに Button クラスなどのコントロール類の多くは ContentControl クラスを継承していて こ -28-
のデータ テンプレートの仕組みを利用可能である ここでは 標準のクラス ライブラリで提供されているデータ型の中から 式ツリー (System.Linq.Expressions 名前空間以下のクラス ) を例にとって説明していく まず List 12 に示すように (x, y) => x + y という ( ラムダ式の ) 式ツリーを DataContext プロパティに渡す Visual Basic Dim sample As System.Linq.Expressions.Expression( _ Of Func(Of Integer, Integer, Integer)) = _ Function(x, y) x + y Me.DataContext = sample Visual C# System.Linq.Expressions.Expression<Func<int, int, int>> sample = (x, y) => x + y; this.datacontext = sample; List 12: 式ツリーをデータ ソースにする例 比較のために テンプレート指定のない場合にどうなるかを見てみよう List 13 に示すように Content プロパティのみを指定する ソース プロパティとして指定している Body プロパティには ラムダ式の本体部分 (= x + y の部分 ) が格納されている 省略 <StackPanel> <ContentControl Content="Binding Body" /> </StackPanel> 省略 List 13: データ テンプレートを指定しない場合 (XAML) データは <ContentControl> 要素の Content プロパティに指定する この場合 ただ単に (x + y) という文字列だけが表示されるはずだ これは 式ツリーを ToString メソッドで文字列化したものが表示されている それでは データ テンプレートを指定してみよう List 14 に示すように ContentTemplate プロパティに対して <DataTemplate> 要素を指定する Figure 8 に表示結果を示す データ テンプレートを指定しなかった場合もまとめて表示している 図中の上段が指定なし 下段が指定ありの場合の表示結果である 省略 <Grid> <ContentControl Content="Binding Body" Grid.Row="1" Grid.Column="1"> <ContentControl.ContentTemplate> <DataTemplate> -29-
<StackPanel Orientation="Horizontal"> <TextBlock Text="Binding NodeType" Background="LightBlue" /> <TextBlock Text=": " /> <TextBlock Text="Binding Left" Background="LightBlue" /> <TextBlock Text=", " /> <TextBlock Text="Binding Right" Background="LightBlue" /> </StackPanel> </DataTemplate> </ContentControl.ContentTemplate> </ContentControl> </Grid> 省略 List 14: データ テンプレートを指定する場合 (XAML) データ テンプレートは <ContentControl> 要素の ContentTemplate プロパティに指定する Figure 8: List 13 と List 14 の表示結果 データ テンプレートの自動適用 データ テンプレートは もちろんリソース化して複数の <ContentControl> 要素間で共有できる また コントロール テンプレートで自動適用ができたように ( 具体的には <ControlTemplate> 要素に TargetType プロパティを設定 ) データ テンプレートも <DataTemplate> 要素の DataType プロパティを設定することで 指定した型に対してデータ テンプレートを自動適用できる 例えば List 14 の例は List 15 のように書き換えられる 省略 <Grid xmlns:exp="clr-namespace:system.linq.expressions;assembly=system.core"> <Grid.Resources> <DataTemplate DataType="x:Type exp:binaryexpression"> <StackPanel Orientation="Horizontal"> -30-
<TextBlock Text="Binding NodeType" Background="LightBlue" /> <TextBlock Text=": " /> <TextBlock Text="Binding Left" Background="LightBlue" /> <TextBlock Text=", " /> <TextBlock Text="Binding Right" Background="LightBlue" /> </StackPanel> </DataTemplate> </Grid.Resources> <ContentControl Content="Binding Body" /> </Grid> 省略 List 15: データ テンプレートの自動適用の例 (XAML) データ テンプレートをリソース化して <DataTemplate> 要素の DataType プロパティを設定することで 指定した型に対して一括でデータ テンプレートを自動適用できる この例では System.Core アセンブリに含まれる BinaryExpression 型 (System.Linq.Expressions 名前空間 ) のデータすべてに対して <DataTemplate> 要素で定義されたデータ テンプレートが自動的に適用される データ テンプレートの自動適用は 型の混在する階層的なデータに対してテンプレートを適用したいときに特に重宝することだろう 例えば 式ツリーは その名前どおり 階層的なデータ構造をしているので List 15 に例示した BinaryExpression クラスなら Left プロパティや Right プロパティも式ツリーになっていて BinaryExpression クラスをはじめとするさまざまなクラスが格納されている このとき Left プロパティなどを再度 <ContentControl 要素 > の Content プロパティにバインディングしておけば 階層的なテンプレート適用が行われる 式ツリーを構成するさまざまな型すべてに対してデータ テンプレートを適用できるリソース ファイルを 下記の URL からダウンロードできるようにしたので 興味がある場合にはダウンロードしてみていただきたい リソース ファイルの利用方法は 本連載第 4 回の 外部リソースの取り込み の項を参照してほしい 式ツリー表示用のリソース ファイル (ExpressionTreeTemplates.xaml) のダウンロード ( 右クリックして [ 対象をファイルに保存 ]) このデータ テンプレート一式を使って -31-
(int x, int y) => (x + 3) * (y - 1) というラムダ式から得られた式ツリー全体を表示すると Figure 9 のようになる Figure 9: 階層的なデータに対してデータ テンプレートを適用した例 ここまでの実行例では Binding Body としてラムダ式の本体部分の式ツリー (=Body プロパティ ) のみを表示していたが この例では Binding (= Binding Path=. とも記述可能 ) としてラムダ式全体 (= バインディング ソース ) の式ツリーを表示している ItemsControl クラス : コレクションに対するデータ テンプレートの適用 データ ソースがコレクションの場合 ItemsControl クラス (Sytem.Windows.Control 名前空間 ) の ItemsSource プロパティにデータを ItemTemplate プロパティにデータ テンプレートを渡すことによってデータ バインディングを行う ItemTemplate プロパティに指定したテンプレートは コレクションの要素 1 つ 1 つに対して適用される また この場合にも ( 単一のデータの場合と同様に ) データ テンプレートの自動適用は有効である List 12 と同じデータ ( x + y という式ツリー ) に対して List 16 に示すような XAML コードを書くと Figure 10 のような表示結果が得られる ソース プロパティとして指定している Parameters プロパティには ラムダ式の引数リストの部分 (= x, y の部分 ) が格納されていて これは ParameterExpression 型のリストになっている 各要素には 型に基づいてテンプレートが自動適用される 省略 <Grid xmlns:exp="clr-namespace:system.linq.expressions;assembly=system.core"> -32-
<Grid.Resources> <DataTemplate DataType="x:Type exp:parameterexpression"> <StackPanel Orientation="Horizontal"> <TextBlock Text="Binding Name" Background="LightPink" /> <TextBlock Text=":" /> <TextBlock Text="Binding Type.Name" Background="LightPink" /> </StackPanel> </DataTemplate> </Grid.Resources> <ItemsControl ItemsSource="Binding Parameters" /> </Grid> 省略 List 16: コレクションに対するデータ テンプレートの適用 (XAML) データは <ItemsControl> 要素の ItemSource プロパティに指定する データ テンプレートは 個々の <ItemsControl> 要素の ItemTemplate プロパティに指定することもできるが この例では データ テンプレートをリソース化して <DataTemplate> 要素の DataType プロパティを設定することで 指定した型に対して一括でデータ テンプレートを自動的に適用している Figure 10: List 16 の表示結果 コラム C# コード内でデータ バインディング 参考までに データ バインディングを C# コードだけで記述する方法に触れておこう <TextBlock Text="Binding Path=X, Mode=OneWay" /> という記述と同様のデータ バインディングを C# コード内で行うためには List 17 に示すような記述が必要になる Binding クラス (System.Windows.Data 名前空間 ) のインスタンスを作成し FrameworkElement クラスの SetBinding メソッドを通じて データ バインディングを行う var source = new X = 10, Y = 20 ; var binding = new Binding(); -33-
binding.source = source; binding.mode = BindingMode.OneWay; binding.path = new PropertyPath("X"); var target = new TextBlock(); target.setbinding(textblock.textproperty, binding); List 17: C# コードのみでデータ バインディングを行う例 第 6 回 コマンド と MVVM パターン を理解する 前回に引き続き ビューとモデルの疎結合を実現するための仕組みの 1 つである コマンド という機能について説明する また データ バインディングとコマンドの仕組みを使った WPF アプリケーションのアーキテクチャ パターンである MVVM(Model-View-ViewModel) パターン についても紹介する GUI アプリケーションに対する要件 前回も行ったように WPF のコマンドの仕組みを説明する前に コマンドに関連する GUI アプリケーションに対する要件を整理してみよう 意味論的なイベント処理 ( コマンド ) 一般的な GUI アプリケーションでは マウスのクリックやキーボードからの入力など 実操作レベルのイベントをそのまま処理するよりも コピー や 貼り付け などの意味論的なレイヤを通してイベント処理したい場合も多い このような考え方は Figure 1 に示すように 意味論的には同じ操作を 複数の入力操作で実行したい場合に有効である WPF では この意味論的なイベントを コマンド (command) と呼んでいる -34-
Figure 1: 生の入力イベントと意味論的なイベント コマンドの実行可否 コマンドは常に実行できるわけではなく モデル (= 扱っているデータ ) の状態によっては実行できない状態になることがある 例えば ユーザーから入力されたデータが想定した形式でなければデータベースへの反映をさせないとか ネットワーク切断時にはオンライン処理を実行できなくするといった場合である ここで コマンドが実行できない状態にある場合 ( そのコマンドに対応する ) ビュー上のメニューやボタンを押せなくしたいという要求がある 実際 例えば多くのアプリケーションでは コピー可能なテキストを選択していないときにはメニューの コピー の項目がグレー表示され 押せなくなっている 注意が必要なのは コマンドの実行可否はモデルの状態に応じて随時変化し これに応じてボタンやメニューなどの有効 / 無効を切り替えなければならないということである 従って モデルの持つ 1 つのデータを複数のビューから参照するための仕組み (= データ バインディング ) と同様に データが変化したことを ( 各ビューに ) 通知する仕組み (= コマンド ) が必要となる WPF では 前回説明したデータ バインディングと 今回説明していくコマンドという仕組みを用いることで ビューとモデルの疎結合を保ちつつ これらの要件を満たすことができる コマンド WPF では 上記の要件でも説明したような 以下の 2 つの機能を提供する コマンド (command) という仕組みを持っている クリックやキー入力などの実操作レベルのイベントではなく コピーや貼り付けなどの意味的なイベント処理を行う モデルの状態に応じてボタンを押せなくするなどして コマンドを実行の可否を切り替える コマンドの実体 実装上の話をすると WPF のコマンドの実体は ICommand インターフェイス (System.Windows.Input 名前空間 ) を実装したクラスである ICommand インターフェイスは以下のようなメンバを持っている Execute メソッド : コマンドを実行する CanExecute メソッド : コマンドが実行可能な状態にあるかどうかを判定する CanExecuteChanged イベント : INotifyPropertyChanged インターフェイスの PropertyChanged イベントと同様 コマンド実行の可否が変化したことを通知するためのイベント コマンドには主に 2 通りの使い方があり 1 つはデータ バインディングを用いてビューの外部からコマンドを与える方法で もう 1 つは ルーティング コマンド と呼ばれる方法である -35-
次のページでは この 2 つの方法の内容をそれぞれ説明する データ バインディングでコマンド処理 前回説明したように WPF にはデータ バインディングという仕組みがあり ビューの外部から表示したいデータを与えることができる ( 外部から与えるデータは 後述する ビューモデル (ViewModel) というものになる場合が多い ) この仕組みを用いることで Figure 2 に示すように 表示したいデータだけでなく コマンドを外部から与えることも可能である こうすることで ビュー内にはデータも処理も書く必要がなくなり 一般にテストが難しくなりがちなビューを極力小さくすることができる Figure 2: データ バインディングでビューの外部からコマンドを与える この方法におけるコマンドの利用手順は以下のようになる ICommand インターフェイスを実装したクラスを作る (= コマンドの実装 ) 外部から与えるデータ (= ビューモデル ) により 実装したコマンドをプロパティとして公開する 公開したプロパティを <Button> 要素や <MenuItem> 要素などの Command プロパティにデータ バインディングする 例えば [OK] ボタンを押すとメッセージボックスを表示するだけの単純なウィンドウを作成するには List 1 のような書き方をする XAML <Window x:class="atmarkit06.commandwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title=" コマンド ( ビューモデル )" Height="80" Width="100"> <Grid> <Button Content="OK" Command="Binding OkCommand" /> -36-
</Grid> </Window> Visual Basic Class CommandWindow Sub New() InitializeComponent() Me.DataContext = New CommandWindowViewModel() End Sub End Class ' 外部から与えるデータ (= ビューモデル ) Public Class CommandWindowViewModel Class OkCommandImpl : Implements ICommand Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute Return True End Function Public Event CanExecuteChanged(ByVal sender As Object, ByVal e As EventArgs) Implements ICommand.CanExecuteChanged Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute MessageBox.Show(" コマンドが実行されました ") End Sub End Class Private newokcommand As ICommand Public Property OkCommand As ICommand Get Return newokcommand End Get Private Set(ByVal value As ICommand) newokcommand = value End Set End Property Sub New() Me.OkCommand = New OkCommandImpl() End Sub End Class Visual C# -37-
using System; using System.Windows; using System.Windows.Input; namespace atmarkit06 public partial class CommandWindow : Window public CommandWindow() InitializeComponent(); this.datacontext = new CommandWindowViewModel(); // 外部から与えるデータ (= ビューモデル ) public class CommandWindowViewModel class OkCommandImpl : ICommand public bool CanExecute(object parameter) return true; public event EventHandler CanExecuteChanged; public void Execute(object parameter) MessageBox.Show(" コマンドが実行されました "); public ICommand OkCommand get; private set; public CommandWindowViewModel() this.okcommand = new OkCommandImpl(); List 1: データ バインディングを使ったコマンド処理 この書き方が 後述する MVVM パターンでのコマンド処理の基本となる ICommand インターフェイスの実装については 本稿後半の MVVM パターンの項で再度説明を行う ルーティング コマンドを使ったコマンド処理 もう 1 つはルーティング コマンド (routed command) を使う方法である WPF 標準で用意された ICommand インターフェイスの 1 実装として RoutedCommand クラスというものがあり このクラスを継承した派生クラスのオブジェクト (= これが ルーティング コマンドの実体 ) を用いる ちなみに WPF では標準で RoutedCommand クラスを継承した RoutedUICommand クラスが用意されている -38-
Figure 3 に示すように RoutedCommand クラスでは Execute メソッドが呼び出された際に Executed などのルーティング イベントを発生させる このルーティング イベントをツリー構造の上位の UI 要素で拾って処理することになる Figure 3: ルーティング コマンド RoutedCommand クラスは 以下のようなルーティング イベントを発生させる ( トンネル イベントでは 要素ツリーのルートからイベント発生源となる要素に向かって 要素ツリーを掘り進むようにイベント ハンドラが呼び出される トンネル イベントの名前には原則として Preview というプレフィックスが付く バブル イベントでは イベント発生源となる要素からルートに向かって 要素ツリーをたどりながら 浮かび上がるようにイベント ハンドラが呼び出される ) PreveiwExecuted: Excute メソッドが呼ばれたときに発生するトンネル イベント Executed: Excute メソッドが呼ばれたときに発生するバブル イベント PreviewCanExecute: CanExcute メソッドが呼ばれたときに発生するトンネル イベント CanExecute: CanExcute メソッドが呼ばれたときに発生するバブル イベント ルーティング コマンドを使う方法でのコマンドの利用手順は以下のようになる RoutedCommand クラスを継承してルーティング コマンドを自作するか あるいは WPF 標準で用意されたルーティング コマンド (=RoutedUICommand クラス ) をそのまま利用する 自作したルーティング コマンドは ( 任意のクラスで ) 静的プロパティなどで公開しておく 一方 WPF 標準のルーティング コマンドは何もしなくてもよい ( 詳しくは後述するが ApplicationCommands クラスなどで静的プロパティとして公開されているため ) ルーティング コマンドの実体 (= 公開済みの静的プロパティの値 ) を <Button> 要素や <MenuItem> 要素などの Command プロパティに与える ルーティング コマンド自作した場合 x:static マークアップ拡張 *1 などを用いて値に与える ( 本稿ではこの手法は割愛 ) 標準のルーティング コマンドは 例えば Command="ApplicationCommands.Copy" *2 などと書くだけで利用可能 ( 詳細後述 ) コマンド処理を行う上位の UI 要素で CommandBindings プロパティを設定 その中の <CommandBinding> 要素の Command プロパティにルーティング コマンドの実体 (= 公開済みの -39-
静的プロパティの値 ) を指定し Executed プロパティなどにイベント ハンドラを登録する (= コマンド バインディング ) *1 x:static マークアップ拡張とは マークアップ拡張構文の一種で < オブジェクト名プロパティ名 ="x:static プレフィックス : 静的な型 / オブジェクトの名前. 静的なメンバの名前 ".../> のような記述により 何らかクラスの静的プロパティを XAML コードで使用するためのもの *2 この例で指定している ApplicationCommands.Copy 静的プロパティからは RoutedUICommand オブジェクトが得られる 詳細後述 WPF 標準のルーティング コマンド (= いずれも RoutedUICommand オブジェクト ) は 利用目的別にいくつかのクラスにまとめられ (= コマンド ライブラリ ) 各クラスの静的プロパティとして公開されている 具体的には以下のようなクラスがある ( EditingCommands クラスのみ System.Windows.Documents 名前空間 それ以外はいずれも System.Windows.Input 名前空間 ) ApplicationCommands: ファイルを開く 保存 ウィンドウを閉じるなど NavigationCommands: ブラウザの戻る ホームを開く 前項 / 次項を開くなど ComponentCommands: リスト アイテムやセルの移動 スクロール フォーカスの移動など EditingCommands: テキスト編集用のコマンド MediaCommands: メディアの再生 停止 次のトラックへ移動など 例えば List 1 と同様のもの (=[OK] ボタンを押すとメッセージボックスを表示するプログラム ) を作成するには List 2 のような書き方をする このコードでは 標準のルーティング コマンドを利用している XAML <Window x:class="atmarkit06.routedcommandwindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title=" コマンド ( ルーティング コマンド )" Height="80" Width="100"> <Window.CommandBindings> <CommandBinding Command="Properties" Executed="CommandBinding_Executed" /> </Window.CommandBindings> <Grid> <Button Content="OK" Command="Properties" /> </Grid> </Window> Visual Basic Class RoutedCommandWindow Private Sub CommandBinding_Executed(ByVal sender As Object, ByVal e As ExecutedRoutedEventArgs) MessageBox.Show(" コマンドが実行されました ") -40-
End Sub End Class Visual C# using System.Windows; using System.Windows.Input; namespace atmarkit06 public partial class RoutedCommandWindow : Window public RoutedCommandWindow() InitializeComponent(); private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e) MessageBox.Show(" コマンドが実行されました "); List 2: ルーティング コマンドを用いたコマンド処理 ちなみにルーティング コマンドを自作する場合は public static RoutedCommand MyRoutedCmd = new RoutedCommand(); // C# の場合 Public Shared MyRoutedCmd As New RoutedCommand() ' VB の場合 のような 1 行を C#/VB コードの任意の public クラス内 ( 例えば 上記の RoutedCommandWindow クラス内 ) に記述して XAML コードの Command プロパティに x:static マークアップ拡張で x:static custom:routedcommandwindow.myroutedcmd のように記述すればよい なお この cusutom という XML 名前空間に対応する xmlns:custom 属性は あらかじめ XAML コードのルートにある <Window> 要素に追記しておく必要がある 例えば下記のようなコードになる xmlns:custom="clr-namespace:atmarkit06" Command プロパティに設定している Properties という値は 正確には ApplicationCommands クラス (System.Windows.Input 名前空間 ) の Properties 静的プロパティを指す 従って 本来なら x:static ApplicationCommands.Properties と記述するべきだが WPF 標準のルーティング コマンド群の場合 通常 Command 属性への指定では ApplicationCommands. などを省略して Command="Copy" と簡潔に記述できる なお この ApplicationCommands.Properties 静的プロパティより得られるルーティング コマンドは -41-
本来 プロパティ ウィンドウを開く という処理を表すものだが 説明の簡素化のために ここではメッセージボックスの表示で代用している ルーティング コマンドは ユーザー コントロールを自作する場合や コントロール テンプレートを利用する場合などに特に有効である 連載第 4 回でスクロールバーへのコントロール テンプレートの適用の例を挙げたが テンプレート側でルーティング コマンドを使って Executed などのルーティング イベントを発生させ コントロール側の <CommandBindings> 要素でそれを拾ってイベント (= コマンド ) 処理してもらうというような使い方が可能だ コマンド ソース データ バインディングを使うにしろ ルーティング コマンドを使うにしろ まずはコマンドの発生源 (= コマンド ソース ) の設定が必要だ WPF では コマンド ソースを実装するための ICommandSource インターフェイス (System.Windows.Input 名前空間 ) があり コマンド ソースとなる UI 要素を作る場合には このインターフェイスを実装する ICommandSource インターフェイスは以下のようなメンバを持っている Command プロパティ : 発生させたいコマンドを設定する CommandParameter プロパティ : ICommand インターフェイスの Execute メソッドや CanExecute メソッドの引数として渡されるパラメータを設定する CommandTarget プロパティ : コマンドの実行対象を設定する 例えば Paste コマンドが実行される際に どの UI 要素に対して貼り付け処理を行うかなどを指定する ルーティング イベントでのみ利用可能 WPF 標準で提供されているコマンド ソースには 大きく分けてコントロールとインプット バインディングの 2 種類ある コントロール <Button> 要素や <MenuItem> 要素など 一部のコントロールは ICommandSource インターフェイスを実装している このため ボタンをクリックした際などに Command プロパティで与えたコマンドが実行される ( 例 : 前述の List 1: データ バインディングを使ったコマンド処理 や List 2: ルーティング コマンドを用いたコマンド処理 で示した <Button> 要素の Command プロパティを参照 ) ただし 残念ながら 標準では左ボタンのクリック イベントを拾ってコマンド実行する以外のことができない 例えば ダブルクリックや 中ボタンのクリックでコマンドを実行したい場合もあるかもしれないが このような場合 後述するインプット バインディングを用いることになる ( 厳密には ビヘイビアを使う方法もあるが Expression Blend SDK が必要になるなど 入門の範囲を超えてしまうため 本連載では割愛する ) インプット バインディング コントロールのような 目に見えるコマンド ソース以外に キーボード ショートカットや特殊なマウス ジェスチャーなどの入力系のイベントを拾ってコマンド実行するためのインプット バインディングという仕組みがある -42-
UIElement クラス (System.Windows 名前空間 )(=WPF の UI 要素の共通基底クラス ) には InputBindings というプロパティがあり これに <KeyBinding> 要素 もしくは <MouseBinding> 要素を与えることでインプット バインディングを行う 例えば [Alt]+[Shift]+[X] キーを押す や [Ctrl] キーを押しながらマウス ホイールを回す などの特殊な操作に対してコマンドを実行するには List 3 に示すような XAML コードを記述する <Window x:class="atmarkit06.commandsourcewindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title=" インプット バインディング " Height="300" Width="300"> <Window.InputBindings> <KeyBinding Gesture="Alt+Shift+X" Command="Binding OkCommand" /> <MouseBinding Gesture="Ctrl+WheelClick" Command="Binding OkCommand" /> </Window.InputBindings> <Grid> </Grid> </Window> List 3: インプット バインディング (XAML) List 1 と同様のビューモデルをデータ バインディングすれば [Alt]+[Shift]+[X] キーを押す [ Ctrl] キーを押しながらマウス ホイールを回す という操作を行った際にメッセージボックスが表示される <KeyBinding> 要素の Gesture プロパティには 1 つ以上の修飾キーと 1 つのキーを定義する KeyGesture クラス (System.Windows.Input 名前空間 ) の値を単一の文字列で設定する 文字列からのコンバータが働くので List 3 に示す Alt+Shift+X などのような略記が可能になっている 同様に <MouseBinding> 要素の Gesture プロパティには ( 必要であれば 1 つ以上の修飾キーと )1 つのマウス操作を定義する MouseGesture クラス (System.Windows.Input 名前空間 ) の値を単一の文字列で List 3 に示す Ctrl+WheelClick などのように略記で設定する ちなみに 標準で用意されている (ApplicationCommands クラスなどの静的プロパティとして公開されている ) ルーティング コマンドに対しては 最初からインプット バインディングが設定されていて 何も記述しなくても Ctrl+S で Save コマンド Ctrl+C で Copy コマンドなどというように ルーティング コマンドが実行される 前回と今回を通したここまでの説明を踏まえて 次のページでは MVVM パターンについて解説する MVVM パターン これまでの説明でも用語として少し出てきたが WPF による GUI アプリケーション開発では ( 特に -43-
アプリケーション規模が大きい場合 ) Figure 4 に示すような ビューとモデルの間に ビューモデル と呼ばれるものを挟んだ 3 階層アーキテクチャで作成する場合が多い このようなアーキテクチャ パターンを MVVM(Model-View-ViewModel) パターン と呼ぶ Figure 4: ビュー ビューモデル モデルの 3 階層構造 ビューモデルからビューへの表示変更の通知は INotifyPropertyChanged インターフェイスの実装を通じて行われる 逆に ビューからビューモデルへのコマンド ( 状態変更 ) の通知は ICommand インターフェイスの実装を通じて行われる MVVM パターンは 有名な MVC(Model-View-Controller) パターンと同種のアーキテクチャ パターンで WPF の強力なデータ バインディング機能の利用を前提とした亜種だと考えればいいだろう ビューモデルの役割 MVVM パターンにおいて ビューモデルは以下の 2 つの役割を担う モデルを WPF 向けにラッピング ビューから状態を分離 モデルをラッピング 前回で説明したように WPF のデータ バインディングの機能を最大限享受するためには データ ソースが INotifyPropertyChanged インターフェイス (System.ComponentModel 名前空間 ) を実装している必要がある また 前節での説明のとおり ユーザーの操作に応じて何らかの処理を行う場合には ICommand インターフェイスを実装したコマンドを利用することになる さらに ユーザーからの入力に不整合がないかなどのデータ検証を行うためには IDataErrorInfo インターフェイス (System.ComponentModel 名前空間 ) を実装する これらのインターフェイスは WPF 専用というわけではなく 実際 Windows フォームや ASP.NET でも使われているものではあるが それでもすべてのモデルがこれらを実装しているわけではない そこで モデルをラッピングして INotifyPropertyChanged インターフェイスによるプロパティ値の変更通知などを実装するのが ビューモデルの役割の 1 つである -44-
ビューから状態を分離 一般に GUI 部分 (= ビュー ) のテストは手間がかかりがちなため ビューは極力小さくとどめたいという要求があり ビューから状態を分離する ということがよく行われる 状態の分離 といわれてもピンと来ないかもしれないが WPF のコマンドの仕組みを前提として説明するなら 以下のような 2 つのテストをそれぞれ別のクラスに分離したいということである このボタンを押したらこのコマンドが実行されるはず というテスト (= ビューに残す部分 ) このコマンドが実行されたらデータ ソースのここがこう変化するはず (= 状態変化 ) というテスト (= ビューモデルに追いやる部分 ) この分離が不明瞭 ( ふめいりょう ) な場合 考え得るすべての状態 ( 仮に m 個とする ) のときに 考え得るすべての入力 ( 仮に n 個 ) を与えてテストを行う必要が生じ テスト ケースが膨大な数 (m n 個 ) となる これに対して 明確に分離すれば テスト ケースを少なく (m+n 個 ) 抑えられる もともと ビューとモデルの疎結合 という考え方も ビューを小さくとどめるための工夫の 1 つである しかし モデルはさまざまなビューでの利用が ( 場合によってはコンソール アプリケーションでも ) 可能な形で作られるもので ある特定のビュー上でしか必要のないような状態を記録しておく場所ではない そこで このような特定のビュー上でのみ利用する状態を保持する場所として ビューモデルを利用することになる データ バインディングによる疎結合 ビューとモデルの疎結合 や ビューからの状態の分離 が可能なのも Figure 5 に示すように INotifyPropertyChanged インターフェイスや ICommand インターフェイスなどの標準的なインターフェイスを介した通知機構のおかげである Figure 5: ビューからの状態の分離 ビューとモデルの疎結合 この図の View は ビューとなる WPF ウィンドウなどを指している また State は ビューモデルとなるオブジェクトを指している -45-
このようなインターフェイスを介した通知機構を利用する際 最後に残る問題は コマンドの登録や PropertyChanged イベントに対するハンドラの登録を誰がどこで行うかという部分である (Figure 5 中の赤い枠線内のような処理 ) WPF のデータ バインディングでは この手の登録作業を View.DataContext = ViewModel; などと書くだけで フレームワークの内部ですべて自動的に行ってくれるため 非常に手軽になっている ( ここでいう View とはビューとなる WPF ウィンドウなどのことで ViewModel とはビューモデルとなるオブジェクトのこと 詳しくは前回のデータ バインディングに関する説明を参照されたい ) ビューモデルの作成方法 それでは INotifyPropertyChanged ICommand IDataErrorInfo などのインターフェイスの実装方法について見ていこう なお ビューモデルとビューをデータ バインディングするための前提条件として ビュー側のコンストラクタに Visual Basic Me.DataContext = New ViewModel() Visual C# this.datacontext = new ViewModel(); というコードが用意され XAML コードに <StackPanel> <TextBox Text="Binding X, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True" ToolTip="Binding RelativeSource =x:static RelativeSource.Self, Path=(Validation.Errors)[0].ErrorContent"/> <Button Content="OK" Command="Binding OkCommand" /> </StackPanel> というコードがあらかじめ記述されているものとする ( このコードにある Binding マークアップ拡張 の書き方については 第 5 回の記事を参照してほしい ) INotifyPropertyChanged インターフェイスの実装 INotifyPropertyChanged インターフェイスは PropertyChanged イベントを持っていて プロパティの値が変化した際には このイベントを起こすことになる 例えば int/integer 型の X というプロパティを実装するなら List 4 に示すようなコードを書く Visual Basic Imports System.ComponentModel Public Class ViewModel : Implements INotifyPropertyChanged Private _x As Integer -46-
Public Property X As Integer Get Return _x End Get Set(ByVal value As Integer) If _x <> value Then _x = value RaisePropertyChanged("X") End If End Set End Property Public Event PropertyChanged(ByVal sender As Object, ByVal e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged Protected Sub RaisePropertyChanged(ByVal propertyname As String) RaiseEvent PropertyChanged( _ Me, New PropertyChangedEventArgs(propertyName)) End Sub End Class Visual C# using System.ComponentModel; public class ViewModel : INotifyPropertyChanged private int _x; public int X get return _x; set if (_x!= value) _x = value; RaisePropertyChanged("X"); public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged(string propertyname) var d = PropertyChanged; if (d!= null) d(this, new PropertyChangedEventArgs(propertyName)); -47-
List 4: INotifyPropertyChanged インターフェイスを用いたプロパティ値の変更通知の実装 また X+Y の値を返すような X プロパティに依存する別のプロパティ ( 今回の例では Sum というプロパティ ) を作る場合には List 5 に示すようなコードを書く ( 追記した個所は太字で示している ) Visual Basic 省略 Public ReadOnly Property Sum As Integer Get Return X + Y End Get End Property ' ToDo: ここに Y プロパティも実装する必要がある Private _x As Integer Public Property X As Integer Get Return _x End Get Set(ByVal value As Integer) If _x <> value Then _x = value RaisePropertyChanged("X") RaisePropertyChanged("Sum") End If End Set End Property 省略 Visual C# 省略 public int Sum get return X + Y; // ToDo: ここに Y プロパティも実装する必要がある private int _x; public int X get return _x; set if (_x!= value) -48-
_x = value; RaisePropertyChanged("X"); RaisePropertyChanged("Sum"); 省略 List 5: ほかのプロパティに依存するプロパティの値の変更通知 ICommand インターフェイス (= コマンド ) の実装 前述のとおり コマンドの仕組みを使う際には ICommand インターフェイスを実装したクラスを作成する ただ コマンド 1 つ 1 つに対して毎度クラスを作成するのは手間がかかるので ヘルパー的なクラスを作っておく場合が多い 特によく使われるパターンは List 6 に示すような Execute メソッドや CanExecute メソッド内ではデリゲートを呼び出すだけというクラスである (DelegateCommand という名前を付けることが多い ) Visual Basic Public Class DelegateCommand : Implements ICommand Public Property ExecuteHandler As Action(Of String) Public Property CanExecuteHandler As Func(Of Object, Boolean) #Region "ICommand メンバー " Public Function CanExecute(ByVal parameter As Object) As Boolean Implements ICommand.CanExecute Dim d = CanExecuteHandler Return IIf(d = Nothing, True, d(parameter)) End Function Public Sub Execute(ByVal parameter As Object) Implements ICommand.Execute Dim d = ExecuteHandler If d <> Nothing Then d(parameter) End If End Sub Public Event CanExecuteChanged(ByVal sender As Object, ByVal e As EventArgs) Implements ICommand.CanExecuteChanged Public Sub RaiseCanExecuteChanged() RaiseEvent CanExecuteChanged(Me, Nothing) End Sub #End Region -49-
End Class Visual C# using System; using System.Windows.Input; public class DelegateCommand : ICommand public Action<object> ExecuteHandler get; set; public Func<object, bool> CanExecuteHandler get; set; #region ICommand メンバー public bool CanExecute(object parameter) var d = CanExecuteHandler; return d == null? true : d(parameter); public void Execute(object parameter) var d = ExecuteHandler; if (d!= null) d(parameter); public event EventHandler CanExecuteChanged; public void RaiseCanExecuteChanged() var d = CanExecuteChanged; if (d!= null) d(this, null); #endregion List 6: DelegateCommand クラス この DelegateCommand クラスを用いて X プロパティの値が正のときだけ実行ができ 実行時にはメッセージボックスを表示する というようなコマンドを実装するなら ビューモデルのコードは List 7 のようになる (List 4 から追記した個所は太字で示している List 5 で追記した部分は削除した ) Visual Basic Imports System.ComponentModel Public Class ViewModel : Implements INotifyPropertyChanged -50-
Private _x As Integer Public Property X As Integer Get Return _x End Get Set(ByVal value As Integer) If _x <> value Then _x = value RaisePropertyChanged("X") CType(OkCommand, DelegateCommand). RaiseCanExecuteChanged() End If End Set End Property Private Sub OkCommandExecute(ByVal parameter As Object) MessageBox.Show(" コマンドが実行されました ") End Sub Private Function OkCommandCanExecute(ByVal parameter As Object) As Boolean Return X > 0 End Function Private _okcommand As ICommand Public ReadOnly Property OkCommand As ICommand Get If _okcommand Is Nothing Then _okcommand = New DelegateCommand With.ExecuteHandler = _ AddressOf OkCommandExecute,.CanExecuteHandler = _ AddressOf OkCommandCanExecute End If Return _okcommand End Get End Property Public Event PropertyChanged(ByVal sender As Object, ByVal e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged Protected Sub RaisePropertyChanged(ByVal propertyname As String) RaiseEvent PropertyChanged( _ Me, New PropertyChangedEventArgs(propertyName)) End Sub -51-
End Class Visual C# using System.ComponentModel; using System.Windows; using System.Windows.Input; public class ViewModel : INotifyPropertyChanged private int _x; public int X get return _x; set if (_x!= value) _x = value; RaisePropertyChanged("X"); ((DelegateCommand)OkCommand). RaiseCanExecuteChanged(); private void OkCommandExecute(object parameter) MessageBox.Show(" コマンドが実行されました "); private bool OkCommandCanExecute(object parameter) return X > 0; private ICommand _okcommand; public ICommand OkCommand get if (_okcommand == null) _okcommand = new DelegateCommand ExecuteHandler = OkCommandExecute, CanExecuteHandler = OkCommandCanExecute, ; return _okcommand; -52-
public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged(string propertyname) var d = PropertyChanged; if (d!= null) d(this, new PropertyChangedEventArgs(propertyName)); List 7: DelegateCommand クラスの利用例 IDataErrorInfo インターフェイス (= データ検証 ) の実装 前回説明したように データ バインディング時に ValidatesOnDataErrors プロパティに True を設定することで (Binding マークアップ拡張は前述のコード例を参照 ) IDataErrorInfo インターフェイス (System.ComponentModel 名前空間 ) を用いたデータ検証が有効になる 例えば List 8 のようなコードを書くことで X プロパティの値が正の数でないときに検証エラーを表示できる Visual Basic Imports System.ComponentModel Public Class ViewModel : Implements INotifyPropertyChanged, IDataErrorInfo ' X プロパティや PropertyChanged イベントの実装は List 4 と同じ 省略 #Region "IDataErrorInfo メンバー " Public ReadOnly Property [Error] As String Implements IDataErrorInfo.Error Get Return IIf(X <= 0, "X は正の数 ", Nothing) End Get End Property Default Public ReadOnly Property Item(ByVal columnname As String) As String Implements IDataErrorInfo.Item Get Select Case columnname Case "X" Return IIf(X <= 0, "X は正の数 ", Nothing) End Select Return Nothing End Get -53-
End Property #End Region End Class Visual C# public class ViewModel : INotifyPropertyChanged, IDataErrorInfo // X プロパティや PropertyChanged イベントの実装は List 4 と同じ 省略 #region IDataErrorInfo メンバー public string Error get return X <= 0? "X は正の数 " : null; public string this[string columnname] get switch(columnname) case "X": return X <= 0? "X は正の数 " : null; return null; #endregion, List 8: IDataErrorInfo インターフェイスの実装例 ただし この例のような インデクサの中で switch(columnname) / Select Case columnname で分岐するような実装では プロパティの数が増えるにつれてコードの管理が大変になる そこで 例えば以下のリンク先の参考ページのように データ検証属性 ( = System.ComponentModel.DataAnnotations 名前空間にある Required 属性などを用いて実装を簡素化するというような方法がよく用いられる ( リンク先の参考ページにおけるサンプル コードは Silverlight 4 向けで そのままでは WPF で利用できないが ほぼ同様の実装が可能である ) かずきの Blog@Hatena:Silverlight 4 のデータ検証 汎用的な INotifyDataErrorInfo の実装 ちなみに ここで出てきた INotifyDataErrorInfo インターフェイスは Silverlight にだけ存在し データ検証状態が変化したことを通知する仕組みが入ったインターフェイスである (IDataErrorInfo インターフェイスには通知の仕組みがなく プロパティをまたいだ検証がしづらくなっている ) -54-
コラム MVVM パターンを活用する前に知っておきたい補足情報 自動生成 List 4~8 のコードを見てのとおり ビューモデルには定型的なコードが大量に並ぶことになるため 実装が非常に面倒という問題がある そこで コードの自動生成などに頼り 開発を効率化するための手段がいくつか提供されている 参考 : T4(Text Template Transformation Toolkit) テンプレートの利用 : T4 Template で ViewModel を生成する コード スニペットの利用 : コードスニペットを使った INotifyPropertyChanged の実装 DynamicObject の利用 : M-V-VM パターン用動的プロキシ デザイン時属性 WPF のデータ バインディングの仕組みは リフレクションなどを使って動的に動作しているため ビルド時にスペルミスなどの人的エラーに悩まされたり Visual Studio の人気機能の 1 つであるコード補完が働かなかったりといった問題もある そこで Visual Studio 2010 や Expression Blend では デザイン時限定で適用され ビルド結果の実行可能ファイル中には情報が残らない d:datacontext というプロパティ (= デザイン時 DataContext) を使って データ バインディングの入力補助を行う仕組みを備えている 参考 : MSDN: チュートリアル : Silverlight Designer でのサンプルデータの使用 (Visual Studio 2010 では WPF と Silverlight のデザイナーが共通化されているため WPF の場合でもそのまま利用可能 ) 今回で 長かった XAML/WPF の基礎や仕組みについて一通り説明が終わった 次回からは個々の UI 要素について説明していく -55-
具体的には Grid や StackPanel などのレイアウト用のパネルについて説明を行っていく予定だ http://www.atmarkit.co.jp/ait/subtop/features/dotnet/app/introwpf_index.html -56-