MVVMパターンでいうCommandクラスは、RelayCommandとかDelegateCommandとという名前で実装されるクラスが有名。
お?MSは、どうやら「RelayCommand」の方にするらしい。
RelayCommand クラス
↑のクラスは、VS2012以降から標準装備になる(?)ので、今は当然ない。
というわけで、他の記事にもあるように、Command用クラスを実装してみる。
using System;
using System.Windows.Input;
namespace TawamureDays {
/// <summary>
/// Command用クラス
/// </summary>
public sealed class RelayCommand : ICommand {
#region 内部変数
/// <summary>実行用メソッド</summary>
private Action<object> execute_ = null;
/// <summary>実行可否判定用メソッド</summary>
private Predicate<object> canExecute_ = null;
#endregion
#region コンストラクタ
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="execute">実行用メソッド</param>
/// <param name="canExecute">実行可否判定用メソッド</param>
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null) {
execute_ = execute;
canExecute_ = canExecute;
}
#endregion
#region ICommand メンバー
/// <summary>
/// 実行可能かどうかを取得します。
/// </summary>
/// <param name="parameter">コマンドパラメータ</param>
/// <returns>true:実行可能</returns>
public bool CanExecute(object parameter) {
return canExecute_ == null ? true : canExecute_(parameter);
}
/// <summary>変更可否判定値変更イベント</summary>
public event EventHandler CanExecuteChanged;
/// <summary>
/// 実行します。
/// </summary>
/// <param name="parameter">コマンドパラメータ</param>
public void Execute(object parameter) {
execute_(parameter);
return;
}
#endregion
}
}
大体こんな感じかな。ただし、仕事ではこれで大丈夫、なんて事はなかった。
浮上した問題点は、いくつかあった。
これらの問題の詳細は追記で。①可否判定値変更イベントを明示的に発生できない。
可否判定が働かないと、バインディングしているボタンの状態は変わってくれない。
②コンストラクタで渡すメソッド(ロジック)がそもそも問題があった。
③別スレッド内にて、このCommandを実行する時に問題が発生した。
①可否判定値変更イベントを明示的に発生できない。
コマンドとバインディングしたボタンの実行可否状態は、CanExecuteの戻り値で決まるんだけど、その可否判定値が変わったよというイベントが発生しないと、再評価してくれない。なので、明示的にイベントを発生させるメソッドが欲しくなった。
ただし、このメソッドにも些か問題があり、1つの画面でコマンドが5個以上になったりすると、
すべてのコマンドのこのメソッドを逐一呼び出す必要がある。呼び損ねると、実行可能に見えたりする。実装も大変、確認も大変、いわゆる「めんどくさい」なんだ。
で、↑のメソッドとイベントを以下のように手直しする。
CommandManagerについては、↓の記事で使われている。
Model-View-ViewModel デザイン パターンによる WPF アプリケーション
ただし、すべてのコマンドに対して、可否判定の再評価を促すので、何度も実行してしまうので注意する必要がある。そういう意味では、上のメソッドをstaticにするのもいいかもしれない。
②コンストラクタで渡すメソッド(ロジック)がそもそも問題があった。
コンストラクタで渡される、実行用メソッド、可否判定用メソッドは、大体がラムダ式で渡される。
この使ったラムダ式なんぞは、RelayCommand内部で参照される事になる。そして、RelayCommandが参照する限り、メモリを解放することはない。
要するに、
使ったらポイしないと、ずっとメモリ内に居座って、しまいにゃリークする。
って事なんだな。WPF4になって、IDisposableの影って薄くなっているけど、WPFだからメモリ管理してくれるなんてのはありえない。後始末は自分できっちりやる必要がある。
これを画面終了時(あるいは、VMのDispose内)で呼ぶ事で参照を消し、メモリ解放へとつなげる。
③別スレッド内にて、このCommandを実行する時に問題が発生した。
Windows Formsだろと、WPFだろうと、WindowsアプリはSTA(Single Thread Apartment)で実行される。UIコントロールへの操作が許されるスレッドは常に1つだ。
この「UI操作が許されるスレッド」上で実行するために必要なのが、Dispatcherオブジェクト。このDispatcherオブジェクトは、DispatcherObjectクラスを継承するコントロールは必ず持っている。このDispatcherを使って操作権限を得る必要がある。それは、MVVMパターンだろうと例外ではなく、VM側でUIに変化を促すプロパティ(IsEnabled等)を別スレッド上で実行するためには、このDiapatcherを使う必要がある。
しかし、ViewModel側からどうやってView側が持っているDispatcherをもらうのか?どうやって渡そうか?なんて悩んだ事もあったんだけど、しごく簡単に修得できてしまう事が、悩んだ後で分かったOrz。
Commandクラスが別スレッド上で実行される事を想定する必要があるなら、Executeメソッド内の実装が、↓の様に変わる。
これで別スレッド上で実行されてもOKになるかな。
ただし、このRelayCommandそのものの問題で、どうにも解決できなかったのが、連打による多重実行防止だった。DBテーブルに対する検索クエリ実行中は、ボタンを押せないようにしたかったんだけど、どうも、Execute実行中にCanExecuteメソッドを呼び出せないらしく(当たり前の話ではある)、Execute実行終了後に呼び出す。
なので、多重押しは、正しく並んで、正しく多重実行されてしまう。さて困った。
結果的には、このクラスとは異なるクラスを作ることで解決した。
(2012/10/10追記)
上記のCanExecuteChangedの実装は、WPF4.5では通用しない(不安定になる)らしい。
Commandの実装を変える必要が出た。
コマンドとバインディングしたボタンの実行可否状態は、CanExecuteの戻り値で決まるんだけど、その可否判定値が変わったよというイベントが発生しないと、再評価してくれない。なので、明示的にイベントを発生させるメソッドが欲しくなった。
/// <summary>
/// 変更可否判定値変更イベントを発生させます。
/// </summary>
public void RaiseCanExecuteChanged() {
if (CanExecuteChanged != null) {
((EventHandler)CanExecuteChanged)(this, EventArgs.Empty);
}
}
ただし、このメソッドにも些か問題があり、1つの画面でコマンドが5個以上になったりすると、
すべてのコマンドのこのメソッドを逐一呼び出す必要がある。呼び損ねると、実行可能に見えたりする。実装も大変、確認も大変、いわゆる「めんどくさい」なんだ。
で、↑のメソッドとイベントを以下のように手直しする。
/// <summary>実行可否変更イベント</summary>
public event System.EventHandler CanExecuteChanged {
add {CommandManager.RequerySuggested += value;}
remove {CommandManager.RequerySuggested -= value;}
}
/// <summary>
/// コマンドの実行可否状態が変更された事を知らせるイベントを発生させます。
/// </summary>
public void RaiseCanExecuteChanged() {
CommandManager.InvalidateRequerySuggested();
return;
}
CommandManagerについては、↓の記事で使われている。
Model-View-ViewModel デザイン パターンによる WPF アプリケーション
ただし、すべてのコマンドに対して、可否判定の再評価を促すので、何度も実行してしまうので注意する必要がある。そういう意味では、上のメソッドをstaticにするのもいいかもしれない。
②コンストラクタで渡すメソッド(ロジック)がそもそも問題があった。
コンストラクタで渡される、実行用メソッド、可否判定用メソッドは、大体がラムダ式で渡される。
var XXXXCommand = new RelayCommad(parameter => {}, parameter => {... return true;});
この使ったラムダ式なんぞは、RelayCommand内部で参照される事になる。そして、RelayCommandが参照する限り、メモリを解放することはない。
要するに、
使ったらポイしないと、ずっとメモリ内に居座って、しまいにゃリークする。
って事なんだな。WPF4になって、IDisposableの影って薄くなっているけど、WPFだからメモリ管理してくれるなんてのはありえない。後始末は自分できっちりやる必要がある。
/// <summary>
/// 内部データ(参照)をクリアします。
/// </summary>
public void Clear() {
canExecute_ = null;
execute_ = null;
return;
}
これを画面終了時(あるいは、VMのDispose内)で呼ぶ事で参照を消し、メモリ解放へとつなげる。
③別スレッド内にて、このCommandを実行する時に問題が発生した。
Windows Formsだろと、WPFだろうと、WindowsアプリはSTA(Single Thread Apartment)で実行される。UIコントロールへの操作が許されるスレッドは常に1つだ。
この「UI操作が許されるスレッド」上で実行するために必要なのが、Dispatcherオブジェクト。このDispatcherオブジェクトは、DispatcherObjectクラスを継承するコントロールは必ず持っている。このDispatcherを使って操作権限を得る必要がある。それは、MVVMパターンだろうと例外ではなく、VM側でUIに変化を促すプロパティ(IsEnabled等)を別スレッド上で実行するためには、このDiapatcherを使う必要がある。
しかし、ViewModel側からどうやってView側が持っているDispatcherをもらうのか?どうやって渡そうか?なんて悩んだ事もあったんだけど、しごく簡単に修得できてしまう事が、悩んだ後で分かったOrz。
using System;
using System.Windows;//Application用名前空間
using System.Windows.Threading;//Dispatcher用名前空間
using System.Windows.Input;
namespace TawamureDays {
/// <summary>
/// Command用クラス
/// </summary>
public sealed class RelayCommand : ICommand {
#region 内部変数
/// <summary>実行用メソッド</summary>
private Action<object> execute_ = null;
/// <summary>実行可否判定用メソッド</summary>
private Predicate<object> canExecute_ = null;
/// <summary>同期処理用ディスパッチャ</summary>
private Dispatcher dispatcher_;
#endregion
#region コンストラクタ
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="execute">実行用メソッド</param>
/// <param name="canExecute">実行可否判定用メソッド</param>
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null) {
execute_ = execute;
canExecute_ = canExecute;
dispatcher_ = Application.Current.Dispatcher;
}
#endregion
}
}
Commandクラスが別スレッド上で実行される事を想定する必要があるなら、Executeメソッド内の実装が、↓の様に変わる。
/// <summary>
/// 実行します。
/// </summary>
/// <param name="parameter">コマンドパラメータ</param>
public void Execute(object parameter) {
if (System.Threading.Thread.CurrentThread != dispatcher_.Thread) {
dispatcher_.Invoke(execute_, parameter);
} else {
execute_(parameter);
}
return;
}
これで別スレッド上で実行されてもOKになるかな。
ただし、このRelayCommandそのものの問題で、どうにも解決できなかったのが、連打による多重実行防止だった。DBテーブルに対する検索クエリ実行中は、ボタンを押せないようにしたかったんだけど、どうも、Execute実行中にCanExecuteメソッドを呼び出せないらしく(当たり前の話ではある)、Execute実行終了後に呼び出す。
なので、多重押しは、正しく並んで、正しく多重実行されてしまう。さて困った。
結果的には、このクラスとは異なるクラスを作ることで解決した。
(2012/10/10追記)
上記のCanExecuteChangedの実装は、WPF4.5では通用しない(不安定になる)らしい。
Commandの実装を変える必要が出た。
スポンサーサイト
当サイトは基本をすっ飛ばしてます。基本文法等は、@ITをどうぞ