ダイアログ表示を行うビューモデル(その3)

ダイアログ表示を行うビューモデル(その2)の続きです。

前回ビヘイビア関係まで書いたので、今回はビューモデル関係です。
このライブラリを使ったアプリケーション例は次回書く予定です。

プロジェクトに「ViewModels」フォルダを作成します。
ViewModels フォルダに「IReceiveFinished」インターフェイスを作成します。

namespace MakCraft.ViewModels
{
    /// <summary>
    /// 画面遷移完了時の操作に用いるインターフェイスです。
    /// </summary>
    interface IReceiveFinished
    {
        /// <summary>
        /// 画面遷移操作完了時に実行されるメソッド
        /// </summary>
        void OnFinished(ITransContainer container);
    }
}

続けて「ITransContainer」インターフェイスを作成します。

namespace MakCraft.ViewModels
{
    /// <summary>
    /// 画面遷移の際のデータコンテナのインターフェイス
    /// </summary>
    public interface ITransContainer
    {
        /// <summary>
        /// 遷移を区別するためのキーを取得します。
        /// 一つのビューモデルで複数の画面遷移を持つ場合の処理の分岐用
        /// </summary>
        string Key { get; }

        /// <summary>
        /// 遷移動作の開始元ビューモデルを取得します。
        /// </summary>
        TransitionViewModelBase TransStartViewModel { get; }

        /// <summary>
        /// 前画面のビューモデルを取得・設定します。
        /// </summary>
        TransitionViewModelBase PreviousViewModel { get; set; }
    }
}

続けて「IWindowCloseCommand」インターフェイスを作成します。

namespace MakCraft.ViewModels
{
    /// <summary>
    /// ウィンドウを閉じるためのビューモデルのインターフェイスです。
    /// </summary>
    public interface IWindowCloseCommand
    {
        /// <summary>
        /// ウィンドウがクローズできる状態かを返します。
        /// </summary>
        bool CanCloseWindow { get; }

        /// <summary>
        /// ビューモデルからウィンドウへ Close を通知するメソッドです。
        /// </summary>
        void WindowClose();
    }
}

続けて「DialogViewModelBase」クラスを作成します。
データ検証とダイアログ表示の基本機能を提供するビューモデルの基底クラスです。

using System;
using System.Windows;

using MakCraft.Behaviors.Interfaces;

namespace MakCraft.ViewModels
{
    /// <summary>
    /// データ検証とダイアログ表示の基本機能を提供するビューモデルの基底クラスです。
    /// </summary>
    public abstract class DialogViewModelBase : ValidationViewModelBase, IDialogTransferContainer
    {
        public DialogViewModelBase() { }

        /// <summary>
        /// 作成する Dialog に渡すデータを取得・設定します。
        /// View 側で DialogTransferDataAction の Parameter にバインドしてください。
        /// </summary>
        public object CommunicationDialog { get; protected set; }

        /// <summary>
        /// 表示するカスタムダイアログボックスの型の情報
        /// View 側で DialogTransferDataAction の DialogType にバインドしてください。
        /// </summary>
        public Type DialogType { get; protected set; }

        #region IDialogTransferContainer
        private object _container;
        /// <summary>
        /// ウィンドウ作成元から渡されたデータの受取用
        /// </summary>
        public virtual object Container
        {
            get { return _container; }
            set
            {
                _container = value;
                OnContainerReceived(_container);
            }
        }
        #endregion IDialogTransferContainer

        #region DialogTransferContainer
        /// <summary>
        /// ダイアログが閉じられた後に実行するコールバックを取得・設定します。
        /// View 側で DialogTransferDataAction の ActionCallBack にバインドしてください。
        /// </summary>
        public Action<bool?> DialogActionCallback { get; protected set; }

        /// <summary>
        /// ダイアログ表示で生成されたダイアログのビューモデルへの参照を取得・設定します。
        /// (ダイアログで設定された値の参照用)
        /// View 側で DialogTransferDataAction の ResultViewModel にバインドしてください。
        /// </summary>
        public object ResultViewModel { get; set; }

        /// <summary>
        /// ウィンドウ作成元からのデータを受け取った際に行う処理
        /// </summary>
        /// <param name="container"></param>
        protected virtual void OnContainerReceived(object container)
        {
        }
        #endregion DialogTransferContainer

