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

前回までの内容

GAE + Silverlight でアプリケーションを開発する本連載も終盤です。ビューモデルの実装が前回で終わり、残すはビューのみ。

いきなり完成画面

文章やコードをずらずら並べただけじゃ味気ないので。

完成画面のスクリーンショットがこちら。
f:id:griefworker:20100826071556j:image
シンプルですね。

ちょっとしたこだわりとして、その場編集機能を付けます。
f:id:griefworker:20100826071557j:image
なんとなくリッチですね。

それではビューを実装していきます。

タスクを表示するビューを作成

<UserControl x:Class="SilverTask.Views.TaskView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:SilverTask.Controls"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    <UserControl.Resources>
        <local:BooleanVisibilityConverter x:Key="_BooleanVisibilityConverter"/>
        <local:BooleanVisibilityConverter x:Key="_NotBooleanVisibilityConverter"
                                          Not="True"/>
        <local:NotConverter x:Key="_NotConverter"/>
    </UserControl.Resources>
    
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <!--通常表示する画面-->
        <Grid Visibility="{Binding Path=IsEditing, Converter={StaticResource _NotBooleanVisibilityConverter}}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <TextBlock Text="{Binding Path=Name}"
                               Grid.Row="0"/>
            <StackPanel Orientation="Horizontal"
                                Grid.Row="1">
                <HyperlinkButton Content="編集"
                                 DataContext="{Binding}"
                                 IsTabStop="False"
                                 Visibility="{Binding Path=Done,Converter={StaticResource _NotBooleanVisibilityConverter}}"
                                 ToolTipService.ToolTip="タスクを編集します。"
                                 Click="EditButtonClick"/>
                <HyperlinkButton Content="完了"
                                 DataContext="{Binding}"
                                 IsTabStop="False"
                                 ToolTipService.ToolTip="タスクを完了します。"
                                 Visibility="{Binding Path=Done,Converter={StaticResource _NotBooleanVisibilityConverter}}"
                                 Click="CompleteButtonClick"/>
                <HyperlinkButton Content="未完了"
                                 DataContext="{Binding}"
                                 ToolTipService.ToolTip="タスクを未完了にします。"
                                 Visibility="{Binding Path=Done,Converter={StaticResource _BooleanVisibilityConverter}}"
                                 Click="UnCompletedButtonClick"/>
                <HyperlinkButton Content="削除"
                                 DataContext="{Binding}"
                                 ToolTipService.ToolTip="タスクを削除します。"
                                 IsTabStop="False"
                                 Click="DeleteButtonClick"/>
            </StackPanel>
        </Grid>
        
        <!--その場編集モード時の画面-->
        <Grid Visibility="{Binding Path=IsEditing, Converter={StaticResource _BooleanVisibilityConverter}}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <StackPanel Grid.Row="0">
                <TextBlock Text="名前"
                           HorizontalAlignment="Left"/>
                <TextBox x:Name="_nameTextBox"
                         HorizontalAlignment="Left"
                         Text="{Binding Path=Name, Mode=TwoWay, UpdateSourceTrigger=Explicit, ValidatesOnExceptions=True, NotifyOnValidationError=True}"
                         Width="200"/>
            </StackPanel>
            <StackPanel Grid.Row="1"
                        Orientation="Horizontal">
                <Button Content="キャンセル"
                    DataContext="{Binding}"
                    Click="CancelButtonClick"/>
                <Button Content="保存"
                    DataContext="{Binding}"
                    Click="SaveButtonClick"/>                
            </StackPanel>
        </Grid>
    </Grid>
</UserControl>

その場編集機能を付けるので、タスクを表示するときは、名前だけじゃなく操作用のボタンも必要。

using System;
using System.Windows;
using System.Windows.Controls;
using SilverTask.ViewModels;

namespace SilverTask.Views
{
    /// <summary>
    /// タスクを表示するためのビュー。
    /// </summary>
    public partial class TaskView : UserControl
    {
        /// <summary>
        /// <see cref="TaskView"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        public TaskView()
        {
            InitializeComponent();
        }

        /// <summary>
        /// ビューにバインドされているビューモデルを取得します。
        /// </summary>
        private TaskViewModel ViewModel
        {
            get { return (TaskViewModel)DataContext; }
        }

