前回までの内容
「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 版作ることになったとき楽が出来ます。
次回も引き続きビューモデルを作成する予定です。
関連記事
- Google App Engine + Silverlight でタスク管理アプリケーション開発(1) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(2) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(3) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(5) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(6) - present