        #region MessageDialogAction
        /// <summary>
        /// MessageDialogActionに渡すパラメーター
        /// View 側で PropertyChangedTrigger の Binding と MessageDialogAction の Parameter にバインドしてください。
        /// </summary>
        public MessageDialogActionParameter MessageDialogActionParam { get; set; }

        /// <summary>
        /// MessageDialogActionの実行後に呼ばれるCallBack
        /// View 側で MessageDialogAction の ActionCallBack にバインドしてください。
        /// </summary>
        public Action<MessageBoxResult> MessageDialogActionCallback { get; set; }
        #endregion MessageDialogAction
    }
}

続けて「MessageDialogActionParameter」クラスを作成します。
MessageDialogAction へ渡すパラメーターのクラスです。

using System.Windows;

using MakCraft.Behaviors.Interfaces;

namespace MakCraft.ViewModels
{
    /// <summary>
    /// MessageDialogAction へ渡すパラメーター
    /// IsDialog が false のときには Button の設定は反映されません。
    /// </summary>
    public class MessageDialogActionParameter : IMessageDialogActionParameter
    {
        /// <summary>
        /// MessageBoxに表示するメッセージ
        /// </summary>
        public string Message { get; protected set; }
        /// <summary>
        /// MessageBox に表示するタイトル
        /// </summary>
        public string Caption { get; protected set; }
        /// <summary>
        /// MessageBox に表示するボタン
        /// </summary>
        public MessageBoxButton Button { get; protected set; }
        /// <summary>
        /// true:ダイアログ(ユーザ応答を処理する)、false:メッセージ
        /// </summary>
        public bool IsDialog { get; protected set; }

        /// <summary>
        /// MessageDialogActionへ 渡すパラメーター(メッセージ表示用)
        /// ボタン表示は OK のみ
        /// </summary>
        /// <param name="message"></param>
        /// <param name="caption"></param>
        public MessageDialogActionParameter(string message, string caption)
            : this(message, caption, MessageBoxButton.OK, false) { }
        /// <summary>
        /// MessageDialogActionへ 渡すパラメーター(ダイアログ表示用)
        /// </summary>
        /// <param name="message"></param>
        /// <param name="caption"></param>
        /// <param name="button"></param>
        public MessageDialogActionParameter(string message, string caption, MessageBoxButton button)
            : this(message, caption, button, true) { }
        /// <summary>
        /// MessageDialogActionへ 渡すパラメーター
        /// </summary>
        /// <param name="message"></param>
        /// <param name="caption"></param>
        /// <param name="button"></param>
        /// <param name="isDialog"></param>
        public MessageDialogActionParameter(string message, string caption, MessageBoxButton button, bool isDialog)
        {
            Message = message;
            Caption = caption;
            Button = button;
            IsDialog = isDialog;
        }
    }
}

続けて「ModalViewModelBase」クラスを作成します。
データ検証とモーダルダイアログ表示機能を持つビューモデルの基底クラスです。

using System.Windows.Input;

namespace MakCraft.ViewModels
{
    /// <summary>
    /// データ検証とモーダルダイアログ表示機能を持つビューモデルの基底クラスです。
    /// </summary>
    public abstract class ModalViewModelBase : DialogViewModelBase
    {
        public ModalViewModelBase() { }

        private bool? _result;
        /// <summary>
        /// View 側の DialogResult セット用の PropertyChangedTrigger へバインドします。
        /// </summary>
        public bool? Result
        {
            get { return _result; }
            set
            {
                _result = value;
                base.RaisePropertyChanged(() => Result);
            }
        }

        private void okExecute()
        {
            // Result プロパティに true をセットすることで Window のトリガが起動し、Window の DialogResult プロパティに値をセットする
            Result = true;
        }
        /// <summary>
        /// OkCommand の 有効/無効 を返します。
        /// データ検証エラーの有無を返します。データ検証エラーを用いないで判断したい場合はオーバーライドしてください。
        /// </summary>
        /// <param name="param"></param>
        /// <returns></returns>
        protected virtual bool OkCanExecute(object param)
        {
            return base.IsValid;
        }
        private ICommand _okCommand;
        /// <summary>
        /// OK ボタン用のコマンドです。
        /// ボタンの有効・無効をコントロールするため、コマンドで Window の DialogResult を設定します。
        /// </summary>
        public ICommand OkCommand
        {
            get
            {
                if (_okCommand == null)
                    _okCommand = new RelayCommand(okExecute, OkCanExecute);
                return _okCommand;
            }
        }
    }
}

