はじめに
本記事は、3年前に書いた以下の記事を大幅に見直して書き改めたものです。
PrismとReactivePropertyで簡単MVVM!
上記の記事は、私の古い認識のもとに書き上げられました。
私の当時の認識で、Prism
の機能のごく一部を使ってWPF
アプリケーションを構成することが「簡単」だと思っていましたが、時が過ぎ、その認識が変化しました。
即ち、Prism
の機能にもう少し乗っかった方が、結果的に「簡単」だと思い直したのです。
また、当時の未熟さから、誤った考えのもとに書かれた部分も見受けられます。
古い記事に「いいね」をいただく度、そのことをお伝えし直したいという気持ちになっていましたが、この度、執筆するやる気を確保できましたので、本記事を投稿するに至りました。
Prism
の機能は多彩ですが、本記事では覚えておくと開発が簡単になる、効果の高い部分のみをピックアップしてお伝えできればなと思います。
.NET Core 3.0
今まで、WPF
は.NET Framework
固有のものでした。
しかし、2019年秋にリリースされるであろう.NET Core 3.0
には、新たにWPF
の機能が乗せられており、.NET Framework
と同じようにWPF
アプリケーションを作成できるようになります。
世間では、Javascript
を中心に、新たなGUIフレームワークが台頭しつつありますが、まだWPF
の人口も残っているのではないでしょうか。
本記事の執筆時点では正式リリースがまだですが、以下では.NET Core 3.0-preview
を使用し、簡単なWPF
のサンプルアプリの作成を通して、Prism
とReactiveProperty
の使用方法をご紹介します。
本記事の開発環境
- Windows 10 Home
- dotnetcore-sdk 3.0.100-preview5-011568
- Prism.Unity 7.2.0.1233-pre
- ReactiveProperty 5.5.1
- Visual Studio Code
GitHub
各ライブラリについて
Prism
について
Nuget
を見ると、Prism
関連のパッケージがたくさんあります。
ここで、各パッケージの意味を見てみましょう。
パッケージ名 | 説明 |
---|---|
Prism.Core | 前提パッケージ |
Prism.Wpf | WPF用パッケージ |
Prism.Forms | Xamarin用パッケージ |
Prism.Autofac | WPF用(Autofac使用) |
Prism.DryIoc | WPF用(DryIoc使用) |
Prism.Mef | WPF用(Mef使用) |
Prism.Ninject | WPF用(Ninject使用) |
Prism.StructureMap | WPF用(StructureMap使用) |
Prism.Unity | WPF用(Unity使用) |
Prism.Autofac.Forms | Xamarin用(Autofac使用) |
Prism.DryIoc.Forms | Xamarin用(DryIoc使用) |
Prism.Unity.Forms | Xamarin用(Unity使用) |
Prism.Core
は前提パッケージで、プラットフォーム共通の機能が入っています。
Prism.Wpf
とPrism.Forms
は、それぞれWPF
用とXamarin
用です。
その下の6つのパッケージはWPF
用で、DIコンテナとしてそれぞれの外部パッケージを使用するものです。
最後の3つはXamarin
用で、DIコンテナとしてそれぞれの外部パッケージを使用します。
WPF
アプリを作るうえでは、WPF
用でコンテナありの6つの中から選択することになります。
今回は、.NET Core
対応ありで公式サンプルにも使用されている、Prism.Unity
を使用します。
Prism.Unity
はPrism.Wpf
に依存し、Prism.Wpf
はPrism.Core
に依存するので、Prism.Unity
だけインストールすれば大丈夫です。
Unity
について
Unity
はDI(依存性の注入)ライブラリです。
Unity
という同名のゲームエンジンが存在しますが、全く関係はありません。
DIライブラリは沢山ありますが、C#
においてはこのUnity
が有名どころです。
Prism.Unity
はUnity
に依存するので、Prism.Unity
だけインストールするようにしましょう。
Prism-Samples-Wpf
Prism.Unity
を使用した、公式サンプルです。
29種類のサンプルがあり、各サンプルは非常に小さなプロジェクトから構成されているので、簡単に機能の把握ができます。
正直、このサンプルを見れば本記事で説明することはあまりありませんが、本記事では、重要な機能をかいつまんでご紹介するようにします。
Prism
の機能概観
機能 | 名前空間 |
---|---|
DIコンテナの補助 | Prism.Ioc, Prism.Unity |
Region | Prism.Regions |
Navigation | Prism.Navigation |
EventAggregator | Prism.Events |
Dialog | Prism.Services.Dialogs |
MVVMの補助 | Prism.Mvvm |
Module | Prism.Modularity |
-
DIコンテナの補助
- 外部のDIライブラリを使用し、自動的にクラスをコンテナに登録したり、コンストラクタインジェクション、セッターインジェクションを行ないます。
-
Region
- 名前をつけたViewの領域に、別のViewを追加するなどの管理ができます。
-
Navigation
- Regionのヒストリ機能や、ライフサイクル管理ができます。
- 本記事では扱いません。
-
EventAggregator
- ViewModelやViewのイベントをPubSubパターンで統合管理します。
-
Dialog
- ダイアログの作成と呼び出しを行ないます。
-
Prism 7.2
からの機能です。 - 本記事では扱いません。
-
MVVMの補助
-
INotifyPropertyChanged
の実装補助、コマンドの実装補助、バリデーションの実装補助を行ないます。 - 本記事では扱いません(
BindableBase
クラスが登場するのみ)。
-
-
Module
- 別のアセンブリにあるクラスを
Prism
アプリケーションに含めるために使用します。 - 本記事では扱いません。
- 別のアセンブリにあるクラスを
ReactiveProperty
について
前章の「MVVMの補助」の部分は、Prism
よりもReactiveProperty
に任せるほうが簡単だという認識です。
Reactiveとは、Observerパターンによる、時系列データ(イベントなど)の監視、およびそのデータ群に対する演算(LINQのような)を扱うものです。
System.Reactive
という外部ライブラリに依存しています。
GUIアプリはイベントに対処しなくてはならないので、このReactiveの考え方は有効です。
ReactiveProperty
は、主にMVVMにおけるViewModelのプロパティとコマンドを、Reactiveに扱うことを可能とします。
サンプルプロジェクトの概要
これから作るアプリの画面を以下に示します。
数値を入力すると、2乗を計算するアプリです。
入力は即座に反映されます。
入力のバリデーションも行います。
ボタンを押すと、ダイアログが表示されます。
プロジェクトの構造
プロジェクト群を以下のように作成しました。
プロジェクトを小分けにしない方が作るのは簡単ですが、今回は分けて作成しました。
# ディレクトリの作成
> mkdir PrismSample
> cd PrismSample
# プロジェクト群の作成
> dotnet new sln
> dotnet new wpf -o PrismSample.App.Main
> dotnet new classlib -o PrismSample.Lib.Views
> dotnet new classlib -o PrismSample.Lib.ViewModels
> dotnet new classlib -o PrismSample.Lib.Models
# ソリューションへの追加
> dotnet sln add .\PrismSample.App.Main\ .\PrismSample.Lib.Views\ .\PrismSample.Lib.ViewModels\ .\PrismSample.Lib.Models\
# プロジェクト参照の追加
> dotnet add .\PrismSample.App.Main\ reference .\PrismSample.Lib.Views\
> dotnet add .\PrismSample.App.Main\ reference .\PrismSample.Lib.ViewModels\
> dotnet add .\PrismSample.Lib.ViewModels\ reference .\PrismSample.Lib.Views\
> dotnet add .\PrismSample.Lib.ViewModels\ reference .\PrismSample.Lib.Models\
# パッケージの追加
> dotnet add .\PrismSample.App.Main\ package Prism.Unity -v 7.2.0.1233-pre
> dotnet add .\PrismSample.Lib.Views\ package Prism.Unity -v 7.2.0.1233-pre
> dotnet add .\PrismSample.Lib.ViewModels\ package Prism.Unity -v 7.2.0.1233-pre
> dotnet add .\PrismSample.Lib.ViewModels\ package ReactiveProperty
.csproj
の修正
.NET Core 3.0
に対応させるため、下記のように修正しました。
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <!--修正-->
<!--修正-->
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Prism.Unity" Version="7.2.0.1233-pre" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<!--修正-->
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Prism.Unity" Version="7.2.0.1233-pre" />
<PackageReference Include="ReactiveProperty" Version="5.5.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PrismSample.Lib.Models\PrismSample.Lib.Models.csproj" />
</ItemGroup>
</Project>
<Project Sdk="Microsoft.NET.Sdk">
<!--修正-->
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
</Project>
実行方法
> dotnet run -p .\PrismSample.App.Main\
ビルドの警告が出なければ成功です。
Prism
アプリケーション
以下では、Visual Studio Codeにより編集を行ないます。
執筆時点では、C#
の拡張機能に不具合があり、シンタックスエラーの赤が表示される場合がありますが、コンパイルが通れば問題ありません。
また、XAML
の編集機能は現時点で無いため、便利に開発したい場合はVisual Studioを使用した方が良いかもしれません。
App.xaml
の編集
- <Application x:Class="PrismSample.App.Main.App"
+ <prism:PrismApplication x:Class="PrismSample.App.Main.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+ xmlns:prism="http://prismlibrary.com/"
xmlns:local="clr-namespace:PrismSample.App.Main"
- StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
- </Application>
+ </prism:PrismApplication>
標準のApplication
クラスをPrismApplication
クラスに置き換えています。
また、StartupUri
が不要になっています。
この作業により、ビルド時に自動生成されるApp
クラスがPrismApplication
を継承するように変化します。
App.xaml.cs
の編集
- using System;
- using System.Collections.Generic;
- using System.Configuration;
- using System.Data;
- using System.Linq;
- using System.Threading.Tasks;
using System.Windows;
+ using Prism.Ioc;
+ using Prism.Unity;
namespace PrismSample.App.Main
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
- public partial class App : Application
+ public partial class App : PrismApplication
{
+ protected override Window CreateShell()
+ {
+ return Container.Resolve<MainWindow>();
+ }
+
+ protected override void RegisterTypes(IContainerRegistry containerRegistry)
+ {
+ }
}
}
標準のApplication
ではなく、Prism.Unity.PrismApplication
を継承します。
CreateShell
メソッドは、アプリケーション開始時に起動するWindow
を返す必要があります。
コンテナからMainWindow
を生成して返しておきます。
ここで、何もしていないのにMainWindow
がDIコンテナに登録されているのがポイントです。
同じアセンブリ(プロジェクト)内のViewとViewModelが自動的に登録されるのです。
Prism
の便利さの本質はここに集約されるのではないでしょうか。
その下にあるRegisterTypes
メソッドは、コンテナに手動でクラスやインスタンスを登録する際に使用します。
実行
> dotnet run -p .\PrismSample.App.Main\
うまくMainWindow
が表示されました。
プロジェクトをまたいだクラスの登録
View
PrismSample.App.Main
のMainWindow.xaml
とMainWindow.xaml.cs
を削除します。
PrismSample.Lib.Views
に、以下のMainWindow.xaml
とMainWindow.xaml.cs
を作成します。
<Window x:Class="PrismSample.Lib.Views.MainWindow"
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:prism="http://prismlibrary.com/"
xmlns:local="clr-namespace:PrismSample.Lib.Views"
mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True"
Title="MainWindow" Width="800" Height="450" FontSize="40">
<Grid>
<TextBlock Text="{Binding Text.Value}"/>
</Grid>
</Window>
using System.Windows;
namespace PrismSample.Lib.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
Viewのprism:ViewModelLocator.AutoWireViewModel="True"
という記述は、関連付けられたViewModelがDIコンテナ内に存在する場合、自動的にDataContext
として登録することを指定しています。
これを行なうのはViewModelLocator
というものです。
ViewのTextBlock.Text
のBindingをText.Value
としている点に注意してください。
以下で登場するReactiveProperty
から値を取得するには、Value
プロパティを参照する必要があります。
ViewModel
PrismSample.Lib.ViewModels
に、以下のMainWindowViewModel.cs
を作成します。
using Prism.Mvvm;
using Reactive.Bindings;
namespace PrismSample.Lib.ViewModels
{
public class MainWindowViewModel : BindableBase
{
public ReactiveProperty<string> Text { get; } = new ReactiveProperty<string>("Hello, Prism!");
}
}
ViewModelにはPrism.Mvvm.BindableBase
を継承させるのを忘れないでください。
Text
はReactive.Bindings.ReactiveProperty
型にしています。
ReactiveProperty
はINotifyPropertyChanged
を実装しているため、双方向バインディングが可能です。
App
App.xaml.cs
に以下の記述を追加します。
using System.Windows;
using Prism.Ioc;
+ using Prism.Mvvm;
using Prism.Unity;
+ using PrismSample.Lib.Views;
+ using PrismSample.Lib.ViewModels;
namespace PrismSample.App.Main
{
public partial class App : PrismApplication
{
protected override Window CreateShell()
{
return Container.Resolve<MainWindow>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
}
+ protected override void ConfigureViewModelLocator()
+ {
+ base.ConfigureViewModelLocator();
+
+ ViewModelLocationProvider.Register<MainWindow , MainWindowViewModel>();
+ }
}
}
ConfigureViewModelLocator
メソッド内で、ViewとViewModelを関連付けています。
実行
> dotnet run -p .\PrismSample.App.Main\
MainWindow
とMainWindowViewModel
が関連付けられています。
Regionの表示
View
前述のN^2
を求めるための表示領域を用意します。
N
の値を入力するほうをOperandView
、計算結果を表示するほうをAnswerView
とします。
<UserControl x:Class="PrismSample.Lib.Views.OperandView"
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:prism="http://prismlibrary.com/"
xmlns:local="clr-namespace:PrismSample.Lib.Views"
mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="N ="
HorizontalAlignment="Left" VerticalAlignment="Center"/>
<TextBox Grid.Column="1"
HorizontalAlignment="Stretch" VerticalAlignment="Center"
Text="{Binding Operand.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
<Button Grid.Row="1" Content="Show Dialog"
Command="{Binding ShowDialogCommand}"/>
</Grid>
</UserControl>
using System.Windows.Controls;
namespace PrismSample.Lib.Views
{
public partial class OperandView : UserControl
{
public OperandView()
{
InitializeComponent();
}
}
}
<UserControl x:Class="PrismSample.Lib.Views.AnswerView"
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:prism="http://prismlibrary.com/"
xmlns:local="clr-namespace:PrismSample.Lib.Views"
mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="N^2 ="
HorizontalAlignment="Left" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{Binding Answer.Value}"/>
</Grid>
<Button Grid.Row="1" Content="Show Dialog"
Command="{Binding ShowDialogCommand}"/>
</Grid>
</UserControl>
using System.Windows.Controls;
namespace PrismSample.Lib.Views
{
public partial class AnswerView : UserControl
{
public AnswerView()
{
InitializeComponent();
}
}
}
ViewModel
上で作成したViewに対応するViewModelを作成します。
using System;
using System.ComponentModel.DataAnnotations;
using Prism.Mvvm;
using Reactive.Bindings;
namespace PrismSample.Lib.ViewModels
{
public class OperandViewModel : BindableBase
{
[Required, Range(-10000, 10000)]
public ReactiveProperty<string> Operand { get; }
public OperandViewModel()
{
Operand = new ReactiveProperty<string>("2")
.SetValidateAttribute(() => Operand);
}
}
}
using Prism.Mvvm;
using Reactive.Bindings;
namespace PrismSample.Lib.ViewModels
{
public class AnswerViewModel : BindableBase
{
public ReactiveProperty<string> Answer { get; }
public AnswerViewModel()
{
Answer = new ReactiveProperty<string>("4");
}
}
}
まだ計算機能はありません。
SetValidateAttribute
メソッドは、System.ComponentModel.DataAnnotations
の属性から、対応するエラーが存在するかどうかを検出可能にします。
MainWindowにRegionを確保
先程のViewを表示するためのRegionをMainWindow
に作成します。
<Window x:Class="PrismSample.Lib.Views.MainWindow"
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:prism="http://prismlibrary.com/"
xmlns:local="clr-namespace:PrismSample.Lib.Views"
mc:Ignorable="d"
prism:ViewModelLocator.AutoWireViewModel="True"
Title="MainWindow" Width="800" Height="450" FontSize="40"
Loaded="OnLoaded">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ContentControl Grid.Column="0" Margin="20"
prism:RegionManager.RegionName="OperandRegion"/>
<ContentControl Grid.Column="1" Margin="20"
prism:RegionManager.RegionName="AnswerRegion"/>
</Grid>
</Window>
prism:RegionManager.RegionName
にRegionの名前を指定します。
RegionにViewを追加
MainWindow
のコードビハインドを修正します。
using System.Windows;
using Prism.Ioc;
using Prism.Regions;
using Unity.Attributes;
namespace PrismSample.Lib.Views
{
public partial class MainWindow : Window
{
[Dependency]
public IContainerExtension ContainerExtension { get; set; }
[Dependency]
public IRegionManager RegionManager { get; set; }
public MainWindow()
{
InitializeComponent();
}
public void OnLoaded(object sender, RoutedEventArgs e)
{
RegionManager.AddToRegion("OperandRegion", ContainerExtension.Resolve<OperandView>());
RegionManager.AddToRegion("AnswerRegion" , ContainerExtension.Resolve<AnswerView>());
}
}
}
Regionの追加に必要なPrism.Ioc.IContainerExtension
とPrism.Regions.IRegionManager
を、DIにより取得しています。
これは、セッターインジェクションという手法で、Unity.Attributes.Dependency
属性をセッターに付加することで実現します。
ViewとViewModelの関連付け
先程と同様に、App.xaml.cs
のConfigureViewModelLocator
メソッドでViewとViewModelを関連付けを行ないます。
protected override void ConfigureViewModelLocator()
{
base.ConfigureViewModelLocator();
ViewModelLocationProvider.Register<MainWindow , MainWindowViewModel>();
+ ViewModelLocationProvider.Register<OperandView, OperandViewModel >();
+ ViewModelLocationProvider.Register<AnswerView , AnswerViewModel >();
}
実行
> dotnet run -p .\PrismSample.App.Main\
Regionに2つのViewが表示されました。
EventAggregatorによるViewModel間通信
ViewModel
EventAggregatorは、PubSubパターンでイベントの通知と購読を管理することで、ViewModel間の通信を実現します。
EventAggregatorは、ViewModelが自動生成される際にDIにより取得可能です。
先程と同様、セッターインジェクションで取得できますが、ここではコンストラクタインジェクションで取得してみましょう。
OperandViewModel
とAnswerViewModel
を改変します。
using System;
using System.ComponentModel.DataAnnotations;
using System.Reactive.Linq;
using Prism.Events;
using Prism.Mvvm;
using Reactive.Bindings;
namespace PrismSample.Lib.ViewModels
{
public class OperandViewModel : BindableBase
{
[Required, Range(-10000, 10000)]
public ReactiveProperty<string> Operand { get; }
public OperandViewModel(IEventAggregator eventAggregator)
{
Operand = new ReactiveProperty<string>("2")
.SetValidateAttribute(() => Operand);
Observable.WithLatestFrom
(
Operand,
Operand.ObserveHasErrors,
(o, e) => (o, e)
)
.Where(z => !z.e)
.Subscribe(z =>
{
eventAggregator
.GetEvent<PubSubEvent<double>>()
.Publish(double.Parse(z.o));
});
}
}
}
using Prism.Events;
using Prism.Mvvm;
using Reactive.Bindings;
using Unity.Attributes;
namespace PrismSample.Lib.ViewModels
{
public class AnswerViewModel : BindableBase
{
public ReactiveProperty<string> Answer { get; }
public ReactiveCommand<object> ShowDialogCommand { get; }
public AnswerViewModel(IEventAggregator eventAggregator)
{
Answer = new ReactiveProperty<string>("4");
eventAggregator
.GetEvent<PubSubEvent<double>>()
.Subscribe(CalculateAnswer);
}
private void CalculateAnswer(double operand)
{
Answer.Value = (operand * operand).ToString();
}
}
}
コンストラクタの引数にEventAggregatorが注入されます。
GetEvent<PubSubEvent<double>>
メソッドにより、double
型のイベントを取得し、Publish
で値を発行、Subscribe
で発行された値を購読します。
ここではdouble
型を使用していますが、EventAggregatorは複数のViewModelで共有されるため、通信用に独自の型を定義した方が良いかもしれません。
このSubscribe
はちょっとくせ者で、購読メソッドを弱参照するという特徴があります。
なぜなら、強参照を持ってしまうと参照の依存関係が生じ、後々解放する必要が出てくるからです。
弱参照にしておけば、その手間が省けるのですが、代わりに注意すべきことがあります。
以下のように、
eventAggregator
.GetEvent<PubSubEvent<double>>()
.Subscribe(o => Answer.Value = (o * o).ToString(););
と、ラムダ式を使って購読した場合、このラムダ式はこの場で生成され、誰も参照を持っていない宙ぶらりんの状態になります。
そうすると、GCにより回収の対象になってしまいます。
結果、イベントが通知されない! ということになります。
ですから、ここはラムダ式を使わず、メソッドへの参照を直接指定しましょう。
eventAggregator
.GetEvent<PubSubEvent<double>>()
.Subscribe(CalculateAnswer);
実行
> dotnet run -p .\PrismSample.App.Main\
ViewModel間で値のやり取りができています。
Modelの追加
Model
N^2
の計算をModelに持たせるようにしてみましょう。
テスト可能なコードにするために、インターフェイスも用意しておきます。
namespace PrismSample.Lib.Models
{
public interface IModel
{
double Calculate(double operand);
}
}
namespace PrismSample.Lib.Models
{
public class Model : IModel
{
public double Calculate(double operand)
{
return operand * operand;
}
}
}
App
IModel
とModel
を関連付けておきましょう。
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
+ containerRegistry.Register<IModel, Model>();
}
ViewModel
IModel
をインジェクションします。
+ [Dependency]
+ public IModel Model { get; set; }
+ private void CalculateAnswer(double operand)
+ {
+ Answer.Value = Model.Calculate(operand).ToString();
+ }
実行
> dotnet run -p .\PrismSample.App.Main\
Modelによる計算ができています。
Dialog
Dialogの表示に関しては、Prism 7.2
以降ではPrism.Services.Dialogs.IDialogService
を使用します。
しかし、メッセージダイアログをちょっと表示したいだけ、という場合、使い勝手は良くないです(DialgのViewとViewModelを自分で書かなくてはいけない)。
本記事では利便性を重視して、WPF
標準のMessageBox
を使用する方法をご紹介します。
View
using System.Windows;
namespace PrismSample.Lib.Views
{
public enum DialogButton
{
OK = MessageBoxButton.OK,
OKCancel = MessageBoxButton.OKCancel,
YesNo = MessageBoxButton.YesNo,
YesNoCancel = MessageBoxButton.YesNoCancel
}
public enum DialogImage
{
None = MessageBoxImage.None,
Information = MessageBoxImage.Information,
Question = MessageBoxImage.Question,
Warning = MessageBoxImage.Warning,
Error = MessageBoxImage.Error
}
public enum DialogResult
{
None = MessageBoxResult.None,
OK = MessageBoxResult.OK,
Cancel = MessageBoxResult.Cancel,
Yes = MessageBoxResult.Yes,
No = MessageBoxResult.No
}
public interface IDialogHelper
{
DialogResult ShowDialog
(
string message,
string caption = "Information",
DialogButton button = DialogButton.OK,
DialogImage image = DialogImage.None
);
}
}
using System.Windows;
namespace PrismSample.Lib.Views
{
public class DialogHelper : IDialogHelper
{
public DialogResult ShowDialog
(
string message,
string caption,
DialogButton button,
DialogImage image
)
{
return (DialogResult)MessageBox.Show
(
message,
caption,
(MessageBoxButton)button,
(MessageBoxImage )image
);
}
}
}
Dialogの表示は思いっきり副作用なので、インターフェイスを定義しておきます。
今回はPrismSample.Lib.Views
内に上記2つを定義しましたが、インターフェイスを別プロジェクトに分けることで、ViewModels
からViews
への参照を無くすることもできます。
ViewModel
Dialogを表示するコマンドを、Reactive.Bindings.ReactiveCommand
を使用して書きます。
[Dependency]
public IDialogHelper DialogHelper { get; set; }
public ReactiveCommand<object> ShowDialogCommand { get; }
// コンストラクタ内
ShowDialogCommand = new ReactiveCommand(Operand.ObserveHasErrors.Select(x => !x))
.WithSubscribe(_ => DialogHelper.ShowDialog($"N = {Operand.Value}"));
[Dependency]
public IDialogHelper DialogHelper { get; set; }
public ReactiveCommand<object> ShowDialogCommand { get; }
// コンストラクタ内
ShowDialogCommand = new ReactiveCommand()
.WithSubscribe(_ => DialogHelper.ShowDialog($"N^2 = {Answer.Value}"));
OperandViewModel
内でReactiveCommand
のコンストラクタに渡しているのは、IObservable<bool>
です。
これは、WPF
のICommand
におけるCanExecute
とCanExecuteChanged
を制御し、コマンドが実行可能かどうかを自動的に反映するようにしてくれるものです。
AnswerViewModel
では、ReactiveCommand
のコンストラクタに引数を渡していませんが、これは常に実行可能なコマンドを生成します。
App
IDialogHelper
とDialogHelper
の関係性を登録します。
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
+ containerRegistry.Register<IDialogHelper, DialogHelper>();
containerRegistry.Register<IModel, Model>();
}
実行
> dotnet run -p .\PrismSample.App.Main\
Dialogが表示できました。
まとめ
無駄に長文になってしまいました。
本記事の内容をまとめます。
-
Prism.Unity
とReactiveProperty
をインストール。 -
App.xaml
を、prism:PrismApplication
を使用するように変更。 -
App.xaml.cs
のCreateShell
でMainWindow
を返す。 -
App.xaml.cs
のRegisterTypes
でDIコンテナに型を登録。 -
App.xaml.cs
のConfigureViewModelLocator
でViewとViewModelの関連を登録。 - ViewModelには
BindableBase
を継承させる。 - DIコンテナからインスタンスを生成する場合、コンストラクタインジェクションまたはセッターインジェクションで、登録された型のインスタンスを取得可能。
- EventAggregatorを使用してViewModel間の通信が可能。
- EventAggregatorは、コールバックを弱参照することに注意。
- Dialogの表示を
Prism
でやるのは大変なので、ヘルパークラスを定義。 -
ReactiveProperty
とReactiveCommand
で、ViewModelのプロパティを便利に管理。
以上です。
ありがとうございました。