前回までの内容
GAE + Silverlight でアプリケーションを開発する本連載も終盤です。ビューモデルの実装が前回で終わり、残すはビューのみ。
いきなり完成画面
文章やコードをずらずら並べただけじゃ味気ないので。
完成画面のスクリーンショットがこちら。
シンプルですね。
ちょっとしたこだわりとして、その場編集機能を付けます。
なんとなくリッチですね。
それではビューを実装していきます。
タスクを表示するビューを作成
<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 のデモみたいなものです。まだまだ作り込みが足りません。連載は今回で終了ですが、今後も気が向いたときに、少しずつ開発していくかもしれません。
関連記事
- Google App Engine + Silverlight でタスク管理アプリケーション開発(1) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(2) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(3) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(4) - present
- Google App Engine + Silverlight でタスク管理アプリケーション開発(5) - present