続けて「TransitionContainerBase」クラスを作成します。
画面遷移の際のデータコンテナの基底クラスです。
abstract 指定をしていないので、画面遷移で引き渡すデータがない場合には(そのようなケースが有るか不明ですが)、このクラスをインスタンス化することもできます。

using System;

namespace MakCraft.ViewModels
{
    /// <summary>
    /// 画面遷移の際のデータコンテナの基底クラス
    /// </summary>
    public class TransitionContainerBase : ITransContainer
    {
        /// <summary>
        /// 画面遷移のキー及び遷移開始元ビューモデルを設定して画面遷移の際のデータコンテナを作成します。
        /// </summary>
        /// <param name="key"></param>
        /// <param name="viewModel"></param>
        public TransitionContainerBase(string key, TransitionViewModelBase viewModel)
        {
            if (viewModel == null) throw new ArgumentException(
                    "viewmodel に null は指定できません。");

            _key = key;
            _transStartViewmodel = viewModel;
            PreviousViewModel = viewModel;
        }

        #region ITransContainer
        private readonly string _key;
        /// <summary>
        /// 遷移を区別するためのキーを取得します。
        /// 一つのビューモデルで複数の画面遷移を持つ場合の処理の分岐用
        /// </summary>
        public string Key
        {
            get { return _key; }
        }

        private readonly TransitionViewModelBase _transStartViewmodel;
        /// <summary>
        /// 遷移動作の開始元ビューモデルを取得します。
        /// </summary>
        public TransitionViewModelBase TransStartViewModel
        {
            get { return _transStartViewmodel; }
        }

        /// <summary>
        /// 前画面のビューモデルを取得・設定します。
        /// </summary>
        public TransitionViewModelBase PreviousViewModel { get; set; }
        #endregion
    }
}

続けて「TransitionViewModelBase」クラスを作成します。
データ検証と画面遷移及び表示状態設定機能を持つビューモデルの基底クラスです。

using System;
using System.Windows.Input;

using MakCraft.Behaviors.Interfaces;

namespace MakCraft.ViewModels
{
    /// <summary>
    /// データ検証と画面遷移及び表示状態設定機能を持つビューモデルの基底クラスです。
    /// </summary>
    public abstract class TransitionViewModelBase : DialogViewModelBase, IWindowCloseCommand, IViewModelStatus, IReceiveFinished
    {
        public TransitionViewModelBase() { }

        private WindowAction _windowAction;
        /// <summary>
        /// ウィンドウの表示状態を取得・設定します。
        /// </summary>
        public WindowAction DisplayMode
        {
            get { return _windowAction; }
            protected set
            {
                _windowAction = value;
                base.RaisePropertyChanged(() => DisplayMode);
            }
        }

        /// <summary>
        /// 前画面のビューモデルを取得します。
        /// </summary>
        protected TransitionViewModelBase PreviousViewModel { get; private set; }

        /// <summary>
        /// ウィンドウ作成元からのデータ受け取り用プロパティ
        /// </summary>
        public override object Container
        {
            get
            {
                return base.Container;
            }
            set
            {
                var container = value as ITransContainer;
                if (container == null) throw new InvalidCastException(
                    "渡されたコンテナが ITransContainer インターフェイスを実装していません。");
                if (container.PreviousViewModel == null) throw new InvalidOperationException(
                    "渡されたコンテナに前画面のビューモデルが設定されていません。");

                // 前画面のビューモデルを保存
                PreviousViewModel = container.PreviousViewModel;

                base.Container = value;
            }
        }

        /// <summary>
        /// ウィンドウが閉じることの出来る状態かどうかを返します。
        /// 仮想メソッドは常に 'true' を返します。制御が必要な場合はオーバーライドしてください。
        /// </summary>
        /// <param name="param"></param>
        /// <returns></returns>
        protected virtual bool WindowCloseCanExecute(object param)
        {
            return true;
        }
        private ICommand _windowCloseCommand;
        /// <summary>
        /// ウィンドウを閉じるコマンド
        /// </summary>
        public ICommand WindowCloseCommand
        {
            get
            {
                if (_windowCloseCommand == null)
                    _windowCloseCommand = new RelayCommand(WindowClose, WindowCloseCanExecute);
                return _windowCloseCommand;
            }
        }