        private void EditButtonClick(object sender, RoutedEventArgs e)
        {
            ViewModel.IsEditing = true;
        }

        private void CancelButtonClick(object sender, RoutedEventArgs e)
        {
            ViewModel.IsEditing = false;
            RequestCancel(this, new EventArgs());

            // TextBox の内容をクリア
            if (ViewModel.IsNew == false)
            {
                _nameTextBox.Text = ViewModel.Name;
                var exp = _nameTextBox.GetBindingExpression(TextBox.TextProperty);
                exp.UpdateSource();
            }
        }

        private void SaveButtonClick(object sender, RoutedEventArgs e)
        {
            var exp = _nameTextBox.GetBindingExpression(TextBox.TextProperty);
            exp.UpdateSource();
            if (ViewModel.HasErrors == false)
            {
                RequestSave(this, new EventArgs());
                ViewModel.IsEditing = false;
            }
        }

        /// <summary>
        /// タスクの削除するときに発生します。
        /// </summary>
        public event EventHandler RequestDelete = delegate { };

        /// <summary>
        /// タスクの完了するときに発生します。
        /// </summary>
        public event EventHandler RequestComplete = delegate { };

        /// <summary>
        /// タスクを保存するときに発生します。
        /// </summary>
        public event EventHandler RequestSave = delegate { };

        /// <summary>
        /// タスクを未完了にするときに発生します。
        /// </summary>
        public event EventHandler RequestUncomplete = delegate { };

        /// <summary>
        /// タスクの編集をキャンセルするときに発生します。
        /// </summary>
        public event EventHandler RequestCancel = delegate { };

        private void CompleteButtonClick(object sender, RoutedEventArgs e)
        {
            RequestComplete(this, new EventArgs());
        }

        private void DeleteButtonClick(object sender, RoutedEventArgs e)
        {
            RequestDelete(this, new EventArgs());
        }

        private void UnCompletedButtonClick(object sender, RoutedEventArgs e)
        {
            RequestUncomplete(this, new EventArgs());
        }
    }
}

このビューには、前回作成した TasksViewModel への参照を持たせていません。TasksViewModel への参照をあちこちに持たせたくないためです。その代わりに、ビューは保存や削除などの操作要求を伝えるイベントを持たせています。

タスク一覧を表示するビューを作成

<UserControl x:Class="SilverTask.Views.TasksView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:SilverTask.Controls"
    xmlns:vw="clr-namespace:SilverTask.Views"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
    <UserControl.Resources>
        <local:NotConverter x:Key="_NotConverter"/>

        <DataTemplate x:Key="TaskTemplate">
            <vw:TaskView RequestComplete="OnRequestComplete"
                         RequestDelete="OnRequestDelete"
                         RequestSave="OnRequestSave"
                         RequestCancel="OnRequestCancel"
                         RequestUncomplete="OnRequestUncomplete"/>
        </DataTemplate>
    </UserControl.Resources>
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <StackPanel Grid.Column="0"
                        Orientation="Horizontal">
                <Button x:Name="_addButton"
                        Content="タスクの追加"
                        Click="AddButtonClick"/>
            </StackPanel>
            <StackPanel Grid.Column="1"
                        Orientation="Horizontal">
                <RadioButton Content="未完了"
                             GroupName="Completed"
                             IsChecked="{Binding Path=IsShowCompleted, Mode=TwoWay, Converter={StaticResource _NotConverter}}"
                             Command="{Binding Path=LoadCommand}"/>
                <RadioButton Content="完了済"
                             GroupName="Completed"
                             IsChecked="{Binding Path=IsShowCompleted, Mode=TwoWay}"
                             Command="{Binding Path=LoadCommand}"/>
            </StackPanel>
        </Grid>
        <ListBox x:Name="_listBox"
                 Grid.Row="1"
                 BorderThickness="0"
                 ItemsSource="{Binding Path=Tasks}"
                 ItemTemplate="{StaticResource TaskTemplate}"/>
    </Grid>
</UserControl>

このビューにTasksViewModelをバインドして、TaksViewModel が保持しているタスク一覧を ListBox に表示しています。表示の際には、先ほど作成した TaskView を使っています。

using System;
using System.Windows;
using System.Windows.Controls;
using SilverTask.ViewModels;
using System.Windows.Input;

