サンプルプログラムの概要

このサンプルプログラムは、ユニバーサルWindowsアプリでPrism for Windows RuntimeとReactivePropertyを使って単純な四則演算を行うアプリケーションです。

Windows Phoneでの実行の様子

Windows での実行の様子

サンプルプログラムの動かし方

NuGetパッケージの復元を有効化してリビルドをしてください。その後スタートアッププロジェクトを指定して実行できます。

サンプルプログラムの解説

サンプルプログラムは、ユニバーサルWindowsアプリのプロジェクトで、SharedプロジェクトにModelに相当するコードを置いています。Modelのコードは、左辺値、右辺値、計算方法を表すenumを保持して、計算を行うメソッドを実行すると答えを表すプロパティに答えが入るというシンプルなオブジェクトです。各プロパティは変更通知を実装しています。

C#
スクリプトの編集|Remove
using Microsoft.Practices.Prism.Mvvm; 
using System; 
using System.Collections.Generic; 
using System.Text; 
 
namespace CalcReactivePropertySample.Models 
{ 
    public class CalcModel : BindableBase 
    { 
        /// <summary> 
        /// 計算方法を保持するマップ 
        /// </summary> 
        private Dictionary<OperatorType, Func<double, double, double>> calculateLogicMap = new Dictionary<OperatorType, Func<double, double, double>> 
        { 
            { OperatorType.Add, (x, y) => x + y}, 
            { OperatorType.Sub, (x, y) => x - y}, 
            { OperatorType.Mul, (x, y) => x * y}, 
            { OperatorType.Div, (x, y) => x / y}, 
        }; 
 
        private double lhs; 
 
        /// <summary> 
        /// 左辺値 
        /// </summary> 
        public double Lhs 
        { 
            get { return this.lhs; } 
            set { this.SetProperty(ref this.lhs, value); } 
        } 
 
        private double rhs; 
 
        /// <summary> 
        /// 右辺値 
        /// </summary> 
        public double Rhs 
        { 
            get { return this.rhs; } 
            set { this.SetProperty(ref this.rhs, value); } 
        } 
 
        private OperatorType operatorType; 
 
        /// <summary> 
        /// 計算方法 
        /// </summary> 
        public OperatorType OperatorType 
        { 
            get { return this.operatorType; } 
            set { this.SetProperty(ref this.operatorType, value); } 
        } 
 
        private double answer; 
 
        /// <summary> 
        /// 答え 
        /// </summary> 
        public double Answer 
        { 
            get { return this.answer; } 
            set { this.SetProperty(ref this.answer, value); } 
        } 
 
        /// <summary> 
        /// 設定された状態に応じて計算を実行 
        /// </summary> 
        public void Calc() 
        { 
            this.Answer = this.calculateLogicMap[this.OperatorType](this.Lhs, this.Rhs); 
        } 
    } 
} 
 

このオブジェクトは、AppContextクラスというModelのルートを表すオブジェクトにプロパティとして保持するようにしています。ViewModelからは、AppContextを経由して、このオブジェクトにアクセスしています。

AppContextクラスのコードを以下に示します。

C#
スクリプトの編集|Remove
using Microsoft.Practices.Prism.Mvvmusing System; 
using System.Collections.Genericusing System.Text; 
 
namespace CalcReactivePropertySample.Models 
{ 
    public class AppContext : BindableBase 
    { 
        public CalcModel CalcModel { getprivate set; } 
 
        public AppContext() 
        { 
            this.CalcModel = new CalcModel(); 
        } 
    } 
} 

 ViewとViewModelとModelの紐づけ

各レイヤの紐づけはApp.xaml.csのOnInitializeAsyncメソッドで行っています。ViewModelLocationProviderのRegisterメソッドでViewとViewModelのマッピングを行い、ViewModelの生成処理でModelのインスタンスを渡しています。

Windows Phoneでは、計算の条件を入力する画面を計算結果を表示する画面をわけて実装しているため、2つのViewModel間でModelのインスタンスが共用されるようにしています。

Windows PhoneのApp.xaml.csのOnInitializeAsyncメソッドを以下に示します。

C#
スクリプトの編集|Remove
protected override Task OnInitializeAsync(IActivatedEventArgs args) 
{ 
    // modelを作成 
    var model = new AppContext(); 
    // ViewとViewModelのインスタンス生成方法のマッピング 
    ViewModelLocationProvider.Register( 
        typeof(MainPage).FullName, () => new MainPageViewModel(model, this.NavigationService)); 
    ViewModelLocationProvider.Register( 
        typeof(AnswerPage).FullName, () => new AnswerPageViewModel(model)); 
    return Task.FromResult<object>(null); 
} 
 

WindowsストアアプリのApp.xaml.csのOnInitializeAsyncメソッドを以下に示します。

C#
スクリプトの編集|Remove
protected override Task OnInitializeAsync(IActivatedEventArgs args) 
{ 
    var model = new AppContext(); 
    ViewModelLocationProvider.Register( 
        typeof(MainPage).FullName, () => new MainPageViewModel(model)); 
    return Task.FromResult<object>(null); 
} 
 

ViewModelの実装

ViewModelは、左辺値と右辺値と計算方法の入力と、計算結果の表示と、計算実行のトリガーとなるコマンドからなります。この他に、入力にエラーがあるときに表示されるエラーメッセージの表示プロパティもあります。Windows Phone版は計算結果の表示を別画面に実装しています。

すべてのプロパティの実装が入っているWindows ストアアプリ版のMainPageViewModelのコードを以下に示します。