        /// <summary>
        /// ウィンドウがクローズされた際の操作
        /// </summary>
        protected virtual void OnWindowClosed()
        {
            if (Container != null || Container as ITransContainer == null)
            {
                var container = Container as ITransContainer;

                if (CurrentStatus == ViewModelStatus.Completed)
                {
                    if (container.TransStartViewModel != PreviousViewModel &&
                        PreviousViewModel != ViewModelUtility.GetMainWindowViewModel())
                    {   // 直前の画面が遷移の開始元でなくなおかつ 直前の画面が MainWindow でなければ閉じる
                        PreviousViewModel.DisplayMode = WindowAction.Close;
                    }
                    else
                    {   // 直前の画面が遷移の開始元 または 直前の画面が MainWindow のときは直前の画面を表示する
                        PreviousViewModel.DisplayMode = WindowAction.Show;
                        // 遷移の開始元の画面遷移完了時の処理をキック
                        container.TransStartViewModel.OnFinished(container);
                    }
                }
                else
                {
                    PreviousViewModel.DisplayMode = WindowAction.Show;
                }
            }
        }
        private ICommand _windowClosedCommand;
        /// <summary>
        /// ウィンドウがクローズされた際の操作コマンド
        /// ウィンドウの Closed イベントが発生した際に呼び出されるようにしてください。
        /// </summary>
        public ICommand WindowClosedCommand
        {
            get
            {
                if (_windowClosedCommand == null)
                    _windowClosedCommand = new RelayCommand(OnWindowClosed);
                return _windowClosedCommand;
            }
        }

        /// <summary>
        /// 一連の画面遷移の完了を設定します。
        /// </summary>
        protected virtual void TransitionComplete()
        {
            CurrentStatus = ViewModelStatus.Completed;
            WindowClose();
        }

        #region IWindowCloseCommand

        /// <summary>
        /// ウィンドウを閉じることが可能かを取得します。
        /// </summary>
        public bool CanCloseWindow
        {
            get { return WindowCloseCanExecute(null); }
        }

        /// <summary>
        /// ビューモデルからウィンドウへ Close を通知するメソッドです。
        /// </summary>
        public virtual void WindowClose()
        {
            // コンテナの前画面情報をクリア
            if (Container != null)
            {
                var container = Container as ITransContainer;
                container.PreviousViewModel = null;
            }
            // Window へ Close を通知する。
            DisplayMode = WindowAction.Close;
        }

        #endregion IWindowCloseCommand

        #region IViewModelStatus
        // ステータスの初期値を Halfway にしておく(Window 作成・表示は ModelessDialogTransferDataAction が行うため)
        // 状態変更時の操作は DisplayModeAction が行う
        private ViewModelStatus _modelStatus = ViewModelStatus.Halfway;
        /// <summary>
        /// ビューモデルの処理状態を取得・設定します。
        /// </summary>
        public ViewModelStatus CurrentStatus
        {
            get { return _modelStatus; }
            set { _modelStatus = value; }
        }
        #endregion IViewModelStatus

        #region IReceiveFinished
        /// <summary>
        /// 画面遷移完了時に実行する処理です。
        /// </summary>
        public virtual void OnFinished(ITransContainer container)
        {
        }
        #endregion IReceiveFinished
    }
}

続けて「ViewModelUtility」クラスを作成します。
ViewModel 関連のユーティリティクラスです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;

namespace MakCraft.ViewModels
{
    /// <summary>
    /// ViewModel 関連のユーティリティクラス
    /// </summary>
    public static class ViewModelUtility
    {
        /// <summary>
        /// MainWindow となっている Window の ViewModel を返します。
        /// </summary>
        /// <returns></returns>
        public static ViewModelBase GetMainWindowViewModel()
        {
            var viewModel = Application.Current.MainWindow.DataContext;
            var result = viewModel as ViewModelBase;
            if (result == null) throw new InvalidCastException(
                "MainWindow の ViewModel が ViewModelBase から派生していません。");
            return result;
        }

        /// <summary>
        /// 指定されたビューモデルのインスタンスの数を返します。
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static int Count(Type type)
        {
            if (!type.IsSubclassOf(typeof(ViewModelBase))) throw new ArgumentException(
                string.Format("引数の型が ViewModelBase の派生クラスになっていません(引数の型:{0})。", type.Name));
            var result = 0;
            targetViewModelDoAction(type, n => ++result);
            return result;
        }

