Google App Engine + Silverlight でタスク管理アプリケーション開発(4)

前回までの内容

「GAE + Silverlight でアプリケーション作るってばよ!」な本連載も4回目。前回で MVVM パターンで使うモデルと、GAE/Python のサービスを呼び出すクラスを実装しました。

今回はいよいよビューモデルに着手します。といっても、まずは下準備があるんですけど。

コマンドを実装

WPF だと CommandManager の RequerySuggested イベントを使って、ボタンの有効・無効の切り替えをまとめて記述できます。Silverlight でも同じことがしたいんですが、CommandManager が提供されていないので、仕方なく自作します。

using System;

namespace SilverTask.ViewModels
{
    /// <summary>
    /// コマンドを管理します。
    /// </summary>
    public static class CommandManager
    {
        public static event EventHandler RequerySuggested;

        /// <summary>
        /// <see cref="RequerySuggested"/> イベントを発生させます。
        /// </summary>
        public static void FireRequerySuggested()
        {
            var handler = RequerySuggested;
            if (handler != null)
            {
                handler(null, EventArgs.Empty);
            }
        }
    }
}

自作 CommanManager の RequestSuggested イベントは自動で発生しないので、手動で発生させる必要あり。

次に、MVVM パターンでよく使われる DelegateCommand を実装します。

using System;
using System.Windows.Input;

namespace SilverTask.ViewModels
{
    public class DelegateCommand : ICommand
    {
        private readonly Action<object> execute;
        private readonly Func<object, bool> canExecute;

        public DelegateCommand(Action<object> execute)
            : this(execute, _ => true)
        {
        }

        public DelegateCommand(Action<object> execute, Func<object, bool> canExecute)
        {
            this.execute = execute;
            this.canExecute = canExecute;
        }

        public void Execute(object param)
        {
            this.execute(param);
        }

        public bool CanExecute(object param)
        {
            return this.canExecute(param);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }
    }
}

CommandManager を自作する際に WPF のものとインタフェースを揃えたので、この DelegateCommand のコードは WPF でも使えます。

ビューモデルの基底クラスを実装

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;

namespace SilverTask.ViewModels
{
    /// <summary>
    /// ビューモデルの基底クラス。
    /// </summary>
    public abstract class ViewModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
    {
        /// <summary>
        /// <see cref="ViewModelBase"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        protected ViewModelBase()
        {
        }

        /// <summary>
        /// プロパティが変更されたときに発生します。
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// <see cref="PropertyChanged"/> イベントを発生させます。
        /// </summary>
        /// <param name="propertyName">プロパティ名。</param>
        protected void OnPropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }

            // Button の状態を変更する
            CommandManager.FireRequerySuggested();
        }

        /// <summary>
        /// 検証エラーを格納します。
        /// </summary>
        private readonly Dictionary<string, string> _errors = new Dictionary<string, string>();

        /// <summary>
        /// オブジェクトに検証エラーがあるかどうかを示す値を取得します。
        /// </summary>
        public bool HasErrors
        {
            get
            {
                return 0 < _errors.Count;
            }
        }

        /// <summary>
        /// プロパティまたはオブジェクト全体の検証エラーが変更されたときに発生します。
        /// </summary>
        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        /// <summary>
        /// 指定されたプロパティまたはオブジェクト全体の検証エラーを取得します。
        /// </summary>
        /// <param name="propertyName">
        /// 検証エラーを取得する対象のプロパティの名前。オブジェクト全体のエラーを取得する場合は null
        /// または System.String.Empty。
        /// </param>
        /// <returns>プロパティまたはオブジェクトの検証エラー</returns>
        public IEnumerable GetErrors(string propertyName)
        {
            return _errors.Values;
        }

        /// <summary>
        /// プロパティに設定する値を検証します。
        /// </summary>
        /// <typeparam name="T">プロパティの型。</typeparam>
        /// <param name="propertyName">プロパティ名。</param>
        /// <param name="value">設定する値。</param>
        protected void Validate<T>(string propertyName, T value)
        {
            try
            {
                Validator.ValidateProperty(value, new ValidationContext(this, null, null)
                {
                    MemberName = propertyName,
                });

                if (_errors.ContainsKey(propertyName))
                {
                    _errors.Remove(propertyName);
                    OnErrorsChanged(propertyName);
                }
            }
            catch (ValidationException ex)
            {
                _errors[propertyName] = ex.Message;
                OnErrorsChanged(propertyName);
                throw;
            }
        }

        /// <summary>
        /// <see cref="ErrorsChanged"/> イベントを発生させます。
        /// </summary>
        /// <param name="propertyName">プロパティ名。</param>
        private void OnErrorsChanged(string propertyName)
        {
            var h = ErrorsChanged;
            if (h != null)
            {
                h(this, new DataErrorsChangedEventArgs(propertyName));
            }
        }
    }
}

