ビヘイビアについて少し考えてみた。
ActualWidth はバインドできない
C# WPF MVVM での開発において、FrameworkElement.ActualWidth は XAML からのバインドができない(VM で ActualWidth を取得できない)。
例えば Window.Title なら「Title="{Binding Hoge}"」のようにバインドできるが、「ActualWidth="{Binding Hoge}"」はエラーとなる。Title も ActualWidth も依存プロパティー(依存関係プロパティー)だが、ActualWidth は読み取り専用のため、「Setter が無い」旨のエラーとなる。
書き込みできないだけなら、「ActualWidth="{Binding ActualWidth, Mode=OneWayToSource}"」のような片方向バインドはさせてくれ、と思うのだが、それもできない。標準コントロールの全プロパティーはバインド可能にしておいて欲しいのだが、そんな日は来るのだろうか。
Livet で楽ちん
バインドできないものをバインドさせるには、通常はビヘイビアを自作する。
が、ActualWidth 程度であれば、ビヘイビアを自作するまでもなく Livet の XXXXSetStateToSourceAction を使うのが簡単。サンプルプログラムでは、同類の ActualHeight について、Livet の WindowSetStateToSourceAction を使用してバインドすることにより、VM で ActualHeight が取得できるようになっている。
<behaviors:Interaction.Triggers> <behaviors:EventTrigger EventName="SizeChanged"> <l:WindowSetStateToSourceAction Source="{Binding ActualHeight, Mode=TwoWay}" Property="ActualHeight" /> </behaviors:EventTrigger> </behaviors:Interaction.Triggers>
今回はビヘイビアについて少し考えてみるのが目的なので、ActualWidth についてビヘイビアを自作する。
ビヘイビアで ActualWidth をバインド可能にする
ビヘイビアの作り方については「ビヘイビア WPF」あたりでググればたくさん出てくるが、コードの内容についての解説があまり見当たらないので、少し考えてみた(コード全体については冒頭のサンプルプログラム参照)。
XAML からバインド可能にするには依存プロパティーを作る必要があり、それが
public static readonly DependencyProperty ActualWidthProperty = DependencyProperty.Register(nameof(ActualWidth), typeof(Double), typeof(WindowBindingSupportBehavior), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
の部分。そして、依存プロパティーとリンクする普通のプロパティーが
public Double ActualWidth { get => (Double)GetValue(ActualWidthProperty); set => SetValue(ActualWidthProperty, value); }
の部分。
依存プロパティーの作成と登録を行うのが DependencyProperty.Register() で、その第一引数 nameof(ActualWidth) がプロパティー名。依存プロパティー名 = プロパティー名 + "Property" となっている必要がある。なっていないと、実行時にエラーになる。ビルド時にエラーにしてほしいところだ。
多くのサイトで「DependencyProperty.Register("ActualWidth"...」のようにプロパティー名を即値で書いているが、「nameof(ActualWidth)」と書く方が多少安全になるかと思う。
DependencyProperty.Register() の第二引数で変数の型、第三引数でビヘイビアクラスを指定する。第四引数の FrameworkPropertyMetadata 内でプロパティーのデフォルト値を指定する。
依存プロパティー登録により ActualWidth のバインドが可能となったので、次は、ActualWidth の値を更新して有用な値にするところ。
ウィンドウのサイズが変更された際に ActualWidth を変化させれば、常に ActualWidth を最新の値に設定できる。
OnAttached() で SizeChanged イベントハンドラー(ControlSizeChanged)を登録し、そのイベントハンドラー内で ActualWidth を更新する。
以上で、ActualWidth をバインドして使えるようになる。
OnAttached() で SizeChanged イベントハンドラー(ControlSizeChanged)を登録し、そのイベントハンドラー内で ActualWidth を更新する。
以上で、ActualWidth をバインドして使えるようになる。
サンプルプログラムを起動してウィンドウの横幅を変化させると、ActualWidth の値も変化する。
ビヘイビアで IsActive をバインド可能にする
Window.IsActive もバインドできないが、これもバインド可能にしてみる。
IsActive は本来読み取り専用だが、ここでは書き込みも可能にして、true にされたらウィンドウをアクティブ化する、ということをしてみる。
基本的なやり方は ActualWidth の時と同じだが、ViewModel から書き込まれた時に処理を行うために、DependencyProperty.Register() で、書き込まれた時のイベントハンドラー SourceIsActiveChanged を指定している。
private static void SourceIsActiveChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args) { if ((obj is not WindowBindingSupportBehavior thisObject) || thisObject.AssociatedObject == null) { return; } if ((Boolean)args.NewValue) { thisObject.AssociatedObject.Activate(); } }
イベントハンドラーの引数 obj にビヘイビアクラスが DependencyObject 型として格納されているので、WindowBindingSupportBehavior 型にキャストして thisObject とする。
thisObject.AssociatedObject がウィンドウ自体なので、これでウィンドウに対する操作がやりたい放題できる。Activate() を呼べばウィンドウがアクティブ化される。
サンプルプログラムでは、チェックボックスでウィンドウのアクティブ状態を表示する。他のウィンドウをアクティブにすると、チェックが外される。
「5 秒後にアクティブ」ボタンをクリックすると、5 秒後にサンプルプログラムのウィンドウをアクティブ化する。ボタンクリック後、他のウィンドウをアクティブにして 5 秒待つと、サンプルプログラムがアクティブになる。