        /// <summary>
        /// 指定されたビューモデルのインスタンスの一覧を返します。
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static IReadOnlyList<ViewModelBase> GetViewModels(Type type)
        {
            if (!type.IsSubclassOf(typeof(ViewModelBase))) throw new ArgumentException(
                string.Format("引数の型が ViewModelBase の派生クラスになっていません(引数の型:{0})。", type.Name));
            var result = new List<ViewModelBase>();
            targetViewModelDoAction(type, n => result.Add(n));
            return result;
        }

        /// <summary>
        /// 指定されたビューモデルのインスタンスの IWindowCloseCommand インターフェイス の
        /// WindowClose メソッドを実行します。
        /// </summary>
        /// <returns></returns>
        public static void CloseViewModels(Type type)
        {
            if (!type.IsSubclassOf(typeof(ViewModelBase))) throw new ArgumentException(
                string.Format("引数の型が ViewModelBase の派生クラスになっていません(引数の型:{0})。", type.Name));
            if (Count(type) == 0) return;

            var list = GetViewModels(type);
            if (list.First() as IWindowCloseCommand == null)
            {
                throw new InvalidCastException(
                "オブジェクトは IWindowCloseCommand インターフェイスを実装していません。: " + type.ToString());
            }

            // ウィンドウが閉じることのできる状態か確認
            if (!isReadyCloseWindows(list))
            {
                throw new WindowPendingProcessException(
                    string.Format("ViewModel:{0} が閉じることの出来る状態にありません。", type.Name));
            }
            // ウィンドウを閉じる
            foreach (var n in list)
            {
                (n as IWindowCloseCommand).WindowClose();
            }
        }

        /// <summary>
        /// すべてのウィンドウが閉じることが可能か確認します。
        /// </summary>
        public static bool IsReadyCloseAllWindows
        {
            get
            {
                var list = new List<ViewModelBase>();
                targetViewModelDoAction(typeof(ViewModelBase), n => list.Add(n), true);
                return isReadyCloseWindows(list);
            }
        }

        // リストで渡されたウィンドウが閉じることが可能か確認します。
        private static bool isReadyCloseWindows(IReadOnlyList<ViewModelBase> list)
        {
            var result = true;
            foreach (var n in list)
            {
                var vm = n as IWindowCloseCommand;
                if (vm == null) throw new InvalidCastException(
                    "ViewModel は IWindowCloseCommand インターフェイスを実装していません。: " + n.GetType().Name);
                if (!vm.CanCloseWindow)
                {
                    result = false;
                    break;
                }
            }
            return result;
        }

        // Application クラスで管理しているインスタンス化された Window のコレクションからビューモデルを取得して、action の処理を行います。
        private static void targetViewModelDoAction(Type type, Action<ViewModelBase> action, bool isSubClass = false)
        {
            foreach (var n in Application.Current.Windows)
            {
                var window = n as Window;
                if (window.DataContext == null) continue;

                var vmType = window.DataContext.GetType();
                var cond = isSubClass ? vmType == type || vmType.IsSubclassOf(typeof(ViewModelBase)) :
                                        vmType == type;

                if (cond)
                {
                    var viewModel = window.DataContext as ViewModelBase;
                    if (viewModel != null)
                    {
                        action(viewModel);
                    }
                }
            }
        }
    }
}

最後に「WindowPendingProcessException」クラスを作成します。
ウィンドウを閉じようとした際にビューモデルが処理途中等で閉じることができない場合にスローされる例外です。

using System;
using System.Runtime.Serialization;

namespace MakCraft.ViewModels
{
    /// <summary>
    /// ウィンドウを閉じようとした際にビューモデルが処理途中等で閉じることができない場合にスローされる例外
    /// </summary>
    [Serializable]
    public class WindowPendingProcessException : Exception
    {
        public WindowPendingProcessException() : base() { }
        public WindowPendingProcessException(string message) : base(message) { }
        public WindowPendingProcessException(string message, Exception inner) : base(message, inner) { }
        public WindowPendingProcessException(SerializationInfo info, StreamingContext context) { }
    }
}

以上でライブラリが出来ました。

アプリケーション例を含めた TransitionViewModelBase の ZIP ファイルを ViewModelBase のページからダウンロードできるようにしたので、ファイルがほしい方はそちらから入手してください。

次回は、このライブラリを使ったアプリケーション例を書く予定です。


ダイアログ表示を行うビューモデル ベース(インデックス)


2 thoughts on “ダイアログ表示を行うビューモデル(その3)

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です