namespace SilverTask.Views
{
    /// <summary>
    /// タスク一覧を表示するビュー。
    /// </summary>
    public partial class TasksView : UserControl
    {
        /// <summary>
        /// <see cref="TaskView"/> クラスの新しいインスタンスを取得します。
        /// </summary>
        public TasksView()
        {
            InitializeComponent();

            CommandManager.RequerySuggested += (sender, e) =>
            {
                // 完了済みを表示しているときはタスクを追加させない
                _addButton.IsEnabled = ViewModel != null ? !ViewModel.IsShowCompleted : false;
            };
        }

        /// <summary>
        /// ビューにバインドされているビューモデルを取得します。
        /// </summary>
        private TasksViewModel ViewModel
        {
            get
            {
                return (TasksViewModel)DataContext;
            }
        }

        private void OnRequestComplete(object sender, EventArgs e)
        {
            TaskViewModel task = (TaskViewModel)((FrameworkElement)sender).DataContext;
            ViewModel.ComplateTask(task);
        }

        private void OnRequestDelete(object sender, EventArgs e)
        {
            TaskViewModel task = (TaskViewModel)((FrameworkElement)sender).DataContext;
            ViewModel.DeleteTask(task);
        }

        private void OnRequestSave(object sender, EventArgs e)
        {
            TaskViewModel task = (TaskViewModel)((FrameworkElement)sender).DataContext;
            if (task.IsNew)
            {
                ViewModel.CreateTask(task);
            }
            else
            {
                ViewModel.UpdateTask(task);
            }
        }

        private void OnRequestCancel(object sender, EventArgs e)
        {
            TaskViewModel task = (TaskViewModel)((FrameworkElement)sender).DataContext;
            if (task.IsNew)
            {
                ViewModel.Tasks.Remove(task);
            }
        }

        private void OnRequestUncomplete(object sender, EventArgs e)
        {
            TaskViewModel task = (TaskViewModel)((FrameworkElement)sender).DataContext;
            ViewModel.UncomplateTask(task);
        }

        private void AddButtonClick(object sender, RoutedEventArgs e)
        {
            var newTask = ViewModel.NewTask();
            _listBox.ScrollIntoView(newTask);
        }
    }
}

追加ボタンをクリックされたり、TaskView の操作要求イベントをハンドルしたときに、TasksViewModel のメソッドを呼び出しています。サービスと通信するのは TasksViewModel だけにしたいので、こんな形になりました。

最後にメインページを作成

<UserControl x:Class="SilverTask.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vw="clr-namespace:SilverTask.Views"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400"
    Loaded="MainPage_Loaded">
    <Grid x:Name="LayoutRoot" Background="White">
        <vw:TasksView DataContext="{Binding Path=Tasks}"/>
    </Grid>
</UserControl>

メインページは、先ほど作成した TasksView を表示するだけです。

using System.Windows;
using System.Windows.Controls;
using SilverTask.ViewModels;

namespace SilverTask
{
    public partial class MainPage : UserControl
    {
        public MainPage()
        {
            InitializeComponent();
        }

        private void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            this.Dispatcher.BeginInvoke(() =>
            {
                var viewModel = new MainViewModel();
                DataContext = viewModel;
                viewModel.Tasks.LoadTasks();
            });
        }
    }
}

メインページ用ビューモデルの初期化と、データの取得を行っています。描画を止めないように、BeginInvoke を使って非同期にしています。ただ、LoadTasks 自体が内部で非同期処理を行っているから、あまり意味無いかも。

これでひとまず完成

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

この連載で作成したコードは下記からダウンロードできます。

作成したアプリケーションを GAE 上にデプロイしました。IE8 での動作は確認済みです。

利用は自己責任でお願いします。大切なデータは登録しないように。

まとめ

GAE + Silverlight でアプリを開発してみて、Silverlight から GAE の Web API を呼び出すところが、とても面倒でした。Thrift みたいな多言語 RPC フレームワークSilverlight にあれば、開発スピードが飛躍的に向上したと思います。Thrift が Silverlight に対応するのが一番いいですね。

この連載で開発したアプリは、GAE + Silverlight のデモみたいなものです。まだまだ作り込みが足りません。連載は今回で終了ですが、今後も気が向いたときに、少しずつ開発していくかもしれません。