ビューモデルはすべてこのクラスを継承します。基底クラスはプロパティの変更通知と検証を提供。MVVM で定番の方法です。

なお、プロパティの検証には System.ComponentModel.DataAnnotations を利用しています。属性でプロパティを検証できるってすばらしい。

ボタンの有効・無効の切り替えを行うタイミングは、ビューモデルのプロパティが変更されたときが丁度いいので、PropertyChanged イベント発生後すぐに CommandManager の RequerySuggested を発生させています。

ビューモデル内でメッセージを表示するためのクラスを実装

MVVM パターンで困るのが、メッセージボックスやダイアログを表示する方法です。ビューモデルにこれらを表示する処理を直書きすると、単体テストが出来なくなります。メッセージボックスやダイアログなんて表示しない、という人もいるかもしれませんが。。。

そこで、メッセージボックスやダイアログを表示するためのクラスを用意します。

using System;
using System.Windows;

namespace SilverTask.ViewModels
{
    public static class ViewModelMessageBox
    {
        internal static Func<string, bool?> _Show = PrivateShow;

        public static bool? Show(string message)
        {
            return _Show(message);
        }

        private static bool? PrivateShow(string message)
        {
            bool? result = null;
            if (MessageBox.Show(message) == MessageBoxResult.OK)
            {
                result = true;
            }
            return result;
        }
    }
}

ビューモデルでメッセージボックスやダイアログを表示する方法の考察は、次の記事に書いています。

タスクをラップするビューモデルを作成

モデルを画面に表示するための、専用のビューモデルを実装します。プロパティ変更イベントの発生や値の検証はここで行います。

using Core.Models;
using System.ComponentModel.DataAnnotations;

namespace SilverTask.ViewModels
{
    public class TaskViewModel : ViewModelBase
    {
        /// <summary>
        /// タスク
        /// </summary>
        private readonly Task _model;

        public TaskViewModel()
            : this(new Task())
        {
            IsNew = true;
        }

        public TaskViewModel(Task model)
        {
            _model = model;
        }

        /// <summary>
        /// タスクの名前を取得または設定します。
        /// </summary>
        [Required(AllowEmptyStrings = false, ErrorMessage = "タスクの名前を記入して下さい。")]
        public string Name
        {
            get { return _model.Name; }
            set
            {
                Validate("Name", value);
                if (_model.Name != value)
                {
                    _model.Name = value;
                    OnPropertyChanged("Name");
                }
            }
        }

        /// <summary>
        /// 完了かどうか示す値を取得または設定します。
        /// </summary>
        public bool Done
        {
            get { return _model.Done; }
            set
            {
                if (_model.Done != value)
                {
                    _model.Done = value;
                    OnPropertyChanged("Done");
                }
            }
        }

        /// <summary>
        /// 編集中かどうか
        /// </summary>
        private bool _isEditing = false;

        /// <summary>
        /// 編集中かどうか示す値を取得または設定します。
        /// </summary>
        public bool IsEditing
        {
            get { return _isEditing; }
            set
            {
                if (_isEditing != value)
                {
                    _isEditing = value;
                    OnPropertyChanged("IsEditing");
                }
            }
        }

        /// <summary>
        /// 新規タスクかどうか
        /// </summary>
        private bool _isNew = false;

        /// <summary>
        /// 新規タスクかどうか示す値を取得または設定します。
        /// </summary>
        public bool IsNew
        {
            get { return _isNew; }
            set
            {
                if (_isNew != value)
                {
                    _isNew = value;
                    OnPropertyChanged("IsNew");
                }
            }
        }

        public Task Unwrap()
        {
            return _model;
        }
    }
}

今回はここまで

今回の変更内容の詳細は下記ページを参照して下さい。

CommandManager や ViewModelBase の実装など、下準備に大半を費やしました。Silverlight 固有の機能を避けて実装しているので、コードを WPF のプロジェクトにコピーしてもビルドできるはず。

Silverlight で開発するとき、WPF のことも考えて実装しておくと、後々 WPF 版作ることになったとき楽が出来ます。

次回も引き続きビューモデルを作成する予定です。