WPFでコードビハインドによるアプリ開発の私的ガイドライン

コンピュータ

こちらの記事は、筆者の私的な WPF アプリ作成におけるガイドラインです。
コードビハインドでの開発を前提としており、
筆者の価値観をベースにした、独断と偏見に満ちた内容となっています。

WPF は本来、大規模開発を想定したフレームワークであり、
MVVM などの設計パターンを採用するケースが多く見られます。

本記事では、そのような一般的な使い方とは異なり、
WPF を用いて 小規模かつ個人用途(筆者自身)のツール系アプリ を
効率よく作成することを目的としています。

基本方針

  • 簡単に作れること
    (難しいことはしない)
  • コードの見通しが良いこと
  • コード量は少なめに
  • 一般的な手法にとらわれない
  • 設計パターンは、リターンが見合う場合にのみ採用する
  • 学習コストは惜しまない
    (学ぶが、使わないという選択もあり)

コンストラクタは非同期処理に出来ない

C# では、コンストラクタを async にすることはできません。
そのため、非同期処理を伴う初期化をコンストラクタ内に直接書くことは避けることになります。

無理にコンストラクタ内で重い処理や I/O を行うと、
UI スレッドをブロックしたり、例外処理が分かりづらくなったりする原因になります。

非同期の初期化が必要な場合は、

Loaded などのライフサイクルイベントで初期化する

といった形で、非同期処理を呼び出せるタイミングを分離するのが望ましい。

コンストラクタは、
「フィールドの初期化」や「依存関係の受け取り」など、
同期で完結する最低限の処理に留めるのが基本方針となります。


コンストラクタにすべての処理を書かない

デリゲートやラムダ式を使えば、
コンストラクタ内にそのウィンドウで行う処理を
すべて記述することも技術的には可能です。

public MainWindow()
{
    InitializeComponent();

    Loaded += (_, _) => { /* 処理 */ };
    Button.Click += (_, _) => { /* 処理 */ };
}

筆者もよく使う手法ではありますが、
素早くアプリのプロトタイプを作成したり、
機能の動作検証用のサンプルプログラムを作成したりする場合には、
コードが一か所にまとまり、コード量も少なくなるため、
短時間で実装できるというメリットがあります。

一方で、本格的なアプリとして開発を進める場合には、
あまりお勧めできません。
コンストラクタが肥大化しやすいことに加え、
処理の流れがほぼ手続き型になってしまい、
せっかくクラスとして設計している意味が薄れてしまいます。

また、Window クラスでは問題が表面化しにくいものの、
この書き方では外部からプロパティやメソッドにアクセスできない構造になりがちです。
その結果、再利用性や拡張性が低下するため、
常用する手法としてはお勧めできないと考えています。


同期処理または非同期処理に統一する

同じ機能について、
同期版と非同期版のメソッドを両方用意する という設計は、
あまりお勧めできません。

どちらを使うべきか迷いやすくなるだけでなく、
内部の処理内容が少しずつずれてしまい、
結果として保守が難しくなることが多いためです。

現在の GUI アプリケーションでは、
処理中に UI をブロックしないことが重要になるため、
基本的には非同期処理を前提に設計する 方が無難です。

同期処理について

同期処理でも UI が固まらない程度の処理になるよう心がけましょう。
GUI アプリの基本です。

非同期処理について

同期処理では UI が固まる可能性がある場合に、非同期処理の出番になります。
ただし、処理時間が分単位になる可能性がある場合は、
GUI アプリではなく CUI アプリにするか、
GUI アプリで進めるとしても、ワーカーなどを用いた
バッチ処理の仕組みを設けることをお勧めします。


XAML のコントロールには名前を付けましょう。

名前を付けることで、コードビハインド側から
コントロールを直接操作できるようになります。
これがないと、コードビハインドでのプログラミングは始まりません。

逆に言えば、名前を付けないコントロールは、
「コードビハインドから操作しない」
(※あくまで目安であり、将来変更される可能性はありますが)
という意思表示にもなります。


イベントの呼び出しは XAML 側に書かない

イベントハンドラを XAML 側に記述すると、
対応するメソッドがコードビハインド側に存在しない場合、
コンパイルが通らなくなります。

イベント登録をコードビハインド側で行えば、
メソッドの有無を含めてコード上で管理でき、
ビルドエラーの原因も追いやすくなります。


XAML は構造と見た目を記述する。

XAML は構造と見た目を記述します。
振る舞いはコードビハインドで行いましょう。

XAML に振る舞いまで持たせると、
コードビハインドと役割が重複し、
結果として設計が複雑化しやすくなります。


データバインディングについて

ItemsControl 系のコントロールは、データバインディングを使用します。

それ以外のコントロールについては、
基本的にデータバインディングは使用しません。

一覧表示や繰り返し表示はバインディングと相性が良く、
コードビハインドで個別に管理するよりも
構造が分かりやすくなります。

一方で、単一コントロールへの値設定や状態変更は、
コードビハインドで明示的に操作した方が、
処理の流れを追いやすくなります。


Dispose()に対応する。

オブジェクトの終了処理を明示的に行いたい場合は、IDisposable を実装するとよいでしょう。

WPF では、Window や UserControl に対して Closed イベントなどで後処理を行うケースも多いが、
親コントロールの終了に合わせて、配下の子オブジェクトの終了処理を確実に実行したい
という場面も少なくない。

そのような場合、親コントロール側の終了イベントで、
子コントロールが IDisposable を実装していれば Dispose() を呼び出すようにしておくと、
子コントロール固有の終了処理を確実に実行できる。

機能の拡張は Helper メソッドで行う

アタッチドプロパティやビヘイビアは、
コントロールの機能を拡張するための仕組みですが、
基本的には XAML から利用することを前提 としています。

そのため、コードビハインドから直接操作する用途には、
あまり向いていません。

コードビハインドを前提とする場合は、
コントロールを引数に取る Helper メソッド を用意し、
そこで機能を拡張するようにしましょう。

Helper メソッドの利点

Helper メソッドは、
WPF のコントロールを引数に取る static メソッドです。

この方法の何が良いかというと、
コードをそのまま再利用できる点 にあります。

例えば、ファイルのドラッグ&ドロップ機能は、
受け入れ側の処理はほぼ定型であり、
実際の処理内容をデリゲートとして受け取ることで、
汎用的な Helper メソッドとして実装できます。

このような Helper メソッドを積み重ねていくことで、
自分専用のライブラリが育っていきます。
アプリを作れば作るほどライブラリ資産が増え、
結果として、アプリ側で記述するコード量を減らすことができます。

もちろん、アタッチドプロパティやビヘイビアでも
同様の再利用効果は得られますが、
それらは「作ったものをそのまま使う」形になりがちです。

一方、Helper メソッドは単なる C# のメソッドであるため、
用途に応じて処理を追加・変更しやすく、
後から柔軟に拡張できる点が大きな利点です。

さいごに

これはガイドラインは厳密に守るものではないし、筆者もガイドラインに当てはまらないプログラムをよく書きます。
状況に応じたプログラミングが最優先だと考えています。
また、ガイドラインの考え方自体、言語機能やライブラリのアップデートで変化していくものなので、柔軟な対応が必要だと思います。

コメント