C#
スクリプトの編集|Remove
using CalcReactivePropertySample.Commons; 
using CalcReactivePropertySample.Models; 
using Codeplex.Reactive; 
using Codeplex.Reactive.Extensions; 
using Microsoft.Practices.Prism.Mvvm; 
using System; 
using System.Collections; 
using System.ComponentModel.DataAnnotations; 
using System.Linq; 
using System.Reactive.Linq; 
 
namespace CalcReactivePropertySample.ViewModels 
{ 
    public class MainPageViewModel : ViewModel 
    { 
        private AppContext Model { get; set; } 
 
        /// <summary> 
        /// 左辺値 
        /// </summary> 
        [CanConvert(typeof(double), ErrorMessage = "左辺値は数値で入力してください")] 
        public ReactiveProperty<string> Lhs { get; private set; } 
 
        /// <summary> 
        /// 右辺値 
        /// </summary> 
        [CanConvert(typeof(double), ErrorMessage = "右辺値は数値で入力してください")] 
        public ReactiveProperty<string> Rhs { get; private set; } 
 
        /// <summary> 
        /// 計算方法 
        /// </summary> 
        [Required(ErrorMessage = "計算方法を選択してください")] 
        public ReactiveProperty<OperatorTypeViewModel> OperatorType { get; private set; } 
 
        /// <summary> 
        /// 答え 
        /// </summary> 
        public ReactiveProperty<double> Answer { get; private set; } 
 
        /// <summary> 
        /// エラーメッセージ 
        /// </summary> 
        public ReactiveProperty<string> ErrorMessage { get; private set; } 
 
        /// <summary> 
        /// 計算実行コマンド 
        /// </summary> 
        public ReactiveCommand CalcCommand { get; private set; } 
 
        /// <summary> 
        /// デザイン時用コンストラクタ 
        /// </summary> 
        public MainPageViewModel() 
        { 
        } 
 
        /// <summary> 
        /// 本番用コンストラクタ 
        /// </summary> 
        /// <param name="model"></param> 
        public MainPageViewModel(AppContext model) 
        { 
            this.Model = model; 
 
            // 左辺値のM->VMの接続 
            this.Lhs = this.Model.CalcModel.ObserveProperty(x => x.Lhs) 
                .Select(x => x.ToString()) 
                .ToReactiveProperty() 
                // 属性による検証を有効化 
                .SetValidateAttribute(() => this.Lhs); 
            // 左辺値のVM->Mの接続 
            this.Lhs 
                // エラーがないときに 
                .Where(_ => !this.Lhs.HasErrors) 
                // パースして 
                .Select(x => double.Parse(x)) 
                // モデルに設定する 
                .Subscribe(x => this.Model.CalcModel.Lhs = x); 
 
            // 左辺値と同じ 
            this.Rhs = this.Model.CalcModel.ObserveProperty(x => x.Rhs) 
                .Select(x => x.ToString()) 
                .ToReactiveProperty() 
                .SetValidateAttribute(() => this.Rhs); 
            this.Rhs 
                .Where(_ => !this.Rhs.HasErrors) 
                .Select(x => double.Parse(x)) 
                .Subscribe(x => this.Model.CalcModel.Rhs = x); 
 
            // 計算方法 
            this.OperatorType = new ReactiveProperty<OperatorTypeViewModel>() 
                .SetValidateAttribute(() => this.OperatorType); 
            this.OperatorType 
                .Where(x => x != null) 
                .Select(x => x.OperatorType) 
                .Subscribe(x => this.Model.CalcModel.OperatorType = x); 
 
            // 答え 
            this.Answer = this.Model 
                .CalcModel 
                .ObserveProperty(x => x.Answer) 
                .ToReactiveProperty(); 
 
            // 入力項目に不備がなければ押せるコマンド 
            this.CalcCommand = new[] 
                { 
                    this.Lhs.Select(_ => !this.Lhs.HasErrors), 
                    this.Rhs.Select(_ => !this.Rhs.HasErrors), 
                    this.OperatorType.Select(_ => !this.OperatorType.HasErrors) 
                } 
                .CombineLatestValuesAreAllTrue() 
                .ToReactiveCommand(); 
            // 計算を実行 
            this.CalcCommand.Subscribe(_ => this.Model.CalcModel.Calc()); 
 
            // エラーメッセージ 
            this.ErrorMessage = new[] 
                { 
                    // すべてのエラーメッセージの最後の値を合成 
                    // すべての値が出そろわないと合成されないためnullとマージして必ず1つは値が出るようにしている 
                    Observable.Merge(Observable.Return<IEnumerable>(null), this.Lhs.ObserveErrorChanged), 
                    Observable.Merge(Observable.Return<IEnumerable>(null), this.Rhs.ObserveErrorChanged), 
                    Observable.Merge(Observable.Return<IEnumerable>(null), this.OperatorType.ObserveErrorChanged), 
                } 
                .CombineLatest(x => 
                { 
                    // 値があるエラー情報の中から最初の値を返す 
                    var result = x.Where(y => y != null) 
                        .Select(y => y.OfType<string>()) 
                        .Where(y => y.Any()) 
                        .FirstOrDefault(); 
                    return result == null ? null : result.FirstOrDefault(); 
                }) 
                .ToReactiveProperty(); 
        } 
    } 
} 
 
入力値の検証方法や、コマンドが実行可能になるための条件などViewModelはReactivePropertyの機能を中心に使って実装しています。エラーメッセージの表示も、すべてのプロパティのエラー情報をもとに、左辺値、右辺値、計算方法の優先順位で、エラーを表示するように合成しています。