読者です 読者をやめる 読者になる 読者になる

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

前回までの内容

前回ようやくビューモデルに着手しました。でも、ビューモデルのベースを用意したり、コマンドを実装したりといった、下準備がほとんど。肝心のビューモデルは、簡単なもの1つ実装しただけです。

なので、今回も引き続きビューモデルを実装していきます。

タスク一覧を表示するためのビューモデル作成

このビューモデルは、GAE/Python で実装したサービスを呼び出したり、表示用のデータを保持したり、データを加工したりします。サービスの呼び出しは、実際は Web API をラップしたクラスを利用しますけど。

using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;
using SilverTask.Models;

namespace SilverTask.ViewModels
{
    public class TasksViewModel : ViewModelBase
    {
        /// <summary>
        /// <see cref="TasksViewModel"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        public TasksViewModel()
            : this(new TaskRepository())
        {
        }

        /// <summary>
        /// <see cref="TasksViewModel"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <param name="repository">タスクが保存されているリポジトリ</param>
        public TasksViewModel(ITaskRepository repository)
        {
            this._repository = repository;
            this.Tasks = new ObservableCollection<TaskViewModel>();
        }

        /// <summary>
        /// タスクが保存されているリポジトリ
        /// </summary>
        private readonly ITaskRepository _repository;

        /// <summary>
        /// タスクの一覧を取得します。
        /// </summary>
        public ObservableCollection<TaskViewModel> Tasks { get; private set; }

        /// <summary>
        /// 完了済みのタスクを表示しているかどうか。
        /// </summary>
        private bool _isShowCompleted = false;

        /// <summary>
        /// 完了済みのタスクを表示しているかどうか示す値を取得または設定します。
        /// </summary>
        public bool IsShowCompleted
        {
            get { return _isShowCompleted; }
            set
            {
                if (_isShowCompleted != value)
                {
                    _isShowCompleted = value;
                    OnPropertyChanged("IsShowCompleted");
                }
            }
        }

        /// <summary>
        /// 新しいタスクを追加します。
        /// </summary>
        /// <returns>新しいタスク。</returns>
        public TaskViewModel NewTask()
        {
            var newTask = new TaskViewModel()
            {
                IsEditing = true,
                IsNew = true,
            };
            Tasks.Insert(0, newTask);
            return newTask;
        }

        private ICommand _loadCommand = null;

        public ICommand LoadCommand
        {
            get
            {
                return _loadCommand = _loadCommand ??
                    new DelegateCommand(_ =>
                    {
                        LoadTasks();
                    });
            }
        }

        /// <summary>
        /// タスクを取得します。
        /// </summary>
        public void LoadTasks()
        {
            this._repository.ReadTask(IsShowCompleted, result =>
            {
                if (result.Error != null)
                {
                    ViewModelMessageBox.Show(result.Error.Message);
                    return;
                }

                App.CurrentDispatcher.BeginInvoke(() =>
                {
                    this.Tasks.Clear();
                    foreach (var task in result.Tasks)
                    {
                        this.Tasks.Add(new TaskViewModel(task));
                    }
                });
            });
        }

        /// <summary>
        /// 指定したタスクを完了します。
        /// </summary>
        /// <param name="task">完了するタスク。</param>
        public void ComplateTask(TaskViewModel task)
        {
            var t = task.Unwrap();
            t.Done = true;
            _repository.UpdateTask(t, result =>
            {
                if (result.Error != null)
                {
                    ViewModelMessageBox.Show(result.Error.Message);
                    return;
                }
                LoadTasks();
            });
        }

        /// <summary>
        /// 指定したタスクを未完了します。
        /// </summary>
        /// <param name="task">未完了にするタスク。</param>
        public void UncomplateTask(TaskViewModel task)
        {
            var t = task.Unwrap();
            t.Done = false;
            _repository.UpdateTask(t, result =>
            {
                if (result.Error != null)
                {
                    ViewModelMessageBox.Show(result.Error.Message);
                    return;
                }
                LoadTasks();
            });
        }

