ViewMakerにみるMVVMの実装(1)
現在作成しているViewMakerもようやくベータ版(機能FIX)をリリースできそうになってきました。
ベータ版をリリースするにあたり作成したコードについて内容を再確認しています。それに合わせて主要な箇所を説明していきたいと思います。まず最初に、ViewModelのベースクラスを説明したいと思います。
ViewModelは基本のINotifyPropertyChangedとViewにコマンド通知するためのイベントを定義したIViewCommandを実装しています。機能的にはICommandを作成したり属性ベースの入力検証するためのヘルパーメソッドをを用意しています。その他のポイントとしては以下のような点です。
できるだけ簡単な仕組みで実装
IViewCommandはMessangerパターンよりも簡易な単なるイベントでCallback機能などはありません。できるだけPlainなクラスにする意図です。
/// Viewへの通知イベントを提供します public interface IViewCommand { event EventHandler<ViewCommandEventArgs> ViewCommandNotified; }
RelayCommandのExecuteをテンプレートメソッドで実装
定番のRelayCommandの実装はViewModelと緊密に連携しExecuteメソッドをテンプレートメソッドで実装しています。該当コマンドの実行前後でViewに対して開始・終了のコマンド(イベント)を発生させることによって、カーソルの砂時計やエラー時のメッセージ通知をView側で処理できるようにしています。
namespace ViewMaker.Core { /// リレーコマンド public class RelayCommand : ICommand { ... /// <summary> /// コマンドを実行する /// </summary> /// <param name="parameter">パラメータ</param> public void Execute(object parameter) { Exception exc = null; try { if (_vm != null) _vm.OnCommandExecuting(this, parameter); _action(parameter); } catch (Exception ex) { exc = ex; } finally { if (_vm != null) _vm.OnCommandExecuted(this, exc); } } } }
メッセージやダイアログ表示は拡張メソッドで実装
ViewModelに含めるには責務的に抵抗のあるメッセージ表示やファイルオープンなどのヘルパーメソッドはViewModelの拡張メソッドにしています。
public static class ViewModelExtension { /// OK・Cancel確認用メッセージボックスを表示する。 public static bool ShowOKCancel(this ViewModel vm, string message, string title = "Confirm") { var info = new ShowMessageViewCommandInfo { Title = title, Message = message, Style = MessageBoxStyle.OkCancel }; vm.ExecuteViewCommand(ViewCommands.ShowMessage, info); return info.Result.Value; } .... }
ViewModelの実装コード
namespace ViewMaker { /// ビューモデルのベースクラス public class ViewModel : INotifyPropertyChanged, IViewCommand { #region INotifyPropertyChanged メンバー /// プロパティ変更通知イベント public event PropertyChangedEventHandler PropertyChanged; /// プロパティ変更通知イベントを発生させる。 protected void OnPropertyChanged(string name) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name)); } #endregion #region IViewCommand メンバー /// Viewコマンド通知イベント public event EventHandler<ViewCommandEventArgs> ViewCommandNotified; /// コマンド実行前に動作する共通処理。 protected internal virtual void OnCommandExecuting(ICommand command, object parameter) { if (ViewCommandNotified != null) ViewCommandNotified(this, new ViewCommandEventArgs { Command = ViewCommands.BeginCommand, Parameter = parameter }); } /// コマンド実行後に動作する共通処理。 protected internal virtual void OnCommandExecuted(ICommand command, Exception error) { if (ViewCommandNotified != null) ViewCommandNotified(this, new ViewCommandEventArgs { Command = ViewCommands.EndCommand, Parameter = error }); } #endregion #region Command /// コマンドを作成する。 protected RelayCommand CreateCommand(Action<object> action, Predicate<object> canExecute = null) { return new RelayCommand(this, action, canExecute); } /// コマンドを作成する。 protected RelayCommand CreateCommand(Action action, Predicate<object> canExecute = null) { return new RelayCommand(this, action); } /// Viewコマンド通知を行うコマンドを作成する protected RelayCommand CreateViewCommand(string command, object parameter = null) { return CreateCommand(() => ExecuteViewCommand(command, parameter)); } /// Viewコマンド通知を実行する。 public void ExecuteViewCommand(string command, object parameter = null) { if (ViewCommandNotified != null) ViewCommandNotified(this, new ViewCommandEventArgs { Command = command, Parameter = parameter }); } #endregion #region Validation /// 入力検証を実行する。 public bool Validate(string areaName = null) { var info = new ValidateViewCommandInfo { AreaName = areaName }; ExecuteViewCommand(ViewCommands.Validate, info); return info.IsValid; } /// 指定したプロパティの入力検証を実行してエラー情報を取得する。 /// 検証ルールは該当プロパティの属性でする。 protected ValidationException GetPropertyError(string propName, object value) { var prop = GetType().GetProperty(propName); var result = new List<ValidationResult>(); var context = new ValidationContext(this, null, null); context.MemberName = propName; if (Validator.TryValidateValue(value, context, result, prop.GetCustomAttributes(typeof(ValidationAttribute), true).Cast<ValidationAttribute>())) return null; return new ValidationException(result.First(), null, value); } /// 指定したプロパティの入力検証を実行する。エラーが発生した場合はValidationExceptionが発生する。 /// 検証ルールは該当プロパティの属性でする。 protected void ValidateProperty(string propName, object value) { var err = GetPropertyError(propName, value); if (err != null) throw err; } #endregion } }