WPF 入門ノート
2017 年 3 月 12 日 (日)
小寺 広志
目次
1
はじめに ... 1
2
WPF とは ... 1
2.1.
WPF のアーキテクチャ ... 1
3
データバインディング ... 3
3.1.
バインディングモード ... 6
3.2.
コンバータ ... 7
3.3.
コマンド ... 10
3.4.
コレクションのデータバインディング ... 11
4
依存関係プロパティ ... 12
4.1. 埋め込み ... 13
4.2. 添付 Behavior ... 13
5
MVVM アーキテクチャ ... 16
5.1.
ViewModel の概念 ... 16
6
WPF のフレームワークの紹介 ... 19
6.1.
Livet ... 19
6.2.
Prism ... 25
7
VisualTree ... 35
7.1.
VisualTree の検索 ... 36
7.2.
階層バインディング ... 37
8
リソース ... 38
8.1.
Style ... 39
8.2.
DataTemplate ... 42
9
標準で提供されている UI コントロール ... 43
10
カスタムコントロール ... 52
11
参考情報 ... 58
12
おわりに ... 58
1
1 はじめに
本稿では、WPF の基本的な所を丁寧に解説することを意図しています。
データバインディング、依存関係プロパティ、ViewModel、VisualTree などの概念が色々
と現れますが、1 つ 1 つ理解していき、何より実際にコードを書くことで自然に WPF が使
えるようになると思います。
2 WPF とは
WPF (Windows Presentation Foundation) には元々、開発プラットフォーム(Windows ア
プリケーション、Web アプリケーション、グラフィックス、等)の統合を目指すという設計
思想があったようです (Chris Anderson;「エッセンシャル WPF:Windows Presentation
Foundation」より)。実際的には、Windows フォームに代わるデスクトップアプリケーショ
ンの開発用フレームワークとみなされます。
2.1. WPF のアーキテクチャ
WPF のアーキテクチャについて msdn に記事があります。
https://msdn.microsoft.com/ja-jp/library/ms750441(v=vs.110).aspx
最初の図だけでもご覧ください。
WPF のプロジェクトを VisualStudio で作成すると
次の 3 つのアセンブリがプロジェクトの参照に追加されます。これらが WPF の実体です。
(1) WindowsBase
(2) PresentationCore
(3) PresentationFramework
2
フリーソフトの ILSpy で上記の各アセンブリを開いてみると、WPF の仕様が巨大であるこ
とが察せられると思います。各アセンブリで提供されているものを幾つかピックアップし
てみます。
(1) WindowsBase
System.Windows 名前空間
「DependencyProperty」、
「DependencyObject」
、「WeakEventManager」
、
「Point」
System.Windows.Threading 名前空間
「Dispatcher」
(2) PresentationCore
System.Windows 名前空間
「RoutedEvent」、
「SizeToContent」
、
「UIElement」
System.Windows.Input 名前空間
「CommandBinding」
System.Windows.Interop 名前空間
「HwndSource」
System.Windows.Media 名前空間
「Brush」、
「Geometry」
、「ImageSource」
、
「Visual」
、「VisualTreeHelper」
System.Windows.Media.Effects 名前空間
「BitmapEffect」、「PixelShader」
、
「ShaderEffect」
System.Windows.Media.Imaging 名前空間
「BitmapImage」、
「WriteableBitmap」
(3) PresentationFramework
System.Windows 名前空間
「DataTemplate」、
「DataTrigger」、
「EventTrigger」、
「FrameworkElement」
、
「PropertyPath」
、「Style」、
「Window」
System.Windows.Controls 名前空間
「Button」
、
「Canvas」
、「CheckBox」、
「ComboBox」
、
「ContentControl」
、
「ControlTemplate」
、
「DataGrid」、
「DockPanel」
、
「Grid」
、
「Label」
、
「ScrollViewer」、「StackPanel」、
「TextBox」、
「UserControl」、
「Viewbox」
System.Windows.Data 名前空間
「Binding」
、
「BindingMode」
、
「MultiBinding」
上記の幾つかは、以降で触れます。
3
ドはパフォーマンスを優先して、スレッドセーフにはなっていません。そのため、Dispatcher
オブジェクトが用意されています。例えば、ボタンのプロパティに対し、別スレッドから直
接アクセスすると例外が飛びます。
この場合、Dispatcher オブジェクト経由でアクセスすると例外は飛びません:
async Task DoTest(){
await Task.Run(() => {
Application.Current.Dispatcher.Invoke(() => { button.Height = 50d; }); }); }
WPF は描画に DirectX を使っています。DirectX が遅い PC では、WPF の描画も遅くな
ります。DirectX の描画では、頂点データをビデオカードに転送します。頂点データがキャ
ッシュされていない初回描画は、2 回目以降の描画に比べて遅いです。
例えば、Excel2010 で
は、マウスの右クリックで表示されるコンテキストメニューで「セルの書式設定」を選択す
ると、初回だけダイアログ表示が遅いことが確認できると思います。
3 データバインディング
Windows フォームにもデータバインディングの概念はあるようですが、WPF の方が柔軟
です。
データバインディングは、
「UI コントロールのプロパティ」と「データ」の同期を取
る仕組みで、理解して使えば実装が楽になると思われます。
データバインディングの例を挙げます。
・「UI コントロールのプロパティ」(※1) は Button の Content プロパティ、
・「データ」は MainWindow の ButtonContent プロパティ (object 型)
とします。以下、STEP1/4~STEP4/4 の変更は全て MainWindow.xaml.cs で行います。
(※1) WPF では、データバインドが可能なプロパティは依存関係プロパティ (Dependency
Property) と呼ばれます。実際、Button.ContentProperty の型は DependencyProperty 型
(System.Windows.DependencyProperty) (WindowsBase.dll 提供) です。
4
STEP 1/4:MainWindow に ButtonContent プロパティをカスタムで定義します。
object buttonContent;public object ButtonContent {
get { return buttonContent; } set { buttonContent = value; } }
STEP 2/4:
「button インスタンスの Content プロパティ」
と
「MainWindow の ButtonContent
プロパティ」を Binding オブジェクトによって結び付けます。ここでは、バインディング
モードを双方向に設定しています。
void TestBind(Button button) {
var buttonContentProp = this.GetType().GetProperty("ButtonContent"); var binding = new Binding {
Path = new PropertyPath(buttonContentProp), Mode = BindingMode.TwoWay
};
button.SetBinding(Button.ContentProperty, binding); }
STEP 3/4:”ButtonContent” プロパティがどのクラスのプロパティであるかを WPF
フレームワークに伝えるために
DataContext
を設定します。
ここでは、this (MainWindow) を設定しています。
public MainWindow() { InitializeComponent(); TestBind(button); this.DataContext = this; }STEP 4/4:”ButtonContent” プロパティから Button.ContentProperty への変更通知を
実装します。そのために、MainWindow に対し
System.ComponentModel.INotifyPropertyChanged
インタフェースを追加します。このインタフェースは、PropertyChanged イベントの
実装を要求します。そこで、PropertyChanged イベントの呼出し用のメソッド
RaisePropertyChanged()を追加し、これを ButtonContent プロパティの Setter から
呼ぶようにします。
5
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged; void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
object buttonContent; public object ButtonContent {
get { return buttonContent; } set { if (buttonContent != value) { buttonContent = value; RaisePropertyChanged("ButtonContent"); } } } }
これでデータバインディングの準備ができました。例えば、マウス左ボタン押下時のハンド
ラを追加し、(button.Content でなく) MainWindow の ButtonContent プロパティに何か
値を設定すると、MainWindow 上のボタンの内容 (button.Content) が変更されることが確
認できます(バインディングモードが TwoWay または OneWay の場合)
。
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
base.OnMouseLeftButtonDown(e); this.ButtonContent = "!?"; }
逆に、button.Content プロパティに何か値を設定すると、MainWindow の ButtonContent
プロパティの値が変更されることが確認できます(バインディングモードが TwoWay また
は OneWayToSource の場合)
。例えば、マウス左ボタン押下時のハンドラとマウス右ボタ
ン押下時のハンドラをそれぞれ用意し、前者で button.Content プロパティの値を変更し、
後者で ButtonContent プロパティの内容を確認します。
6
3 点補足します。
補足 1/3:PropertyChanged イベントは次のようにも書けます。
PropertyChangedEventHandler propChanged;public event PropertyChangedEventHandler PropertyChanged {
add { propChanged += value; } remove { propChanged -= value; } }
このように書いて、add の箇所にデバッグ用のブレークポイントを置いてデバッグ実行す
ると、DataContext = this の実行後に add が呼ばれることが確認できま す。また、
DataContext = this を実行しないと add が呼ばれないことも確認できます。
補足 2/3:RaisePropertyChanged() メソッドで PropertyChanged イベントを呼ぶと、
(button.Content の値の変化があったときのみ) Button クラスの OnPropertyChanged()
ハンドラが呼ばれます。カスタムでコントロールを作成する場合、OnPropertyChanged()
ハンドラ内で何か処理を書くことがあると思います。
補足 3/3:上記の TestBind() では Binding オブジェクトを C#のコードで new していまし
たが、Xaml で Binding を書くこともできます (簡潔に書けるためこちらの書き方を推奨し
ます)。
例えば次の通りです。
<Window x:Class="WpfSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"> <Grid>
<Button Content="{Binding Path=ButtonContent, Mode=TwoWay}" /> </Grid>
</Window>
3.1. バインディングモード
Binding オブジェクトを生成するとき、バインディングモードという設定があります。
OneWay は OneWayToSource の逆方向なので、OneWayToSource
だけ押さえて下さい。
これは要するに、ViewModel への通知です (ViewModel については後述します)。
「UI コン
トロールの依存関係プロパティ」→「データのプロパティ」の方向にのみ通知する設定です。
通知方向を一方向に限定できるならば、TwoWay でなく OneWay か OneWayToSource
を設定した方が意図が明確になります。
7
3.2. コンバータ
データバインディングでは、依存関係プロパティの変更を検出し、通知するというメカニ
ズムが働いています。コンバータはその応用です。
対象とする依存関係プロパティが変化したタイミングで呼ばれ、依存関係プロパティを
入 力 と し て 、 出 力 を 別 の 依 存 関 係 プ ロ パ テ ィ に 渡 す 、 と い う こ と が コ ン バ ー タ
(IValueConverter または IMultiValueConverter) を使って実現できます。
コンバータの例を挙げます。TextBox から入力した数値によって、ラベルの背景色を変更
する例です。
STEP 1/2:コンバータを作成します。ConvertBack() は使わないので実装しません。
using System; using System.Globalization; using System.Windows.Data; using System.Windows.Media; namespace WpfSample.Converters {public class NumberToColorConverter : IValueConverter
{
static SolidColorBrush green = new SolidColorBrush(Colors.LightGreen); static SolidColorBrush yellow = new SolidColorBrush(Colors.Yellow); static SolidColorBrush pink = new SolidColorBrush(Colors.LightPink); public object Convert(object value, Type targetType, object parameter,
CultureInfo culture) {
var text = value.ToString();
var number = string.IsNullOrEmpty(text) ? 100 : System.Convert.ToDouble(value); if (number >= 70) return green; else if (number >= 40) return yellow; return pink; }
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture) {
throw new NotImplementedException(); }
} }
STEP 2/2:Xaml で TextBox と Label を配置し、Label の Background 依存関係プロパティ
8
<Window x:Class="WpfSample.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:WpfSample.Converters" Title="MainWindow" Height="350" Width="525"> <StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<conv:NumberToColorConverter x:Key="colorConv" /> </StackPanel.Resources>
<TextBox x:Name="numberText" Width="100" Height="30"></TextBox> <Label Background="{Binding Path=Text, ElementName=numberText,
Converter={StaticResource colorConv}}" Width="100" Height="30" /> </StackPanel> </Window>
複数の依存関係プロパティを入力とするコンバータ (マルチコンバータ) を作成すること
もできます。金額と税率を入力とし、それらから計算される金額を出力とするコンバータの
例を挙げます。
STEP 1/2:マルチコンバータを実装します。
using System; using System.Globalization; using System.Windows.Data; namespace WpfSample.Converters {public class AmountConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter,
CultureInfo culture) {
var amountStr = values[0] as string; var taxRateStr = values[1] as string;
var amount = string.IsNullOrEmpty(amountStr) ? 0m : System.Convert.ToDecimal(amountStr);
var taxRate = string.IsNullOrEmpty(taxRateStr) ? 0m : System.Convert.ToDecimal(taxRateStr);
return amount * (1m + taxRate); }
9
public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
CultureInfo culture) {
throw new NotImplementedException(); }
} }
STEP 2/2:Xaml 側でのマルチコンバータの呼出しを実装します。
<Window x:Class="WpfSample.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:WpfSample.Converters" Title="MainWindow" Height="350" Width="525"> <StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<conv:AmountConverter x:Key="amountConv" /> </StackPanel.Resources>
<TextBox x:Name="amount" Width="100" Height="30" /> <TextBox x:Name="taxRate" Width="100" Height="30" /> <Label Width="100" Height="30" Background="Aqua"> <Label.Content>
<MultiBinding Converter="{StaticResource amountConv}"> <Binding Path="Text" ElementName="amount" /> <Binding Path="Text" ElementName="taxRate" /> </MultiBinding> </Label.Content> </Label> </StackPanel> </Window>
Xaml での MultiBinding の書き方を紹介する意図でマルチコンバータの例を挙げました。
コンバータは、DataContext のダンプなどのデバッグ用にも使えます。DataContextProperty
もまた依存関係プロパティなので、コンバータの入力に指定できます。DataContext のダン
プ用コンバータを実際に書いてみると、コンバータの理解が深まると思います。
10
3.3. コマンド
Button や MenuItem は、Command 依存関係プロパティを持っています。ICommand 型
のプロパティをバインドすると、Button や MenuItem のクリック時にデータバインディン
グの仕組みを経由してコマンドを実行することができます。
コマンドの例として、Button をクリックすると TextBox の値を表示する例を挙げます。
STEP 1/3:ICommand を継承した RelayCommand を作成します (名前の通り、コンスト
ラクタで渡した Func と Action をそのままリレーして使うコマンドです)。
using System;
using System.Windows.Input;
namespace WpfSample.Commands {
public class RelayCommand : ICommand
{
public event EventHandler CanExecuteChanged; Func<object, bool> canExecuteFunc;
Action<object> executeAction;
public RelayCommand(Func<object, bool> canExecuteFunc, Action<object> executeAction) {
this.canExecuteFunc = canExecuteFunc; this.executeAction = executeAction; }
public bool CanExecute(object parameter) => canExecuteFunc(parameter); public void Execute(object parameter) => executeAction(parameter); }
}
STEP 2/3:MainWindow に TestCommand プロパティを定義します。
public partial class MainWindow : Window, INotifyPropertyChanged{
public RelayCommand TestCommand => new RelayCommand(
param => true,
param => MessageBox.Show($"{param}", "Test Command")); }
11
<Window x:Class="WpfSample.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"> <StackPanel Orientation="Horizontal">
<TextBox x:Name="numberText" Width="100" Height="30" /> <Button Content="コマンドテスト" Width="100" Height="30" Command="{Binding Path=TestCommand, Mode=OneTime}"
CommandParameter="{Binding Path=Text, ElementName=numberText}" /> </StackPanel>
</Window>
3.4. コレクションのデータバインディング
ComboBox の ItemsSource 依存関係プロパティに int 型のコレクションをバインドする
例を挙げます。ObservableCollection<T> を使います。List<T> と比較すると挙動が分か
り易いと思うので、ComboBox を 2 つ用意し、1 つは ObservableCollection<T>にバインド
しもう 1 つは List<T>にバインドして、両者を比較してみます。
STEP 1/2:MainWindow.xaml.cs に、各 ComboBox にバインドさせるプロパティと、
コレクションへの項目の追加処理を記載します。
using System.Collections.Generic; using System.Collections.ObjectModel; using System.Windows; using System.Windows.Input; namespace WpfSample {public partial class TestWindow : Window
{
public List<int> ComboItems { get; set; } = new List<int>();
public ObservableCollection<int> ComboObservableItems { get; set; } = new ObservableCollection<int>();
public TestWindow() {
InitializeComponent(); this.DataContext = this; }
12
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) { base.OnMouseLeftButtonDown(e); this.ComboItems.Add(10); this.ComboObservableItems.Add(10); } } }
STEP 2/2:Xaml 側に ComboBox を 2 つ追加します。
<Window x:Class="WpfSample.TestWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:WpfSample.Converters" Title="TestWindow" Height="100" Width="450"> <StackPanel Orientation="Horizontal">
<TextBlock Text="Not Observable" Height="30" Margin="0 8 4 0" />
<ComboBox Width="100" Height="30" ItemsSource="{Binding Path=ComboItems}" /> <TextBlock Text="Observable" Height="30" Margin="8 8 4 0" />
<ComboBox Width="100" Height="30" ItemsSource="{Binding Path=ComboObservableItems}" /> </StackPanel> </Window>
上記の例では、マウス左ボタン押下ごとにドロップダウンに 10 を追加することを想定して
います。実行してみると、Observable でない左側のコンボボックスには 10 は追加されず、
Observable な 右 側 の コ ン ボ ボ ッ ク ス に は 10 が 追 加 さ れ る こ と が 確 認 で き ま す 。
System.Collections.ObjectModel.ObservableCollection では、INotifyCollectionChanged と
INotifyProperyChanged インタフェースが実装されています。
4 依存関係プロパティ
依存関係プロパティはカスタムで定義することもできます。以下、カスタムで作成したビ
ューに依存関係プロパティを埋め込みで追加する例と、任意のビューに添付する形で振舞
いを追加する例を紹介します。
13
4.1. 埋め込み
カスタムで作成した依存関係プロパティは、Xaml からも参照可能です。MainWindow に
AspectRatioProperty をカスタムで定義する例を挙げます。
STEP 1/2:DependencyProperty 型の AspectRatioProperty を定義します。
0.75d はデフォルト値です。
// ここでは AspectRatio = Height / Width で定義
public static DependencyProperty AspectRatioProperty = DependencyProperty.Register("AspectRatio",
typeof(double), typeof(MainWindow),
new PropertyMetadata(0.75d));
STEP 2/2:AspectRatio プロパティに対する Getter/Setter を定義します。
public double AspectRatio{
get { return (double)base.GetValue(AspectRatioProperty); } set { base.SetValue(AspectRatioProperty, value); }
}
AspectRatio の値を変更すると、MainWindow の OnPropertyChanged() ハンドラが呼ばれ
ます。これは、3 章のデータバインディングの箇所(補足 2/3)で記載した通りです。
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) {base.OnPropertyChanged(e);
if (e.Property == AspectRatioProperty) { // Windowのサイズ変更の処理 (Width固定)
this.Height = this.ActualWidth * (double)e.NewValue; } }
4.2. 添付 Behavior
依存関係プロパティをビューの外部で定義して、定義した依存関係プロパティが変化し
たときに何らかの振舞い(Behavior)を任意のビューに追加する、ということができます。
例えば、任意の Window に対し、次の 2 つの振舞いを添付する例を挙げます。
1. ”appendInvokedTime” を添付プロパティに設定しておくと、Window が起動した
ときに、タイトルに起動時刻を追記する。
14
2. ”showVmProps” を添付プロパティに設定すると、Window の VM の Public な
インスタンスプロパティのリストをメッセージボックスで表示する。
まず、添付 Behavior を定義します。クラスを Static で定義することと、PropertyMetadata
にコールバックを設定していることがポイントです。
// SampleBehavior.cs using System; using System.Linq; using System.Windows; namespace WpfSample.Behavior {public static class SampleBehavior
{
public static readonly DependencyProperty SampleProperty = DependencyProperty.RegisterAttached("Sample",
typeof(string),
typeof(SampleBehavior),
new PropertyMetadata(string.Empty,
new PropertyChangedCallback(SampleChanged))); public static string GetSample(DependencyObject o) => o.GetValue(SampleProperty) as string;
public static void SetSample(DependencyObject o, string value) => o.SetValue(SampleProperty, value);
static void SampleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
var newValue = e.NewValue as string; var window = d as Window;
if (window != null) {
if (newValue == "appendInvokedTime") {
window.ContentRendered += WindowContentRendered; } else if (newValue == "showVmProps") {
var vm = window.DataContext; var props =
vm.GetType().GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
MessageBox.Show(string.Join("\r\n", props.Select(x => x.Name)), "VMのプロパティ");
} } }
static void WindowContentRendered(object ob, EventArgs e) {
var window = (Window)ob;
window.ContentRendered -= WindowContentRendered;
15
}} }
次に、MainWindow.xaml に添付 Behavior の Sample プロパティの初期値を設定します。
<!-- MainWindow.xaml -->
<Window x:Class="WpfSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behv="clr-namespace:WpfSample.Behavior" behv:SampleBehavior.Sample="appendInvokedTime" Title="MainWindow" Height="350" Width="525">
そして、マウス左ボタン押下時に、Sample プロパティの値を ”showVmProps” に変更しま
す。
public partial class MainWindow : System.Windows.Window
{
public MainWindow() {
InitializeComponent(); }
protected override void OnMouseLeftButtonDown(System.Windows.Input.MouseButtonEventArgs e) {
base.OnMouseLeftButtonDown(e);
Behavior.SampleBehavior.SetSample(this, "showVmProps"); }
}
16
5 MVVM アーキテクチャ
今までの例では、MainWindow にバインド対象のプロパティを書いていましたが、プロ
グラムの作りとしてはよくありません。プログラムが単純な構成要素に分かれていないか
らです。ここで、MVVM アーキテクチャ (または、MVVM デザインパターン) を導入しま
す。MVVM アーキテクチャは WPF が最初らしいです。
WEB で読める MVVM の解説記事を 2 つ紹介します。
Model-View-ViewModel デザインパターンによる WPF アプリケーション
https://msdn.microsoft.com/ja-jp/magazine/dd419663.aspx
GUI アーキテクチャパターンの基礎から MVVM パターンへ
https://www.slideboom.com/presentations/591514/
2 つ目の記事は、WPF のフレームワークの「Livet」の作者であり Microsoft MVP でもある
ugaya40 氏による PowerPoint の資料です。
以下、自分の理解している範囲で MVVM について解説します。Model と View は古来か
らある MVC アーキテクチャでの Model および View と同じです。ViewModel が何である
かが分かれば、MVVM も分かると思います。
5.1. ViewModel の概念
ViewModel (以降、VM と略します)は、”View が直接参照するモデル” であり、View
(MainWindow など) と Model (Entity など) を仲介する役割を持ったオブジェクトです。
このことは次の図で表せます。
V – VM – M
View と VM は疎結合にするルールがあります (MVVM の規約)。疎結合は、データバイン
ディングによって実現されます。MVVM の規約を守らなくても実装できますが、多分メン
テしにくいプログラムになります (少なくとも、WPF での標準的な作り方ではないです)。
WPF では、VM は DataContext に設定するオブジェクトです。DataContext 依存関係プ
ロパティは System.Windows.FrameworkElement で定義されていて、例えば、ユーザコント
ロール単位でもボタン単位でも VM を設定できます。
ユーザコントロール - ユーザコントロール用の VM
ボタン - ボタン用の VM
17
両者に同じ VM を結び付けることもできます。
VM の概念は、ASP.NET MVC でも Knockout.js でも出現します。ASP.NET MVC で
は、View (cshtml) に @model で指定するオブジェクトが (C#で書かれた) VM です。
Knockout.js では、ko.applyBindings() で指定するオブジェクトが (JavaScript で書かれた)
VM です。いずれの場合も、VM は View が直接参照するモデルです。
では、3 章の冒頭で紹介した例を、MVVM アーキテクチャで書き直してみます。
STEP 1/4:ViewModelBase クラスを追加します。後述する WPF のフレームワークを
使う場合は、VM のベースは既に用意されているので追加する必要はありません。
// ViewModelBase.cs using System.ComponentModel; namespace WpfSample.ViewModel {public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged(string propertyName) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
} }
STEP 2/4:ViewModelBase を継承して MainWindowViewModel クラスを追加し、
ButtonContent プロパティを定義します。
// MainWindowViewModel.cs
namespace WpfSample {
public class MainWindowViewModel : ViewModel.ViewModelBase
{
object buttonContent; public object ButtonContent {
get { return buttonContent; } set { if (buttonContent != value) { buttonContent = value; base.RaisePropertyChanged("ButtonContent"); } } } } }
18
STEP 3/4:MainWindow の DataContext に MainWindowViewModel を設定します。
ここではモーダルダイアログを想定しています。モードレスダイアログの場合は
VM インスタンスの設定は Xaml では行いません。VM インスタンスのスコープと
モードレスダイアログのスコープを一致させるためです。
<!-- MainWindow.xaml -->
<Window x:Class="WpfSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfs="clr-namespace:WpfSample"
Title="MainWindow" Height="350" Width="525"> <Window.DataContext>
<wpfs:MainWindowViewModel /> </Window.DataContext>
<Grid>
<Button Content="{Binding Path=ButtonContent}" Width="100" Height="30" /> </Grid>
</Window>
STEP 4/4:MainWindow.xaml.cs でマウス左ボタンの押下時に ButtonContent に値を
設定するコードを追加します。
// MainWindow.xaml.cs
using System.Windows.Input;
namespace WpfSample {
public partial class MainWindow : System.Windows.Window
{
public MainWindow() {
InitializeComponent(); }
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e) {
base.OnMouseLeftButtonDown(e);
var vm = this.DataContext as MainWindowViewModel; vm.ButtonContent = "!?"; } } }
VM を導入したことでコードが分離され、見通しがよくなりました。プログラムの規模が大
きくなってくると、VM なしで大量に書いたコードはメンテナンスしにくくなるので、VM
を使ってコーディングすることを推奨します。
19
6 WPF のフレームワークの紹介
WPF の入門の段階では、フレームワークを使わない方が WPF の理解が進むと思われる
のですが、ある程度慣れてきたら、フレームワークを使った方が開発効率が上がると思いま
す (フレームワーク自体の使い方に慣れることも必要になりますが)。
以下、WPF のフレームワークとして Livet と Prism を紹介します。Livet は国産の OSS
のフレームワークで、作者は Microsoft MVP の ugaya40 氏です。ドキュメントが日本語で
あることとコンパクトであることが特徴です。一方の Prism は Livet よりも多機能な OSS
のフレームワークで、Microsoft 内の人達が書いているそうです。
6.1. Livet
Livet は、GitHub の次の URL で公開されています。
https://github.com/ugaya40/Livet
VisualStudio の NuGet パッケージマネージャーからもインストールできます。
ライセンスは zlib/libpng ライセンスです。
以下、Livet により提供される機能を幾つか例示します。ちなみに、Livet のコードはコメ
ントが全て日本語で、かつ、規模がそれほど大きくないため、割と読めると思います。
(1) Livet.ViewModel
INotifyPropertyChanged インタフェースを実装した Livet.NotificationObject があり、
Livet.ViewModel はそれを継承した抽象クラスです。RaisePropertyChanged() で、文字列
でなく式木でプロパティを渡せるようになっている点が特徴です。これにより、プロパティ
名を変更したときの VisualStudio でのリファクタリングが効くようになります。
§5.1 の MainWindowViewModel.cs を書き換えると次のようになります。
// MainWindowViewModel.cs namespace WpfSample {public class MainWindowViewModel : Livet.ViewModel
20
object buttonContent;public object ButtonContent {
get { return buttonContent; } set { if (buttonContent != value) { buttonContent = value; base.RaisePropertyChanged(() => ButtonContent); } } } } }
(2) Livet.Behaviors.LivetCallMethodAction
Livet では、VM のメソッドを、コマンドバインドを使わずに View から呼ぶ方法が提供
されています。例えば、MainWindow の ContentRendered イベントの発生時に VM の
Initialize() メソッドを呼ぶ例を挙げます。
<!-- MainWindow.xaml --><Window x:Class="WpfSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<wpfs:MainWindowViewModel /> </Window.DataContext>
<i:Interaction.Triggers>
<i:EventTrigger EventName="ContentRendered">
<l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize" /> </i:EventTrigger> </i:Interaction.Triggers> </Window> // MainWindowViewModel.cs namespace WpfSample {
public class MainWindowViewModel : Livet.ViewModel
{
public void Initialize() {
// 何らかの初期化
} } }
21
(3) Livet.EventListeners.PropertyChangedEventListener
Livet では、指定した Livet.NotificationObject の PropertyChanged イベントのリスナー
が提供されています。例えば、Model のプロパティの変更を VM が受け取るために使えま
す。例として、Livet.NotificationObject を継承した Entity を抽象クラスとして用意し、
Entity クラスを継承した Book クラスを作成します。そして、VM 上でリスナーを生成し
Book オブジェクトの Name プロパティの変更を受け取る例を挙げます。
--- Model ---
// Entity.cs namespace WpfSampleLivet.Models {public abstract class Entity : Livet.NotificationObject
{
public abstract string Key { get; } }
}
// Book.cs
namespace WpfSampleLivet.Models {
public class Book : Entity
{
string id; public string Id {
get { return id; } set { if (id != value) { id = value; base.RaisePropertyChanged(() => Id); } } } string name; public string Name {
get { return name; } set { if (name != value) { name = value; base.RaisePropertyChanged(() => Name); /* (B) */ } } }
public override string Key => Id; }
22
--- VM ---
// MainWindowViewModel.cs using Livet.EventListeners; using WpfSampleLivet.Models; namespace WpfSample {public class MainWindowViewModel : Livet.ViewModel
{
Book book = new Book(); public string Name {
get { return book.Name; } set { book.Name = value; } }
public void Initialize() {
var listener =
new PropertyChangedEventListener(book,
(_, __) => base.RaisePropertyChanged(() => Name) /* (C) */); base.CompositeDisposable.Add(listener);
}
public void OnAct() {
this.Name = "Buttonがクリックされました。"; /* (A) */
} } }
--- View ---
<!-- MainWindow.xaml -->
<Window x:Class="WpfSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfs="clr-namespace:WpfSample"
xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" Title="MainWindow" SizeToContent="WidthAndHeight">
<Window.DataContext>
<wpfs:MainWindowViewModel /> </Window.DataContext>
<i:Interaction.Triggers>
<i:EventTrigger EventName="ContentRendered">
<l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize" /> </i:EventTrigger>
</i:Interaction.Triggers>
<StackPanel>
<Button Content="Click" Width="100" Height="30"> <i:Interaction.Triggers>
23
<i:EventTrigger EventName="Click"><l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="OnAct" /> </i:EventTrigger>
</i:Interaction.Triggers> </Button>
<TextBlock Text="{Binding Path=Name}" Width="150" Height="30" /> </StackPanel>
</Window>
イベントの通知パスは次の通りです (デバッガで追えます)。
View: Button.Click → VM: OnAct() → Model: Book.Name /*(A)*/ →
Model: Book.Name: RaisePropertyChanged /*(B)*/ →
VM: PropertyChangedEventListner のハンドラ /*(C)*/
(4) Livet.Messaging.InteractionMessenger
Livet では、VM から View に対してメッセージを送る仕組みが提供されていて、(VM の)
Messenger オブジェクトのデータバインディング経由で実行されます。以下、Window を起
動する TransitionMessage を、
モード付で Window を起動する場合と、モードなしで Window
を起動する場合の 2 つの場合について例示します。
--- VM ---
// MainWindowViewModel.cs using Livet.EventListeners; using WpfSampleLivet; namespace WpfSample {public class MainWindowViewModel : Livet.ViewModel
{
TestWindow2ViewModel vmTestWindow2; public void OpenModal()
{
var message = new Livet.Messaging.TransitionMessage("KeyTestWindow1"); base.Messenger.Raise(message);
}
public void OpenModeless() {
if (vmTestWindow2 == null) {
vmTestWindow2 = new TestWindow2ViewModel(); }
var message = new Livet.Messaging.TransitionMessage(typeof(TestWindow2), vmTestWindow2,
Livet.Messaging.TransitionMode.NewOrActive, "KeyTestWindow2"); base.Messenger.Raise(message);
} } }
24
--- View ---
<!-- MainWindow.xaml -->
<Window x:Class="WpfSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfs="clr-namespace:WpfSample"
xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" Title="MainWindow" SizeToContent="WidthAndHeight">
<Window.DataContext>
<wpfs:MainWindowViewModel /> </Window.DataContext>
<i:Interaction.Triggers>
<l:InteractionMessageTrigger Messenger="{Binding Path=Messenger}"
MessageKey="KeyTestWindow1">
<l:TransitionInteractionMessageAction Mode="Modal"
WindowType="{x:Type wpfs:TestWindow}" /> </l:InteractionMessageTrigger>
<l:InteractionMessageTrigger Messenger="{Binding Path=Messenger}"
MessageKey="KeyTestWindow2">
<l:TransitionInteractionMessageAction /> </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
<StackPanel>
<Button Content="Modal" Width="100" Height="30"> <i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<l:LivetCallMethodAction MethodTarget="{Binding}"
MethodName="OpenModal" /> </i:EventTrigger>
</i:Interaction.Triggers> </Button>
<Button Content="Modeless" Width="100" Height="30"> <i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<l:LivetCallMethodAction MethodTarget="{Binding}"
MethodName="OpenModeless" /> </i:EventTrigger> </i:Interaction.Triggers> </Button> </StackPanel> </Window>
VM で Message を生成して View に送信し、View に (Message を受取ったときに実行する)
MessageAction を定義します。モードなしの場合は、MainWindow の VM で Message を
new する際に、TestWindow2 の VM のインスタンスを渡している点にご注意下さい。
ちなみに、Window でなくメッセージダイアログを表示する Message もあります。
Information ダイアログの場合、View 側で次のようにメッセージアクションを定義し、
25
<i:Interaction.Triggers><l:InteractionMessageTrigger Messenger="{Binding Path=Messenger}"
MessageKey="KeyInfo">
<l:InformationDialogInteractionMessageAction /> </l:InteractionMessageTrigger>
</i:Interaction.Triggers>
VM で次のようにメッセージを生成・送信します。
var message = new Livet.Messaging.InformationMessage("text", "caption", "KeyInfo"); base.Messenger.Raise(message);
6.2. Prism
Prism は、GitHub の次の URL で公開されています。
https://github.com/PrismLibrary/Prism
2017/2/26 に、Prism.Wpf の Version 6.3.0-pre2 がリリースされたようです。
NuGet パッケージマネージャーからインストールできます。ライセンスは Apache-2.0。
Prism.Wpf をインストールすると、それが依存している Prism.Core もインストールされ
ます。
DI コンテナの Unity の拡張コンポーネント Prism.Unity もインストールしておきます
(UnityBootstrapper が使えます ((5) で後述します))。
以下、Prism により提供される機能を幾つか例示します。
(1) Prism.Mvvm.BindableBase
Prism では、System.ComponentModel.INotifyPropertyChanged インタフェースを実装し
ている Prism.Mvvm.BindableBase を継承して VM を作成します。BindableBase は、VM
のデータバインド対象プロパティの Setter を簡潔に書く方法を提供します。例えば、次の
26
ような感じです。
public class MainWindowViewModel : Prism.Mvvm.BindableBase
{
object buttonContent; public object ButtonContent {
get { return buttonContent; }
set { base.SetProperty(ref buttonContent, value); } } }
(2) Prism.Commands.DelegateCommand
§3.3 /RelayCommand と同じものが、Prism.Commands.DelegateCoommand として提供
されています。コマンドのアクション (省略不可) とコマンドの実行可能条件を計算するデ
リゲート (省略可:省略すると常に true) を次のように指定します。
public class MainWindowViewModel : Prism.Mvvm.BindableBase
{
public Prism.Commands.DelegateCommand TestCommand => new Prism.Commands.DelegateCommand(() => {
System.Windows.MessageBox.Show("Invoked TestCommand."); }, () => { return /* 何らかの条件 (型はbool) */; }); }
コマンドの実行可能条件を再評価するには、
TestCommand.RaiseCanExecuteChanged();を明示的に呼びます(ボタンの Enable/Disable を更新できます)
。
(3) Prism.Mvvm.ViewModelLocator
Prism では、あるルールの下で View と VM を自動で結び付ける仕組みがあります。ルー
ルは次の通りです。
・R1)プロジェクト直下の Views フォルダにビュー xxxxx(名称)をおく。
・R2)プロジェクト直下の ViewModels フォルダに xxxxxViewModel をおく (VM の
名称はビューの名称の末尾に「ViewModel」を付ける)。
ここで言う「ビュー」は、System.Windows.FrameworkElement を継承したクラス (ユーザ
コントロールやウィンドウなど) です。
View と VM を自動で結び付ける設定を有効にするには、Prism.Mvvm.ViewModelLocator
の AutoWireViewModel 依存関係プロパティに次のように true を設定します。例えば、
Views/SampleUserControl.cs と ViewModels/SampleUserControlViewModel.cs を用意し
27
ます(VM は BindableBase を継承します)
。
<UserControl x:Class="WpfSamplePrism.Views.SampleUserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True" Width="300" Height="300">
<StackPanel>
<TextBlock Text="サンプル UserControl" Width="200" Height="30" />
<TextBox Text="{Binding Path=SampleText}" Width="100" Height="30" IsReadOnly="True" /> </StackPanel>
</UserControl>
(4) Prism.Interactivity.InteractionRequest.InteractionRequestTrigger
Prism では (Livet にもあるように)、VM から View に対してメッセージを送る仕組みが
あります。Prism.Interactivity.PopupWindowAction を使う (VM のコマンドから View の
PopupWindowAction を起動する) 例を挙げます。PopupWindowAction が View 側で起動
されると Window が自動生成され、そこに指定した Title (string 型) と Content (object
型) が埋め込まれます。View 側で Window に埋め込むビュー (例えば SampleUserControl
(UserControl 型)) を指定することもできます。
関連する参考情報の URL を挙げておきます。
Interactivity Code Sample using the Prism Library 5.0 for WPF
https://code.msdn.microsoft.com/Interactivity-Code-Sample-9fe8963f
// MainWindowViewModel.cs using Prism.Commands; using Prism.Interactivity.InteractionRequest; using Prism.Mvvm; namespace WpfSamplePrism.ViewModels {public class MainWindowViewModel : BindableBase
{
public MainWindowViewModel() {
this.NotificationRequest = new InteractionRequest<INotification>(); this.SampleRequest = new InteractionRequest<INotification>(); }
public DelegateCommand ShowPopupCommand => new DelegateCommand(() => {
this.NotificationRequest.Raise(
new Notification { Content = "Notification.", Title = "Test Notification" });
28
public DelegateCommand ShowSampleUserControlCommand => new DelegateCommand(() => {
this.SampleRequest.Raise(
new Notification { Title = "サンプル" }); });
// (cf.) https://msdn.microsoft.com/en-us/library/ff921081(v=pandp.40).aspx
public InteractionRequest<INotification> NotificationRequest { get; set; } public InteractionRequest<INotification> SampleRequest { get; set; } }
}
<!-- MainWindow.xaml -->
<Window x:Class="WpfSamplePrism.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" xmlns:views="clr-namespace:WpfSamplePrism.Views"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True" Title="MainWindow" Height="350" Width="525"> <i:Interaction.Triggers>
<prism:InteractionRequestTrigger
SourceObject="{Binding Path=NotificationRequest, Mode=OneWay}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True" /> </prism:InteractionRequestTrigger>
<prism:InteractionRequestTrigger
SourceObject="{Binding Path=SampleRequest, Mode=OneWay}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True"> <prism:PopupWindowAction.WindowContent> <views:SampleUserControl /> </prism:PopupWindowAction.WindowContent> </prism:PopupWindowAction> </prism:InteractionRequestTrigger> </i:Interaction.Triggers>
<StackPanel VerticalAlignment="Center">
<Button Content="Notification" Width="100" Height="30" Command="{Binding Path=ShowPopupCommand}" /> <Button Content="Sample" Width="100" Height="30"
Command="{Binding Path=ShowSampleUserControlCommand}" /> </StackPanel>
</Window>
(5) Prism.Unity.UnityBootstrapper
Prism.Unity は、Microsoft.Practices.Unity の拡張パッケージです。Microsoft.Practices.
Unity アセンブリは 「Unity」 という名称の DI コンテナを提供します。DI コンテナ用の
インタフェースとして Microsoft.Practices.Unity.IUnityContainer があり、例えば、次のよ
うに、Resolve() メソッドで指定クラスのインタンスを生成することができます。
29
using Microsoft.Practices.Unity; using Prism.Commands; using Prism.Mvvm; namespace WpfSamplePrism.ViewModels {public class MainWindowViewModel : BindableBase
{
IUnityContainer Container { get; } = new UnityContainer(); public DelegateCommand ShowTestWindowCommand =>
new DelegateCommand(() => {
this.Container.Resolve<TestWindow>().Show(); }); } }
Prism.Unity アセンブリは、初期化処理用のフレームワークである UnityBootstrapper
( 抽 象 ク ラ ス ) を 提 供 し ま す 。 抽 象 ク ラ ス で あ る た め 継 承 し て 使 い ま す 。 例 え ば 、
UnityBootstrapper を 継 承 し て SampleBootstrapper を 作 成 す る 例 を 挙 げ ま す 。
UnityBootstrapper は、IUnityContainer 型の Container プロパティを持っています。ここで
Prism.Unity では、MainWindow のことを Shell と呼びます。
// SampleBootstrapper.cs
using System.Windows; using Prism.Unity;
namespace WpfSamplePrism {
public class SampleBootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell() => new MainWindow(); protected override void InitializeShell()
{
base.InitializeShell();
// 例えば、モードレスダイアログ用のVMインスタンスの生成を記述する。
Application.Current.MainWindow = (Window)this.Shell; Application.Current.MainWindow.Show();
} } }
SampleBootstrapper は、App.xaml.cs の OnStartup() メソッドで new して実行します。
// App.xaml.cs
using System.Windows;
30
{public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
var bootstrapper = new SampleBootstrapper(); bootstrapper.Run();
} } }
UnityBootstrapper を App.xaml.cs の OnStartup() で使う場合、App.xaml の StartupUri
指定で実行されるウィンドウ表示処理と処理が重複するため、StartupUri をコメントアウ
トもしくは削除します (そうしないとアプリケーションの実行時にウィンドウが 2 つ起動
します)。
<!-- App.xaml -->
<Application x:Class="WpfSamplePrism.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <!--StartupUri="MainWindow.xaml">--> <Application.Resources> </Application.Resources> </Application>
(6) モジュール
Prism では、モジュールの初期化用 (※1) のフレームワークが提供されています。ここ
では、1 つのアセンブリ(VisualStudio で言うプロジェクト)をモジュールとします。
(※1) 初期化処理が強調されることには、背景があると思われます。WPF のグラフィック
スには DirectX(特に、Direct3D)が使われていることを§2.1 で記載しました。3D グラフ
ィックスでは、初期化処理で記述するコード量が多いです。そこで、初期化のコードを整理
するフレームワークが提供されていると推察されます。
使い方を例示します。例えば、カスタムコントロール用の CustomControls プロジェク
ト(モジュール)を作成し、その初期化処理を WpfSamplePrism プロジェクトで行います。
STEP 1/3:CustomControls プロジェクトを作成し、NuGet パッケージマネージャーから
Prism.Core と Prism.Unity をインストールします。
31
STEP 2/3:Prism.Modularity.IModule インタフェースを実装するクラス(この例では、
EntryPoint クラス)を作成します。RegionManager については (7) で触れます。
// EntryPoint.cs using Microsoft.Practices.Unity; using Prism.Regions; namespace CustomControls {public class EntryPoint : Prism.Modularity.IModule
{
// Prism.Unity フレームワークにより Setter にインスタンスが設定される
[Dependency]
public IUnityContainer Container { get; set; } [Dependency]
public IRegionManager RegionManager { get; set; } /// <summary>
/// モジュールを初期化します。
/// </summary>
public void Initialize() {
// 何らかの初期化処理を行います。
} } }
STEP 3/3:WpfSamplePrism プロジェクトの SampleBootstrapper クラス((5)で書いた
もの)で、ConfigureModuleCatalog() メソッドをオーバライドして、CustomControls
プロジェクトの EntryPoint を AddModule()で追加します (追加すると、EntryPoint の
Initialize() メソッドが Prism フレームワークから呼ばれます)。
// SampleBootstrapper.cs using System.Windows; using Prism.Unity; namespace WpfSamplePrism {public class SampleBootstrapper : UnityBootstrapper
{
protected override void ConfigureModuleCatalog() {
base.ConfigureModuleCatalog();
var catalog = (Prism.Modularity.ModuleCatalog)this.ModuleCatalog; // AddModule()によりEntryPointのInitialize()が呼ばれる
catalog.AddModule(typeof(CustomControls.EntryPoint)); }
} }
32
確かに、初期化処理が整理した形で書けそうです。
(7) Prism.Regions.RegionManager
MainWindow が Shell と呼ばれていることからも分かるように、
Prism では 1 つの Window
を使い続ける設計が多いことが想定されています。すると、1 つの Window を複数の領域に
分けて、その領域ごとにビューを割り当てる、という設計が自然に導かれます。この各領域
は Region と呼ばれ、Region のコレクションを管理するために、RegionManager が Prism
により提供されています。
(6)で書いた CustomControls プロジェクトと WpfSamplePrism プロジェクトの例の上で、
RegionManager の使い方を例示します。
STEP 1/3: WpfSamplePrism プロジェクトの Shell(MainWindow.xaml)を 3 つの Region
に分けて RegionName を設定します(この例では、”ExplorerRegion”、”OutputRegion”、
”WorkingRegion”)
。
<!-- MainWindow.xaml -->
<Window x:Class="WpfSamplePrism.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True" Title="MainWindow" Height="350" Width="525"> <Window.Content>
<DockPanel LastChildFill="True"> <DockPanel.Children>
<Border DockPanel.Dock="Left" Width="100" Background="Aqua"> <Border.Child>
<ContentControl prism:RegionManager.RegionName="ExplorerRegion" /> </Border.Child>
</Border>
<Border Background="Azure"> <Border.Child>
<DockPanel LastChildFill="True"> <DockPanel.Children>
<Border DockPanel.Dock="Bottom" Height="70"
Background="LightBlue"> <Border.Child>
<ContentControl
prism:RegionManager.RegionName="OutputRegion" /> </Border.Child>
</Border>
<Border Background="Aquamarine"> <Border.Child>
<ContentControl
prism:RegionManager.RegionName="WorkingRegion" /> </Border.Child>
</Border>
33
</DockPanel> </Border.Child> </Border> </DockPanel.Children> </DockPanel> </Window.Content> </Window>STEP 2/3:CustomControls プロジェクトで、”ExplorerRegion” に埋め込むことを
想定した View(SampleExplorer.xaml)を VM と Model と共に作成します。
--- Model ---
// ControlMessages.cs
namespace CustomControls.Models {
public class ControlMessages
{
public string ExplorerTitle { get; set; } public string OutputTitle { get; set; } public string WorkingTitle { get; set; } public ControlMessages() { Initialize(); } void Initialize() { ExplorerTitle = "エクスプローラー"; OutputTitle = "出力"; WorkingTitle = "git1"; } } }
--- View ---
<!-- SampleExplorer.xaml --><UserControl x:Class="CustomControls.Views.SampleExplorer"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"> <UserControl.Content>
<StackPanel>
<StackPanel.Children>
<TextBlock Text="{Binding Path=Messages.ExplorerTitle}" /> <TreeView Background="Transparent" Margin="4 8 4 0"> <TreeView.Items>
<TreeViewItem Header="1:00"> <TreeViewItem.Items>
34
<TreeViewItem Header="1:33" /> <TreeViewItem Header="1:40" /> </TreeViewItem.Items> </TreeViewItem> <TreeViewItem Header="2:00"> <TreeViewItem.Items> <TreeViewItem Header="2:20" /> <TreeViewItem Header="2:55" /> </TreeViewItem.Items> </TreeViewItem> </TreeView.Items> </TreeView> </StackPanel.Children> </StackPanel> </UserControl.Content> </UserControl>--- VM ---
// SampleExplorerViewModel.cs using Microsoft.Practices.Unity; using CustomControls.Models; namespace CustomControls.ViewModels {public class SampleExplorerViewModel : Prism.Mvvm.BindableBase
{
[Dependency]
public ControlMessages Messages { get; set; } }
}
STEP 3/3: CustomControls プロジェクトの EntryPoint で、RegionName ”ExplorerRegion”
に対して SampleExplorer(UserControl)を割り当てる設定を、RegionManager により
行います。
// EntryPoint.cs using Microsoft.Practices.Unity; using Prism.Regions; using CustomControls.Models; using CustomControls.Views; namespace CustomControls {public class EntryPoint : Prism.Modularity.IModule
{
// Prism.Unity フレームワークにより Setter にインスタンスが設定される
[Dependency]
public IUnityContainer Container { get; set; } [Dependency]
35
/// <summary>/// モジュールを初期化します。
/// </summary>
public void Initialize() {
// モデルを破棄するスコープの設定
this.Container.RegisterType<ControlMessages>( new ContainerControlledLifetimeManager());
// コンテナにビューを登録
this.Container.RegisterType<object, SampleExplorer>(nameof(SampleExplorer)); // RegionManagerにビューを登録
this.RegionManager.RequestNavigate("ExplorerRegion", nameof(SampleExplorer)); } } }