        /// <summary>
        /// タスクを作成します。
        /// </summary>
        /// <param name="task">作成するタスク。</param>
        public void CreateTask(TaskViewModel task)
        {
            _repository.CreateTask(task.Unwrap(), result =>
            {
                if (result.Error != null)
                {
                    ViewModelMessageBox.Show(result.Error.Message);
                    return;
                }
                LoadTasks();
            });
        }

        /// <summary>
        /// 指定したタスクを更新します。
        /// </summary>
        /// <param name="task">更新するタスク。</param>
        public void UpdateTask(TaskViewModel task)
        {
            _repository.UpdateTask(task.Unwrap(), result =>
            {
                if (result.Error != null)
                {
                    ViewModelMessageBox.Show(result.Error.Message);
                    return;
                }
                LoadTasks();
            });
        }

        /// <summary>
        /// 指定したタスクを削除します。
        /// </summary>
        /// <param name="task">削除するタスク。</param>
        public void DeleteTask(TaskViewModel task)
        {
            _repository.DeleteTask(task.Unwrap(), result =>
            {
                if (result.Error != null)
                {
                    ViewModelMessageBox.Show(result.Error.Message);
                    return;
                }
                LoadTasks();
            });
        }
    }
}

エラー発生時にメッセージボックスを表示するようになっていますが、メッセージボックスではなく、エラー通知コントロールを使うべきだったかも。

サービス呼び出しで使うインタフェースをコンストラクタで受け取るようにしているので、モックと差し替えれば、サービスが無くてもテストができます。ある程度完成するまでは、モックを使って動作を確認するのがお勧めです。実際にサービスを呼び出すのは仕上げ段階にした方が、開発効率がいいと思います。

メインページ用ビューモデル作成

namespace SilverTask.ViewModels
{
    public class MainViewModel : ViewModelBase
    {
        /// <summary>
        /// <see cref="MainViewModel"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        public MainViewModel()
        {
            this.Tasks = new TasksViewModel();
        }

        /// <summary>
        /// タスク一覧を取得します。
        /// </summary>
        public TasksViewModel Tasks { get; private set; }
    }
}

メインページのビューモデルは、さっき実装した TasksViewModel を保持するだけです。ただそれだけ。この程度なら無くてもいいんですけど、将来、リスト機能やタグ機能を追加したいので用意しました。このクラスはビューモデルのまとめ役になります。

コンバーターを作成

ビューモデルをビューにバインドするときに使うコンバーターを実装します。

まずは、bool 型を Visibility 型に変換するコンバーター。

using System;
using System.Windows;
using System.Windows.Data;
using System.Globalization;

namespace SilverTask.Controls
{
    /// <summary>
    /// <see cref="Boolean"/><see cref="Visibility"/> に変換します。
    /// </summary>
    public class BooleanVisibilityConverter : IValueConverter
    {
        /// <summary>
        /// 否定かどうか示す値を取得または設定します。
        /// </summary>
        public bool Not { get; set; }

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool)
            {
                if (Not)
                {
                    return (bool)value ? Visibility.Collapsed : Visibility.Visible;
                }
                return (bool)value ? Visibility.Visible : Visibility.Collapsed;
            }
            return value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is Visibility)
            {
                if (Not)
                {
                    return (Visibility)value == Visibility.Collapsed;
                }
                return (Visibility)value == Visibility.Visible;
            }
            return value;
        }
    }
}

「ビューモデルのあるプロパティの値が false のとき、コントロールを非表示にする」という処理はよくやるので、たいていのプロジェクトで作る事になるコンバーターだと思います。.NET Framework に含まれていればいいのに。もしかして、含まれてる?それとも他にいい方法があったりして…。

bool 型のプロパティの否定値をバインドしたい、という場面も結構あるので、否定用のコンバーターも作成します。

using System;
using System.Windows.Data;
using System.Globalization;

namespace SilverTask.Controls
{
    /// <summary>
    /// <see cref="Boolean"/> の否定の値に変換します。
    /// </summary>
    public class NotConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool)
            {
                return !(bool)value;
            }
            return value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value is bool)
            {
                return !(bool)value;
            }
            return value;
        }
    }
}

今回はここまで

変更内容の詳細は下記のページで確認できます。

これでビューモデルまで完成しました。残すはビューのみ。ようやく XAML を書きます。なお、デザインセンスには全く自身がないので、超シンプルな画面にする予定です。