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
    }
}