• 検索結果がありません。

目次 1 はじめに WPF とは データバインディング バインディングモード コンバータ コマンド コレクションのデータバインディング 依存関係プロパティ...

N/A
N/A
Protected

Academic year: 2021

シェア "目次 1 はじめに WPF とは データバインディング バインディングモード コンバータ コマンド コレクションのデータバインディング 依存関係プロパティ..."

Copied!
61
0
0

読み込み中.... (全文を見る)

全文

(1)

WPF 入門ノート

2017 年 3 月 12 日 (日)

小寺 広志

(2)

目次

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

(3)

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

(4)

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

(5)

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」

上記の幾つかは、以降で触れます。

(6)

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 提供) です。

(7)

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 から

呼ぶようにします。

(8)

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 プロパティの内容を確認します。

(9)

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

を設定した方が意図が明確になります。

(10)

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 依存関係プロパティ

(11)

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); }

(12)

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 のダン

プ用コンバータを実際に書いてみると、コンバータの理解が深まると思います。

(13)

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")); }

(14)

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; }

(15)

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 依存関係プロパティ

依存関係プロパティはカスタムで定義することもできます。以下、カスタムで作成したビ

ューに依存関係プロパティを埋め込みで追加する例と、任意のビューに添付する形で振舞

いを追加する例を紹介します。

(16)

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 が起動した

ときに、タイトルに起動時刻を追記する。

(17)

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;

(18)

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"); }

}

(19)

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

(20)

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"); } } } } }

(21)

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

を使ってコーディングすることを推奨します。

(22)

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

(23)

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() {

// 何らかの初期化

} } }

(24)

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; }

(25)

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>

(26)

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);

} } }

(27)

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 側で次のようにメッセージアクションを定義し、

(28)

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 を簡潔に書く方法を提供します。例えば、次の

(29)

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 を用意し

(30)

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" });

(31)

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() メソッドで指定クラスのインタンスを生成することができます。

(32)

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;

(33)

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 をインストールします。

(34)

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)); }

} }

(35)

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>

(36)

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>

(37)

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]

(38)

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)); } } }

以上の準備の上で、WpfSamplePrism プロジェクトから Shell を起動すると、次の図のよう

に左側の ExplorerRegion に SampleExplorer が埋め込まれることが確認できます。

7 VisualTree

Html で書かれたドキュメントを DOM ツリーと呼ぶのに対し、Xaml で書かれたドキュ

メントを VisualTree と呼びます (WPF は System.Windows.Media.VisualTreeHelper とい

う名前のヘルパークラスも提供しています)。

VisualTree でまず押さえておくべきことは、明示的に指定しなければ、子の DataContext

は親の DataContext を継承する、ことです。これが成立しないと、DataContext の指定の手

参照

関連したドキュメント

断面が変化する個所には伸縮継目を設けるとともに、斜面部においては、継目部受け台とすべり止め

週に 1 回、1 時間程度の使用頻度の場合、2 年に一度を目安に点検をお勧め

LLVM から Haskell への変換は、各 LLVM 命令をそれと 同等な処理を行う Haskell のプログラムに変換することに より、実現される。

参考資料ー経済関係機関一覧(⑤各項目に関する機関,組織,企業(2/7)) ⑤各項目に関する機関,組織,企業 組織名 概要・関係項目 URL

(1) テンプレート編集画面で、 Radius サーバ及び group server に関する設定をコマンドで追加して「保存」を選択..

次に、第 2 部は、スキーマ療法による認知の修正を目指したプログラムとな

48.10 項及び 48.11 項又は上記(Ⅱ)に属するものを除くものとし、ロール状又はシート状

太宰治は誰でも楽しめることを保証すると同時に、自分の文学の追求を放棄していませ