MVVM

ActualWidth のバインド&取得と、ビヘイビアの紐解き

Windowビヘイビアについて少し考えてみた。

サンプルプログラムは GitHub に置いてある。Livet 使用。

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 をバインドして使えるようになる。

サンプルプログラムを起動してウィンドウの横幅を変化させると、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 秒待つと、サンプルプログラムがアクティブになる。


フォームと WPF でコードメトリックスを計算・比較してみた

MVVM
同等の機能を持つソフトでフレームワークが異なる場合、Visual Studio 2017 で計算できるコードメトリックスはどのくらい変わるのか、というのが気になったので、あまり意味はないけど測定してみた。

対象プロジェクトはゆかりすたー。バージョン間で細かな違いはもちろんあるものの、大まかに見れば機能的にはほとんど同等。結果は以下の通り。

フォーム
ゆかりすたー
Ver 7.51
WPF / コードビハインド
ゆかりすたー METEOR
Ver 1.50
WPF / MVVM
ゆかりすたー METEOR
Ver 2.02
保守容易性指数828486
サイクロマティック複雑度388839334480
継承の深さ799
クラス結合431466465
コード行1249896859333

保守容易性指数

公式解説によれば保守容易性指数の値が大きいほど保守性が高く、0~9 がダメ、10~19 がまぁまぁ、20~100 が OK とのこと。

いずれのフレームワークでも 80 以上なのでかなり良好と言えると思うが、WPF のほうが少し高い数値になっている。

ただ気になるのは、WPF の XAML 部分はあまり考慮されていないのではないかということ。WPF / MVVM で試しにメインウィンドウの XAML からコントロールをさくっと削り、メインウィンドウ部分の評価を比較したのが以下。

元の WPF / MVVMコントロール削除
保守容易性指数91
91
サイクロマティック複雑度11
継承の深さ99
クラス結合72
コード行22

保守容易性指数(というかクラス結合以外)に変化がない。XAML に複雑な表示ロジックをべた書きして、代わりに ViewModel からコードを減らしたら、保守容易性指数はあがってしまうのではないか。ちょっとインチキ臭い。XAML を考慮に入れると、フォームと WPF で保守容易性はほとんど変わらないと言えるかもしれない。

さて、総合的には保守容易性指数が 86 の WPF / MVVM でも、細かく見ていくと値が低い部分もある。最低は 0 で、ViewTFoundsWindowViewModel.cs の DataGridListSorting()。ソート基準項目によってひたすら switch するので低い評価になっている。

次に低いのが CsvOutputWriter.cs の Output() で 28。こちらも出力項目ごとに switch していて、switch の case が並ぶのは低い評価になりやすいのかもしれない。

サイクロマティック複雑度

Wikipedia を読むと、サイクロマティック複雑度(循環的複雑度)はプログラム実行時の経路数に関係する値で、大きいほど経路・分岐が多くなる。プログラムの動作を理解するのが大変になったり、テストケースが多くなって大変になったりするのだろう。

プログラムの規模が大きくなるにつれてサイクロマティック複雑度も大きくなっていく性質があるので、単純に数値だけを見て良い悪いは言えないが、相対的に WPF よりフォームの方が値が小さいので、フォームの方が理解しやすいという見方ができるのではないか。体感としても、フォームの方が簡単だと思う。

継承の深さ

使用しているクラスが、根源となるクラスである Object から何段階派生しているか。WPF のほうがクラス階層が深いが、最大の 9 となっているクラスはすべてウィンドウだった。WPF でウィンドウを使うと必ず 9 以上になるということからすれば、結果が 9 であるというのは少ない値と言えるだろう。

クラス結合

低い方が良い数値。フォームの方が低い。

コード行

ソースコードの行数ではなく、IL の数。ソースコードのコメント行や空行は数値に影響しないと思われるため、単純にソースコードの行数を数えるよりも実際的。

ソースコード行数より少ない数値となる傾向があり、例えば 最大値を記録した SyncClient はソースコード上は 2058 行だが、IL では 882 行と、半分以下になっている。

フレームワーク間で比較すると、WPF のほうがずっと少なくなっている。フレームワークに関係ないところで最適化してるところが(ソースコード行数ベースで)1000 行ほどあるが、それを差し引いても WPF のほうが少ない。

ただし、WPF は使用しているライブラリが多く、それらまで含めると、総合的なコード量はフォームのほうが少なくなるのではないだろうか。

まとめ

元々あまり意味のある比較とは思えないものの、傾向としては、フォームの方がシンプル・コンパクトということになるだろう。元々 WPF / MVVM はシンプルさで売ってないし……。


MVVM ダメ Tips

使ってはいけない Tips。コードは Livet 環境前提。

ViewModel から直接 View を参照してしまう

最早 MVVM ではないが、添付ビヘイビアとか作るの面倒くさい時に。
Initialize2() の引数に Window オブジェクトが渡される。

XAML 側コード

<i:EventTrigger EventName="ContentRendered">
    <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
    <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize2" MethodParameter="{Binding ElementName=MyWindow}"/>
</i:EventTrigger>

ViewModel 側コード

public void Initialize2(Object oParam)
{
    Debug.WriteLine("Initialize2() param type: " + oParam?.GetType().ToString());
}


カンパのお願い
Amazon でお買い物の際は、下記で検索して頂けたら幸いです。
記事検索
最新コメント
月別アーカイブ
  • ライブドアブログ