Windows ストア アプリの開発で、今始めるうえで一番無難に便利にはじめれる個人的な考えを書いてみます。
選択するフレームワーク
- Prism for Windows Runtime(Windows 8.1版)
MS公式のライブラリということで、やっぱり一番安定してますよね。安定というのは、別に機能が豊富だとか、イケてるだとかいうのではなく、プロジェクトで採用しやすいだろうとうそういう意図が込められていたりもします。
プロジェクトテンプレート
フレームワークを使った開発は、通常のテンプレートが吐き出すコードとは、必ずしもマッチするとは限らないので専用のプロジェクトテンプレートとアイテムテンプレートを入れましょう。最初からPrismの基本クラスを継承したAppクラスや、Pageクラスが作成されます。
- Prism for the Windows Runtime Templates (Win 8.1)
選択するプロジェクトテンプレート
Prismのプロジェクトテンプレートには、以下の2種類があります。
- Prism App
- Prism App using Unity
特に理由が無い限りは、本気で作るならPrism App using Unityをお勧めします。View, ViewModel, Model, Prismの提供するクラス群を組み立てて適切にセットするという手間をUnityがかなり軽減してくれます。
土台作り
では、作成します。Prism App using Unityから新規作成します。とりあえず最終目標は仰々しい足し算アプリCalcAppという名前にします。
Models名前空間を作ろう
Models名前空間にアプリケーションのコア機能を実装します。ここらへんは、別にModelsじゃなくて、もっとしっくり来る名前があったり、単一名前空間に収まらないくらいの規模で整理整頓が必要だと感じたら、適時名前空間の分割を行ったりするといいと思います。今回は、単純にCalcAppModelという名前のクラスを作成します。プロパティの変更通知は、必須機能なので、Prismの基本クラスであるBindableBaseクラスを継承して殻だけ作っておきます。
using Microsoft.Practices.Prism.StoreApps; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CalcApp.Models { publicclass CalcAppModel : BindableBase { } }
このCalcAppModelは、アプリケーション内では単一のインスタンスを使いまわすようにすることで、ページやViewModel間で同じデータを元にいろいろな表現ができるようにしたいと思います。これは、Unityの機能を使って行います。Prismでは、AppクラスのOnInitializeメソッドで行います。
protectedoverridevoid OnInitialize(IActivatedEventArgs args) { _container.RegisterInstance(NavigationService); // Modelの登録// コンテナ内で1つのインスタンスしか作られないようにContainerControlledLifetimeManagerを設定する。 _container.RegisterType<CalcAppModel>(new ContainerControlledLifetimeManager()); }
このように、アプリケーションのコア機能のルートになるようなクラスは、インスタンスを乱発することは少ないと思うので、Unityに管理を委ねるのが楽だと思います。
ViewModelでModelのインスタンスを使おう
では、メインのページのViewModelであるMainPageViewModel(プロジェクトテンプレートで作成されています)で先ほど作ったCalcAppModelを使用できるように準備したいと思います。Unityでコンテナに組み立ててもらうときにオブジェクトを貰うには、コンストラクタで受け取るようにするか、[Dependency]属性をつけたプロパティを作るかの2通りになります。どちらでも構わないのですが、個人的には引数受け取りすぎるコンストラクタは好みではないので、今回は後者のプロパティで受け渡してもらうようにします。
publicclass MainPageViewModel : ViewModel { private INavigationService _navigationService; /// <summary>/// Unityからインスタンスをもらうためのプロパティ/// </summary> [Dependency] public CalcAppModel Model { get; set; } /// <summary>/// デフォルトではコンストラクタ経由で画面遷移を行うためのINavigationServiceを貰うようになってる/// </summary>/// <paramname="navigationService"></param>public MainPageViewModel(INavigationService navigationService) { _navigationService = navigationService; } }
ViewとViewModelの紐づけ
デフォルトではViews名前空間のMainPageViewというクラスに対して、ViewModels名前空間のMainPageViewModelがDataContextにセットされるようになっています。ViewModelのインスタンスの生成はUnityで行われるため、先ほど行ったModelを受け取るという定義が効いて、Modelプロパティに値がセットされます。
ViewからViewModelの呼び出しの確認
ViewからViewModelを呼ぶには、一般的にICommandを経由しておこないます。DelegateCommandクラスがPrismでのICommandの実装なので、下記のようにMainPageViewModelにDelegateCommand型のプロパティを作成します。
private DelegateCommand _sampleCommand; public DelegateCommand SampleCommand { get { return _sampleCommand ?? (_sampleCommand = new DelegateCommand(() => { // ここに何か処理を書く// 確認用にVSのデバッグの出力にModelを出力してみる Debug.WriteLine(this.Model); })); } }
Commandは、ButtonなどのCommandプロパティのあるクラスや、InvokeCommandActionなどを使って呼び出すことが出来ます。動作確認には、画面に適当にボタンを貼り付けて、Commandプロパティにバインドするのが簡単です。
<Button Content="Button"HorizontalAlignment="Left"Margin="120,23,0,0"Grid.Row="1"VerticalAlignment="Top"Command="{Binding SampleCommand}"/>
実行して動作確認
ここまでで、デバッグ実行してボタンを押すと、出力ウィンドウに以下のようなメッセージが表示されます。
CalcApp.Models.CalcAppModel
ここまでのまとめ
- Prism for Windows Runtimeを使う
- プロジェクトテンプレートでUnityを使うものを使う
- Modelを作ってUnityに管理は任せる
- ViewModelとModelの関連付けはUnityに任せる
- ViewからViewModelの呼び出しにはCommandを使う
足し算アプリに仕立て上げる
では、なんとなく土台が出来たので肉付けして足し算アプリにします。最初のページで2つのテキストボックスに値を入力して、計算ボタンを押すと画面遷移をして答えが出るというものです。答えのページから戻ると、前回の入力内容はそのまま残ってるという感じでいきましょう。
Modelを作る
足し算には仰々しいですが、足し算するクラスだけ別クラスに切り出して、残りの処理はCalcAppModelに持たせました。左辺値、右辺値、答えをプロパティで持って、計算を開始するメソッドを持ってる感じです。
using Microsoft.Practices.Prism.StoreApps; using Microsoft.Practices.Unity; using System; using System.Threading.Tasks; namespace CalcApp.Models { publicclass CalcAppModel : BindableBase { privateint _lhs; /// <summary>/// 左辺値/// </summary>publicint Lhs { get { returnthis._lhs; } set { this.SetProperty(refthis._lhs, value); } } privateint _rhs; /// <summary>/// 右辺値/// </summary>publicint Rhs { get { returnthis._rhs; } set { this.SetProperty(refthis._rhs, value); } } privateint _answer; /// <summary>/// 答え/// </summary>publicint Answer { get { returnthis._answer; } set { this.SetProperty(refthis._answer, value); } } privatebool _isProcessing; /// <summary>/// 処理中かどうかを表す/// </summary>publicbool IsProcessing { get { returnthis._isProcessing; } set { this.SetProperty(refthis._isProcessing, value); } } /// <summary>/// Unityにインスタンスを入れてもらう/// </summary> [Dependency] public Calculator Calculator { get; set; } /// <summary>/// 時間がかかる計算をするということで…/// </summary>/// <returns></returns>public Task CalcAsync() { return DoTask<object>(async () => { await Task.Delay(3000); this.Answer = this.Calculator.Add(this.Lhs, this.Rhs); returnnull; }); } private async Task<T> DoTask<T>(Func<Task<T>> f) { this.IsProcessing = true; try { return await f(); } finally { this.IsProcessing = false; } } } /// <summary>/// 計算をするクラス/// </summary>publicclass Calculator { publicint Add(int x, int y) { return x + y; } } }
ViewModelを作る
計算の入力値を受け取りバリデーションをするクラス
using Microsoft.Practices.Prism.StoreApps; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CalcApp.ViewModels { publicclass CalcInputViewModel : ValidatableBindableBase { privatestring _lhs; [Required(ErrorMessage = "左辺値を入力してください")] [CustomValidation(typeof(CalcInputViewModel), "ValidateIntValue", ErrorMessage = "左辺値は整数値で入力してください")] publicstring Lhs { get { returnthis._lhs; } set { this.SetProperty(refthis._lhs, value); } } privatestring _rhs; [Required(ErrorMessage = "右辺値を入力してください")] [CustomValidation(typeof(CalcInputViewModel), "ValidateIntValue", ErrorMessage = "右辺値は整数値で入力してください")] publicstring Rhs { get { returnthis._rhs; } set { this.SetProperty(refthis._rhs, value); } } publicstatic ValidationResult ValidateIntValue(stringvalue, ValidationContext ctx) { int dummy; returnint.TryParse(value, out dummy) ? ValidationResult.Success : new ValidationResult(null); } } }
そして、それを保持して、コマンドの活性非活性を制御しつつ、コマンドが実行されたらモデルに値を渡して呼び出しを行って画面遷移してます。
using CalcApp.Models; using Microsoft.Practices.Prism.StoreApps; using Microsoft.Practices.Prism.StoreApps.Interfaces; using Microsoft.Practices.Unity; using System.Collections.Generic; using System.Diagnostics; using Windows.UI.Xaml.Navigation; using System.Linq; namespace CalcApp.ViewModels { publicclass MainPageViewModel : ViewModel { private INavigationService _navigationService; /// <summary>/// Unityからインスタンスをもらうためのプロパティ/// </summary> [Dependency] public CalcAppModel Model { get; set; } private CalcInputViewModel _input; public CalcInputViewModel Input { get { returnthis._input; } set { this.SetProperty(refthis._input, value); } } public DelegateCommand CalcCommand { get; private set; } /// <summary>/// デフォルトではコンストラクタ経由で画面遷移を行うためのINavigationServiceを貰うようになってる/// </summary>/// <paramname="navigationService"></param>public MainPageViewModel(INavigationService navigationService) { _navigationService = navigationService; this.CalcCommand = new DelegateCommand(() => { this.Model.Lhs = int.Parse(this.Input.Lhs); this.Model.Rhs = int.Parse(this.Input.Rhs); var nowait = this.Model.CalcAsync(); this._navigationService.Navigate("Answer", null); }, () => !this.Input.Errors.Errors.Any()); } publicoverridevoid OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState) { base.OnNavigatedTo(navigationParameter, navigationMode, viewModelState); this.Input = new CalcInputViewModel { Lhs = this.Model.Lhs.ToString(), Rhs = this.Model.Rhs.ToString() }; // エラーに変更があったらコマンドの活性非活性を切り替えるthis.Input.ErrorsChanged += (_, __) => this.CalcCommand.RaiseCanExecuteChanged(); } } }
Answerのほうは、ViewModelこんな感じで、ModelのプロパティをProxyするのがだるくなったのでModelをそのまま持つだけにしました。
using CalcApp.Models; using Microsoft.Practices.Prism.StoreApps; using Microsoft.Practices.Unity; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; using Windows.UI.Xaml.Navigation; namespace CalcApp.ViewModels { publicclass AnswerPageViewModel : ViewModel { [Dependency] public CalcAppModel Model { get; set; } } }
あとは適当にViewつくっとけばいいです。
画面
力尽きた。コードだけ
<prism:VisualStateAwarePagex:Name="pageRoot"x:Class="CalcApp.Views.MainPage"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="using:CalcApp.Views"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:prism="using:Microsoft.Practices.Prism.StoreApps"mc:Ignorable="d"prism:ViewModelLocator.AutoWireViewModel="True"d:DataContext="{d:DesignData /SampleData/MainPageViewModelSampleData.xaml}"><prism:VisualStateAwarePage.Resources><!-- TODO: Delete this line if the key AppName is declared in App.xaml --><x:String x:Key="AppName">MainPage</x:String></prism:VisualStateAwarePage.Resources><!-- This grid acts as a root panel for the page that defines two rows: * Row 0 contains the back button and page title * Row 1 contains the rest of the page layout --><Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"><Grid.ColumnDefinitions><ColumnDefinition Width="120"/><ColumnDefinition/></Grid.ColumnDefinitions><Grid.ChildrenTransitions><TransitionCollection><EntranceThemeTransition/></TransitionCollection></Grid.ChildrenTransitions><Grid.RowDefinitions><RowDefinition Height="140"/><RowDefinition Height="*"/></Grid.RowDefinitions><!-- Back button and page title --><Grid Grid.ColumnSpan="2"><Grid.ColumnDefinitions><ColumnDefinition Width="120"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><Button x:Name="backButton"AutomationProperties.Name="Back"AutomationProperties.AutomationId="BackButton"AutomationProperties.ItemType="Navigation Button"Command="{Binding GoBackCommand, ElementName=pageRoot}"Margin="39,59,39,0"Style="{StaticResource NavigationBackButtonNormalStyle}"VerticalAlignment="Top" /><TextBlock x:Name="pageTitle"Grid.Column="1"IsHitTestVisible="false"Margin="0,0,30,40"Style="{StaticResource HeaderTextBlockStyle}"Text="{StaticResource AppName}"TextWrapping="NoWrap"VerticalAlignment="Bottom" /></Grid><TextBox HorizontalAlignment="Left"Margin="67,10,0,0"Grid.Row="1"TextWrapping="Wrap"Text="{Binding Input.Lhs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"VerticalAlignment="Top"Grid.Column="1"Width="315"/><TextBox HorizontalAlignment="Left"Margin="67,63,0,0"Grid.Row="1"TextWrapping="Wrap"Text="{Binding Input.Rhs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"VerticalAlignment="Top"Grid.Column="1"Width="315"/><TextBlock Grid.Column="1"HorizontalAlignment="Left"Margin="11,10,0,0"Grid.Row="1"TextWrapping="Wrap"Text="左辺値"VerticalAlignment="Top"Style="{StaticResource CaptionTextBlockStyle}"/><TextBlock Grid.Column="1"HorizontalAlignment="Left"Margin="11,63,0,0"Grid.Row="1"TextWrapping="Wrap"Text="右辺値"VerticalAlignment="Top"Style="{StaticResource CaptionTextBlockStyle}" /><TextBlock Grid.Column="1"HorizontalAlignment="Left"Margin="398,10,0,0"Grid.Row="1"TextWrapping="Wrap"Text="{Binding Input.Errors[Lhs][0], Mode=OneWay}"VerticalAlignment="Top"Style="{StaticResource BaseTextBlockStyle}"Foreground="Red"/><TextBlock Grid.Column="1"HorizontalAlignment="Left"Margin="398,63,0,0"Grid.Row="1"TextWrapping="Wrap"Text="{Binding Input.Errors[Rhs][0], Mode=OneWay}"VerticalAlignment="Top"Style="{StaticResource BaseTextBlockStyle}"Foreground="Red"/><Button Content="計算"Grid.Column="1"HorizontalAlignment="Left"Margin="305,129,0,0"Grid.Row="1"VerticalAlignment="Top"Width="80"Command="{Binding Path=CalcCommand}"/></Grid></prism:VisualStateAwarePage>
<prism:VisualStateAwarePagexmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local="using:CalcApp.Views"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:prism="using:Microsoft.Practices.Prism.StoreApps"xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"xmlns:Core="using:Microsoft.Xaml.Interactions.Core"x:Name="pageRoot"x:Class="CalcApp.Views.AnswerPage"mc:Ignorable="d"prism:ViewModelLocator.AutoWireViewModel="True"d:DataContext="{d:DesignData /SampleData/AnswerPageViewModelSampleData.xaml}"><prism:VisualStateAwarePage.Resources><!-- TODO: Delete this line if the key AppName is declared in App.xaml --><x:String x:Key="AppName">AnswerPage</x:String></prism:VisualStateAwarePage.Resources><!-- This grid acts as a root panel for the page that defines two rows: * Row 0 contains the back button and page title * Row 1 contains the rest of the page layout --><Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"><Grid.ChildrenTransitions><TransitionCollection><EntranceThemeTransition/></TransitionCollection></Grid.ChildrenTransitions><Grid.RowDefinitions><RowDefinition Height="140"/><RowDefinition Height="*"/></Grid.RowDefinitions><!-- Back button and page title --><Grid><Grid.ColumnDefinitions><ColumnDefinition Width="120"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions><Button x:Name="backButton"AutomationProperties.Name="Back"AutomationProperties.AutomationId="BackButton"AutomationProperties.ItemType="Navigation Button"Command="{Binding GoBackCommand, ElementName=pageRoot}"IsEnabled="{Binding CanGoBack, ElementName=pageRoot}"Margin="39,59,39,0"Style="{StaticResource NavigationBackButtonNormalStyle}"VerticalAlignment="Top" /><TextBlock x:Name="pageTitle"Grid.Column="1"IsHitTestVisible="false"Margin="0,0,30,40"Style="{StaticResource HeaderTextBlockStyle}"Text="{StaticResource AppName}"TextWrapping="NoWrap"VerticalAlignment="Bottom" /></Grid><TextBlock Text="{Binding Model.Answer}"Margin="119,46,0,0"HorizontalAlignment="Left"VerticalAlignment="Top"Grid.Row="1"Style="{StaticResource BodyTextBlockStyle}"><Interactivity:Interaction.Behaviors><Core:DataTriggerBehavior x:Name="True1"Binding="{Binding Model.IsProcessing}"Value="True"><Core:ChangePropertyAction PropertyName="Visibility"><Core:ChangePropertyAction.Value><Visibility>Collapsed</Visibility></Core:ChangePropertyAction.Value></Core:ChangePropertyAction></Core:DataTriggerBehavior><Core:DataTriggerBehavior x:Name="False1"Binding="{Binding Model.IsProcessing}"Value="False"><Core:ChangePropertyAction PropertyName="Visibility"/></Core:DataTriggerBehavior></Interactivity:Interaction.Behaviors></TextBlock><ProgressRing HorizontalAlignment="Center"VerticalAlignment="Center"Grid.RowSpan="2"Width="50"Height="50"IsActive="True"><Interactivity:Interaction.Behaviors><Core:DataTriggerBehavior x:Name="True"Binding="{Binding Model.IsProcessing}"Value="True"><Core:ChangePropertyAction PropertyName="Visibility"><Core:ChangePropertyAction.Value><Visibility>Visible</Visibility></Core:ChangePropertyAction.Value></Core:ChangePropertyAction></Core:DataTriggerBehavior><Core:DataTriggerBehavior x:Name="False"Binding="{Binding Model.IsProcessing}"Value="False"><Core:ChangePropertyAction PropertyName="Visibility"><Core:ChangePropertyAction.Value><Visibility>Collapsed</Visibility></Core:ChangePropertyAction.Value></Core:ChangePropertyAction></Core:DataTriggerBehavior></Interactivity:Interaction.Behaviors></ProgressRing></Grid></prism:VisualStateAwarePage>
Modelが処理中の時はAnswerPageではプログレスリング出してます。それくらい?
Microsoft SkyDrive - Access files anywhere. Create docs with free Office Web Apps.