t90 きっと怖くないmvvm & mvpvm
TRANSCRIPT
わんくま同盟 東京勉強会 #90
きっと怖くない MVVM&MVPVM
暁 紫電@akatukisiden
わんくま同盟 東京勉強会 #90
自己紹介
• HN: 暁 紫電• Twitter: @akatukisiden• 本名 : 伊藤 伸男• フリーランス プログラマー• 使用言語
–C++–C#–C++/CLI
わんくま同盟 東京勉強会 #90
アジェンダ
• 目的• MVVM とは• View と ViewModel の分
離• 細かいことは程々にし
て実装してみた• MVVM での画面遷移• MVVM まとめ
• MVPVM とは• とりあえず実装してみた• MVPVM での画面遷移• MVPVM まとめ• まとめ
わんくま同盟 東京勉強会 #90
このセッションの目的
• 細かいことは置いておいて、とりあえず MVVM,MVPVM っぽい形でプログラムを書けるようにする。
• MVVM でのナビゲーション手法について理解する
• MVPVM でのナビゲーションについて理解する
• 疎結合、密結合、コードビハインドなどの用語をできる限り使わずに説明する
わんくま同盟 東京勉強会 #90
MVVM(Model-View-ViewModel) とは
• 最近流行りの UI アーキテクチャパターン• いくつかの理由により XAML 系フレームワーク
(WPF,Silverlight,WinRT) では必須と言われている• UI(View) とビジネスロジック (Model) のあいだに
ViewModel を置くことで二つを分離する。• View ・ ViewModel 間のやり取りはデータバインドを用
いる
わんくま同盟 東京勉強会 #90
View• ユーザーインターフェース• UI への出力と UI からの入力を担当する。• FrameworkElement の派生クラス• XAML の記述 + 対応する (partial )class
• View の必要な情報を保持公開• View からの入力やコマンドを処
理しModel を呼び出す
ViewModel Model• ビジネスロジック• プログラムの中核となる処理• View と ViewModel 以外の部
分
わんくま同盟 東京勉強会 #90
View と ViewModel の分離(疎結合と密結合)
• MVVMの説明などでよく使われる用語、疎結合と密結合
• View と ViewModel を密結合にならないようにし、疎結合に保つと良いらしい
• 「疎結合に保つ」「密結合になってしまっている」という記述はよく見るが具体的にどのような状況を疎結合・密結合と言うのか書かれていることはあまりない
• もしかしたら一般的な用語で説明する必要もないのかもしれないが、少なくとも自分にとって MVVM/MVPVM の文脈でしか聞かない言葉
わんくま同盟 東京勉強会 #90
さまざまな資料の記述を総合すると
• View/ViewModel で互いのインスタンスや型名を直接扱うと密結合
• データバインドを使えば疎結合
わんくま同盟 東京勉強会 #90
具体的に何が許されて何が許されないのか
• 直接触れると密結合– View は ViewModel が特定の型であることに依存してはいけな
い– View で ViewModel のインスタンスを扱ってはいけない– ViewModel は View が特定の型であることに依存してはいけな
い– ViewModel で View のインスタンスを扱ってはいけない
• データバインドは疎結合– View は ViewModel が INotifyPropertyChanged を
実装していることに依存してよい。– View は ViewModel が特定の名前のプロパティを
持つことに依存してよい
わんくま同盟 東京勉強会 #90
厳密にいうとこれもだめかもしれない
<Window x:Class="MVVM1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="300" Width="300" > <Window.DataContext > <MainViewModel /> </Window.DataContext>
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = new MainViewModel(); }
View 内で ViewModel の型を扱っている
わんくま同盟 東京勉強会 #90
細かいことは置いておいて
• MainWindow のイベントハンドラに全部の処理を書いた状態から少しずつ修正して MVVM の形にしてみようと思います。
• テキストボックス、ラベル、ボタンを配置し、ボタンを押すとテキストボックスに入力した文字列を加工してラベルに出力するアプリを作る
※ 小さすぎて MVVM にする意味がないとか言わないでください 意味がなくてもとりあえず始めることが大事です。
わんくま同盟 東京勉強会 #90
STEP1 :全部イベントハンドラ等に記述
わんくま同盟 東京勉強会 #90
MainWindow.xaml
<Window x:Class=“Step1.MainWindow” // 略 Title="MainWindow" Height="300" Width="300"> <Grid> <Grid.RowDefinitions><RowDefinition /><RowDefinition /><RowDefinition /></Grid.RowDefinitions> <Button Content="Button" Grid.Row="0“ HorizontalAlignment="Center" VerticalAlignment="Center" Width="120" Click="Button_Click"/> <TextBox Name="textBox1" Grid.Row="1“ HorizontalAlignment="Center" VerticalAlignment="Center“ Height="23" Width="120" /> <Border Grid.Row="2" BorderThickness="1" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center" > <Label Name="label1" HorizontalAlignment="Center" VerticalAlignment="Center" Width="120"/> </Border> </Grid></Window>
わんくま同盟 東京勉強会 #90
MainWindow.xaml.cs
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); }
private void Button_Click(object sender,RoutedEventArgs e) { // なんか複数の関数が絡んだ複雑な処理 string f1 = Func1(textBox1.Text); string f2 = Func2(f1); string f3 = Func3(f2); label1.Content = f3; }
// 頭に B をつける private string Func1(string input) { return “B” + input; }
// 末尾に E をつける private string Func2(string input) { return input+"E"; }
// 順番をひっくり返す。 private string Func3(string input) { var rev = input.Reverse().ToArray(); string ret = new string(rev); return ret; } }
わんくま同盟 東京勉強会 #90
STEP2:MainWindow.xaml.cs は
イベントハンドラだけにしたいので処理内容を別クラスに移す
C++ で云うところのpImpl
わんくま同盟 東京勉強会 #90
MainWindow.xaml.cs
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); }
private MainWindowImpl impl = new MainWindowImpl();
private void Button_Click(object sender, RoutedEventArgs e) { label1.Content = impl.Logic(textBox1.Text); } }
処理を移すための別クラス
別クラスに移した処理の呼び出し
わんくま同盟 東京勉強会 #90
MainWindowImpl.cs
public class MainWindowImpl { public string Logic(string input) { string f1 = Func1(input); string f2 = Func2(f1); string f3 = Func3(f2);
return f3; }
private string Func1(string input) { return "B" + input;}
private string Func2(string input) { return input + "E";}
private string Func3(string input) { var rev = input.Reverse().ToArray(); string ret = new string(rev); return ret; } }
わんくま同盟 東京勉強会 #90
STEP3:入出力 (引数 /戻り値 ) が多く必要になると
記述が大変なのでデータバインディングを使ってみる
わんくま同盟 東京勉強会 #90
MainWindow.xaml
<Window x:Class=“Step3.MainWindow" // 略 Title="MainWindow" Height="300" Width="300"> <Grid><Grid.RowDefinitions><RowDefinition /><RowDefinition /><RowDefinition /></Grid.RowDefinitions> <Button Content="Button" Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Width="120" Click="Button_Click"/>
<TextBox Name="textBox1" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" Height="23" Width="120" Text="{Binding Input}“
<Border Grid.Row="2" BorderThickness="1" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center" > <Label Name=“label1” HorizontalAlignment=“Center” VerticalAlignment=“Center” Width=“120“ Content="{Binding Output}" /> </Border> </Grid></Window>
わんくま同盟 東京勉強会 #90
MainWindow.xaml.cs
public partial class MainWindow : Window { // コントロールからの入力、出力をデータバインドに変換 private MainWindowImpl impl = new MainWindowImpl();
public MainWindow() { InitializeComponent(); this.DataContext = impl; }
private void Button_Click(object sender, RoutedEventArgs e) { impl.Logic(); } }
バインド対象の設定
入出力はバインドしたので引数・戻り値なしになっている
わんくま同盟 東京勉強会 #90
MainWindowImpl.cs
public class MainWindowImpl:BindingSourceBase{ public string input_; public string Input { get { return input_; } set { if (input_ != value) { input_ = value; OnPropertyChanged("Input"); } } }
public void Logic() { string f1 = Func1(Input); string f2 = Func2(f1); string f3 = Func3(f2); Output = f3; }
private string Func1(string input){ 略 } private string Func2(string input){ 略 } private string Func3(string input){ 略 }}
public string output_;public string Output{ get { return output_; } set { if (output_ != value) { output_ = value; OnPropertyChanged("Output"); } }}
わんくま同盟 東京勉強会 #90
BindingSourceBase
public class BindingSourceBase:System.ComponentModel.INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyname)
{
if(PropertyChanged != null)
{
PropertyChanged(this,new PropertyChangedEventArgs(propertyname));
}
}
}
わんくま同盟 東京勉強会 #90
STEP4: 引数・戻り値なしなら直接呼出したいの
で、 イベントハンドラを Impl に移動する。
わんくま同盟 東京勉強会 #90
MainWindow.xaml.cs
public partial class MainWindow : Window { private MainWindowImpl impl = new MainWindowImpl();
public MainWindow() { InitializeComponent();
// バインド対象の設定 this.DataContext = impl;
Button1.Click += impl.Button_Click; }
}
Click イベントに impl クラスの関数を登録
わんくま同盟 東京勉強会 #90
MainWindow.xaml
<Window x:Class=“Step3.MainWindow" // 略 Title="MainWindow" Height="300" Width="300"> <Grid><Grid.RowDefinitions><RowDefinition /><RowDefinition /><RowDefinition /></Grid.RowDefinitions> <Button Content=“Button” Grid.Row=“0” HorizontalAlignment=“Center” VerticalAlignment=“Center” Width=“120” /> <TextBox Name="textBox1" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" Height="23" Width="120" Text="{Binding Input}" /> <Border Grid.Row="2" BorderThickness="1" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center" > <Label Name="label1" HorizontalAlignment="Center" VerticalAlignment="Center" Width="120“ Content="{Binding Output}" /></Border> </Grid></Window>
イベントの登録部分を削除
わんくま同盟 東京勉強会 #90
MainWindowImpl.cs
public class MainWindowImpl:BindingSourceBase{ public void Button_Click(object sender,RoutedEventArgs e) { this.Logic(); }
public string input_; public string Input { get { return input_; } set { 略 } } public string output_; public string Output { get { return output_; } set { 略 } }
public void Logic() { string f1 = Func1(Input); string f2 = Func2(f1); string f3 = Func3(f2); Output = f3; }
private string Func1(string input){ 略 } private string Func2(string input){ 略 } private string Func3(string input){ 略 }}
Button.Click イベントから呼び出される
ビジネスロジック
わんくま同盟 東京勉強会 #90
STEP5:Impl クラスにデータバインドと
ビジネスロジック両方があるとじゃまなのでビジネスロジックをさらに別クラスへ移動する。
わんくま同盟 東京勉強会 #90
MainWindowImpl.cs
public class MainWindowImpl:BindingSourceBase{ private BusinessLogic BL = new BusinessLogic();
public void Button_Click(object sender, RoutedEventArgs e) { Output = BL.Logic(Input); }
public string input_; public string Input { get { return input_; } set { 略 } }
public string output_; public string Output { get { return output_; } set { 略 }}
}
わんくま同盟 東京勉強会 #90
BusinessLogic.cs
public class BusinessLogic{ public string Logic(string Input) { string f1 = Func1(Input); string f2 = Func2(f1); string f3 = Func3(f2); return f3; }
private string Func1(string input){ 略 } private string Func2(string input){ 略 } private string Func3(string input){ 略 }}
関数が増えてクラスが肥大化するのであれば外部とのインターフェースになる関数だけ残して内部実装はさらに別のクラスに移した方がいいかも
わんくま同盟 東京勉強会 #90
STEP6:XAML環境ではロジックもコマンドとしてバインドできるので
イベントハンドラからコマンドバインディングに変更する
※WinForms でも Form クラス等に Action プロパティを作りOn○○メソッド等でアクションを発火するようにすれば可能
わんくま同盟 東京勉強会 #90
MainWindow.xaml.cs
public partial class MainWindow : Window { private MainWindowImpl impl = new MainWindowImpl();
public MainWindow() { InitializeComponent();
// バインド対象の設定 this.DataContext = impl;
// Button1.Click += impl.Button_Click; }
}
削除
わんくま同盟 東京勉強会 #90
MainWindow.xaml
<Window x:Class=“Step6.MainWindow" // 略 Title="MainWindow" Height="300" Width="300"> <Grid><Grid.RowDefinitions><RowDefinition /><RowDefinition /><RowDefinition /></Grid.RowDefinitions> <Button Content="Button" Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Width="120" Command="{Binding MainCommand}“ />
<TextBox Name="textBox1" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" Height="23" Width="120" Text="{Binding Input}" />
<Border Grid.Row="2" BorderThickness="1" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center" > <Label Name="label1" HorizontalAlignment="Center" VerticalAlignment="Center" Width="120" Content="{Binding Output}" /> </Border> </Grid></Window>
わんくま同盟 東京勉強会 #90
MainWindowImpl.cs
public class MainWindowImpl:BindingSourceBase{ private BusinessLogic BL = new BusinessLogic(); public ICommand MainCommand{get;set;}
public MainWindowImpl() { MainCommand = new Microsoft.TeamFoundation.
MVVM.RelayCommand(() => { Output = BL.Logic(Input); }); } public string input_; public string Input { get { return input_; } set { 略 } }
public string output_; public string Output { get { return output_; } set{ 略 } }
ロジックのバインド用のコマンド
コマンドの中身を作成。
わんくま同盟 東京勉強会 #90
(LastSTEP:) クラス名の変更
• MainWindowImpl→ MainViewModel• BindingSourceBase → ViewModelBase• BussinesLogic → MainModel
わんくま同盟 東京勉強会 #90
とりあえず MVVM完成
※但し画面遷移無し
わんくま同盟 東京勉強会 #90
画面遷移とは
• 表示されているデータでは無く、コントロール自体を変更する
• 主にユーザーコントロールやパネルの切り替え
わんくま同盟 東京勉強会 #90
MVVM での画面遷移手法
• ViewModel の型に応じてコントロールを切り替える方法。– データテンプレート
• 表示するデータの型に応じて表示方法を変える機能• 指定した型のデータの表示方法を定義する。
• コントロールの切り替えロジックを呼び出し、新しいコントロールに新しい ViewModel を関連付ける手法。– ViewModel から View を呼び出す。
• 逆方向バインディングコマンド• Behavior• TriggerAndAction
わんくま同盟 東京勉強会 #90
とりあえずサンプル作る
• ボタンを押すことで背景色の異なる (赤・青 ) 二つのユーザーコントロールを切り替える。
• Model は省略• 切り替えロジック呼び出しのサンプルは逆方向バインディングコマンドで実装
わんくま同盟 東京勉強会 #90
共通部分 (赤 )
<UserControl x:Class="NavigationCommon.UCRed“ ( 略 ) d:DesignHeight="150" d:DesignWidth="150" Background="Red"> <Grid> <TextBlock HorizontalAlignment="Center“ VerticalAlignment="Center" Text="{Binding Text}" /> </Grid></UserControl>
public partial class UCRed : UserControl{ public UCRed() { InitializeComponent(); }}
public class RedViewModel:ViewModelBase{ private string text = "赤 "; public string Text { set{ if (text != value) { text = value; OnPropertyChanged("Text"); } } get{ return text; } }}
View ViewModel
わんくま同盟 東京勉強会 #90
共通部分 (青 )
<UserControl x:Class="NavigationCommon.UCBlue“ ( 略 ) d:DesignHeight="150" d:DesignWidth="150" Background=“Blue"> <Grid> <TextBlock HorizontalAlignment="Center“ VerticalAlignment="Center" Text="{Binding Text}" /> </Grid></UserControl>
public partial class UCBlue : UserControl{ public UCBlue() { InitializeComponent(); }}
public class BlueViewModel:ViewModelBase{ private string text = “青 "; public string Text { set{ if (text != value) { text = value; OnPropertyChanged("Text"); } } get{ return text; } }}
View ViewModel
わんくま同盟 東京勉強会 #90
データテンプレート
わんくま同盟 東京勉強会 #90
View
<Window x:Class="MainWindow" ( 略 ) Title="MainWindow" > <Window.Resources> <DataTemplate DataType="{x:Type navi:RedViewModel}"> <navi:UCRed x:Name="RedUC" /> </DataTemplate> <DataTemplate DataType=“{x:Type navi:BlueViewModel}”> <navi:UCBlue x:Name="BlueUC" /> </DataTemplate> </Window.Resources><Grid > <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions>
<Grid Grid.Column="0" Name="ParentGrid"> <ContentControl Name="ContentControl“ Content="{Binding ChildViewModel}" /> </Grid> <Grid Grid.Column="1" ><Button Content="Button“ Command="{Binding NavigationCommand}" HorizontalAlignment="Center" VerticalAlignment="Center" Width="75" Height="25" /> </Grid></Grid></Window>
ViewModel の型を直接扱っている。
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = new MainViewModel(); } }
ViewModel を表示しようとしている
わんくま同盟 東京勉強会 #90
ViewModelpublic class MainViewModel:ViewModelBase{ ViewModelBase childVM; public ViewModelBase ChildViewModel { get { return childVM; } set { 略 } }
ICommand nCommand; public ICommand NavigationCommand { get { return nCommand; } set { 略 } }
public MainViewModel(){ NavigateRed(); }
private void NavigateRed() { ChildViewModel = new RedViewModel(); NavigationCommand = new Microsoft.TeamFoundation. MVVM.RelayCommand( (p) =>{NavigateBlue();}); }
private void NavigateBlue() { ChildViewModel = new BlueViewModel(); NavigationCommand = new Microsoft.TeamFoundation. MVVM.RelayCommand( (p) =>{ NavigateRed();} ); }}
もう一度ボタンを押したら元の色に戻るようにコマンドをリセット
データテンプレートで表示する子ViewModel
表示する ViewModel を変更
わんくま同盟 東京勉強会 #90
逆方向バインディングコマンド
わんくま同盟 東京勉強会 #90
Command UserControl
public class DependencyCommandControl : Control { public DependencyCommandControl():base(){ }
static DependencyCommandControl() { DefaultStyleKeyProperty.OverrideMetadata( typeof(DependencyCommandControl), new FrameworkPropertyMetadata( typeof(DependencyCommandControl)) ); }
public static readonly DependencyProperty CommandProperty = DependencyProperty.Register( "Command“, typeof(ICommand), typeof(DependencyCommandControl) );
public ICommand Command { set { SetValue(CommandProperty, value); } get { return (ICommand)GetValue(CommandProperty); } }}
XAML上で扱うには依存関係プロパティである必要があるので依存関係プロパティ化した ICommand を持つカスタムコントロールを作る
わんくま同盟 東京勉強会 #90
View
<Window x:Class="MainWindow"( 略 )Title="MainWindow"><Grid > <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/><ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid Grid.Column="0" Name="ParentGrid“ >
</Grid> <Grid Grid.Column="1" > <Button Content="Button" Command="{Binding NavigationCommand}" HorizontalAlignment="Center" VerticalAlignment="Center" Width="75" Height="25"/> <DependencyCommandControl x:Name="CommandHolder“ Command="{Binding Mode=OneWayToSource,Path='SouceToTargetCommand' }" /> </Grid></Grid></Window>
ボタンクリック時に呼ばれる順方向コマンド
ViewModel から呼ばれる逆方向コマンド
逆方向バインディング
カスタムコントロール
わんくま同盟 東京勉強会 #90
ViewModel
public class MainViewModel:ViewModelBase{ ViewModelBase childVM; public ViewModelBase ChildViewModel{get;set}
ICommand navigationcommand; public ICommand NavigationCommand { get { return navigationcommand; }set { 略 } }
ICommand sttCommand; public ICommand SouceToTargetCommand { get { return sttCommand; } set{ 略 } }
public MainViewModel() { NavigateToRed(); }
ボタンクリックから呼び出される( 順方向 ) コマンド
ViewModel から View を呼び出すための逆方向バインディングをするコマンド
わんくま同盟 東京勉強会 #90
ViewModel
private void NavigateToRed(){ ChildViewModel = new RedViewModel();
if (SouceToTargetCommand != null) SouceToTargetCommand.Execute(ChildViewModel);
NavigationCommand = new Microsoft.TeamFoundation.MVVM.RelayCommand( (p) => { NavigateToBlue(); });}
private void NavigateToBlue(){ ChildViewModel = new BlueViewModel();
if (SouceToTargetCommand != null) SouceToTargetCommand.Execute(ChildViewModel);
NavigationCommand = new Microsoft.TeamFoundation.MVVM.RelayCommand( (p) => { NavigateToRed(); } ); }}
子コントロール用の新しい ViewModel を作成、コマンドで View に送る
もう一度ボタンを押したら元に戻すために順方向コマンドをリセット
わんくま同盟 東京勉強会 #90
View
public partial class MainWindow : Window{ public MainWindow() { InitializeComponent(); var mainVM = new MainViewModel(); ChangeToRed(mainVM.ChildViewModel); this.DataContext = mainVM; }
private void ChangeToBlue(object vm) { ParentGrid.Children.Clear(); var newPanel = new UCBlue(); newPanel.DataContext = vm; ParentGrid.Children.Add(newPanel); CommandHolder.Command = new Microsoft.TeamFoundation.MVVM.RelayCommand(ChangeToRed); }
private void ChangeToRed(object vm) { ParentGrid.Children.Clear(); var newPanel = new UCRed(); newPanel.DataContext = vm; ParentGrid.Children.Add(newPanel); CommandHolder.Command = new Microsoft.TeamFoundation.MVVM.RelayCommand(ChangeToBlue); }}
ViewModel から受け取った新 ViewModel をView で直接扱っている
新しい View を作成
新しい ViewModel を受け取る
わんくま同盟 東京勉強会 #90
MVVM まとめ
• View と ViewModel が互いを扱わなくていいはずのMVVM で画面遷移を行おうとすると View で ViewModel を扱ってまう。→ そもそも MVVM には XAML環境に適応するための 最低限必要な機能しかなく、 画面遷移の存在を前提にしていないのでは? • MVVM の利点 ( XAML≒ 環境への適応≒データバインディ
ング ) を生かしつつ画面遷移の存在を前提とするパターンが必要
→ MVPVM
わんくま同盟 東京勉強会 #90
MVPVM(Model-View-Presenter-ViewModel) とは
• MVP と MVVM を合体させたもの• View,ViewModel の上に Presenter を置き、そこで、画
面遷移、ナビゲーションを管理する。
• ViewModel に書かれていたロジックを Presenter に移動することで ViewModel にはバインド対象となるプロパティのみが含まれるようにする。
• ViewModel にロジックが含まれなくなるので使いまわしが容易になる。
わんくま同盟 東京勉強会 #90
View
• ユーザーインターフェース• UI への出力と UI からの入力を担当する。• FrameworkElement の派生クラス• XAML の記述 + 対応する partial class
Model
• ビジネスロジック / プログラムの中核となる処理• View と ViewModel( と Presenter) 以外の部分
わんくま同盟 東京勉強会 #90
• View の必要な情報を保持公開• View から受け取った入力やコマンドをを Presenter
に渡す (MVPVM)• View からの入力やコマンドを処理し Model を呼び出
す (MVVM)
ViewModel
わんくま同盟 東京勉強会 #90
• View,ViewModel を保持、その接続を管理する• ViewModel から受け取った入力やコマンドを処理し
Model を呼び出す。• ViewModel のプロパティを通して View を変更する。• ナビゲーション・画面遷移の管理
(子 View,子 ViewModel の作成、接続 )
Presenter
わんくま同盟 東京勉強会 #90
とりあえず組んでみた
• MVVM の時と同じようにテキストボックスから入力、加工してラベルに出力するアプリ
※ 画面遷移がないのに MVPVM で組む必要性がないとか言わないように
わんくま同盟 東京勉強会 #90
View
<Window x:Class="MVPVM1.MainWindow"( 略 ) Title="MainWindow" Height="300" Width="300"> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Button Content="Button" Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Width="120" Command="{Binding MainCommand}"/> <TextBox Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" Height="23" Width="120" Text="{Binding Input}" /> <Border Grid.Row="2" BorderThickness="1" BorderBrush="Black" HorizontalAlignment="Center" VerticalAlignment="Center" > <Label HorizontalAlignment="Center" VerticalAlignment="Center" Content="{Binding Output}" Width="120" /> </Border> </Grid></Window>
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } }
わんくま同盟 東京勉強会 #90
ViewModel
class MainViewModel:ViewModelBase { public MainViewModel() { }
public override void Cleanup() { MainCommand = null; base.Cleanup(); }
private ICommand _mainCommand; public ICommand MainCommand { set{ 略 } get{ return _mainCommand;} } private string _input; public string Input { set{ 略 } get{ return _input;} } private string _output; public string Output { set { 略 } get{ return _output;} } }
Command の中身の定義Model の関数の呼び出しコードがなくなっている
わんくま同盟 東京勉強会 #90
PresenterBase class PresenterBase<TView,TViewModel> where TView:System.Windows.FrameworkElement where TViewModel:ViewModelBase { public PresenterBase(TView view, TViewModel viewmodel) { View = view; ViewModel = viewmodel; }
public TView View { set; get; } public TViewModel ViewModel { set; get; }
public virtual void Initialize() { View.DataContext = ViewModel; }
DataContext の設定View-ViewModel の接続
public virtual void CleanUp() { if (ViewModel != null) { ViewModel.Cleanup(); ViewModel = null; }
if (View != null) { View.DataContext = null; View = null; } } }
View-ViewModel 間の接続解除View,ViewModel の参照解除
引数のView,ViewModelメンバに代入
わんくま同盟 東京勉強会 #90
Presenter
class MainPresenter:PresenterBase<MainWindow,MainViewModel> { MainModel model = new MainModel();
public MainPresenter(MainWindow view,MainViewModel viewmodel) :base(view,viewmodel){ }
public override void Initialize() { ViewModel.MainCommand = new Microsoft.TeamFoundation.MVVM.RelayCommand( ()=>{ViewModel.Output = model.Logic(ViewModel.Input); });
base.Initialize(); }
public override void CleanUp() { base.CleanUp(); } }
ViewModel のコマンドの中身を作成
コンストラクタが View とViewModel のインスタンスを受け取る
Model の関数の呼び出し。View の操作はデータバインドしたViewModel のプロパティを通して行う。
わんくま同盟 東京勉強会 #90
App.xaml
<Application x:Class="MVPVM1.App"( 略 )Startup="Application_Startup“Exit="Application_Exit“
> <Application.Resources> </Application.Resources></Application>
public partial class App : Application { MainPresenter presenter ; private void Application_Startup(object sender, StartupEventArgs e) { var view =new MainWindow(); var viewmodel = new MainViewModel(); presenter = new MainPresenter(view, viewmodel); presenter.Initialize();
view.Show(); }
private void Application_Exit(object sender, ExitEventArgs e) { presenter.CleanUp(); } }
App.xaml.cs
StartupUri=“MainWindow.xaml” を削除Startup,Exit を追加
アプリ起動時に、 V,VM,P を作成、ウィンドウを表示
アプリ終了時に Presenter から全体をクリア
わんくま同盟 東京勉強会 #90
MVPVM での画面遷移
• View,ViewModel両方を扱うことが出来るPresenter に記述する
• MVVM では View,ViewModel の二か所に分割されていた画面遷移ロジックを Presenter 一つにまとめられるため自然な形で実装することができる。
わんくま同盟 東京勉強会 #90
View<Window x:Class="MVPVMNavigation.MainWindow" ( 略 ) Title="MainWindow" Height="350" Width="525"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Grid Grid.Column="0" Name="ParentGrid" > </Grid> <Grid Grid.Column="1" > <Button Content=“Button” HorizontalAlignment=“Center” VerticalAlignment=“Center” Command="{Binding NavigationCommand}" Width="75" /> </Grid> </Grid></Window>
public partial class MainWindow: Window { public MainWindow() { InitializeComponent(); } }
View に画面遷移用のロジックが存在しない
ボタンがクリックされた時に呼び出されるコマンドのバインド
わんくま同盟 東京勉強会 #90
ViewModel
public class MainViewModel:ViewModelBase{ ViewModelBase childVM; public ViewModelBase ChildViewModel { get { return childVM; } set { 略 } } ICommand navigationCommand; public ICommand NavigationCommand { get { return navigationCommand; } set { 略 } }
public MainViewModel(){}
public override void Cleanup() { if (ChildViewModel != null) { NavigationCommand = null; ChildViewModel.Cleanup(); ChildViewModel = null; } base.Cleanup(); }}
子 ViewModel子 Presenter からアクセスできるのでもしかしたら要らないかも
ボタンクリックで呼び出される画面遷移用コマンド
各プロパティのクリア
わんくま同盟 東京勉強会 #90
子 Presenter(赤 ,青 )
public class RedPresenter : PresenterBase<UCRed, RedViewModel>{ public RedPresenter(UCRed view, RedViewModel viewmodel) : base(view, viewmodel) { }
public override void Initialize() { base.Initialize(); }
public override void CleanUp() { base.CleanUp(); } }
public class BluePresenter :PresenterBase<UCBlue,BlueViewModel>{ public BluePresenter(UCBlue view, BlueViewModel viewmodel) : base(view, viewmodel) { }
public override void Initialize() { base.Initialize(); }
public override void CleanUp() { base.CleanUp(); } }
わんくま同盟 東京勉強会 #90
MainPresenter(1)public class MainPresenter: PresenterBase<MainWindow,MainViewModel>{ public MainPresenter(MainWindow view,MainViewModel viewmodel):base(view,viewmodel){ }
public IPresenter ChildPresenter { get; set; }
public override void Initialize() { base.Initialize(); NavigateToRed(); }
public override void CleanUp() { ChildPresenter.CleanUp(); base.CleanUp(); }
子コントロール (赤・青 )用の Presenter
子 Presenter の解放
わんくま同盟 東京勉強会 #90
MainPresenter(2)
private void NavigateToRed(){ if (ChildPresenter != null) { ChildPresenter.CleanUp(); }
var v = new UCRed(); var vm = new RedViewModel(); var presenter = new RedPresenter(v, vm); View.ParentGrid.Children.Clear(); View.ParentGrid.Children.Add(v);
ViewModel.ChildViewModel = vm; ViewModel.NavigationCommand = new Microsoft.TeamFoundation.MVVM.RelayCommand( (p) => {NavigateToBlue();} );
ChildPresenter = presenter; ChildPresenter.Initialize(););
新しいView,ViewModel,Presenter の作成
次回ボタンクリック時に元の色に戻すためにコマンドをリセット
新しい View を Grid に追加
旧 Presenter のクリア
わんくま同盟 東京勉強会 #90
MainPresenter(3)
private void NavigateToBlue(){ if (ChildPresenter != null) { ChildPresenter.CleanUp(); }
var v = new UCBlue(); var vm = new BlueViewModel(); var presenter = new RedPresenter(v, vm); View.ParentGrid.Children.Clear(); View.ParentGrid.Children.Add(v);
ViewModel.ChildViewModel = vm; ViewModel.NavigationCommand = new Microsoft.TeamFoundation.MVVM.RelayCommand( (p) => {NavigateToRed();} );
ChildPresenter = presenter; ChildPresenter.Initialize(););
新しい View,ViewModelPresenter の作成
次回ボタンクリック時に元の色に戻すためにコマンドをリセット
新しい View を Grid に追加
旧 Presenter のクリア
わんくま同盟 東京勉強会 #90
MVPVM まとめ
• ViewModel は Binding 対象になるプロパティのみを記述しコマンドの中身は Presenter に記述する
• Presenter は View,ViewModel どちらも扱えるのでその二つに分けて記述していた画面遷移ロジックをPresenter 1ヵ所に記述することができる。
→MVVM では三ケ所すべてにロジックが含まれる可能性があったのが MVPVM では基本的に P,M の二か所に減る
• 何らかの事情 (ActiveX,低スキル ,etc…) により View がビジネスロジックを持っている場合でも Presenter から直接呼べる
わんくま同盟 東京勉強会 #90
まとめ
• View との入出力はバインディングで行い、インターフェースになる関数だけ残して実装は別クラスに移動することを繰り返せばとりあえず MVVM の形にはなる
• Presenter で View,ViewModel を管理し ViewModel のコマンドの中身をPresenter で作れば MVPVM になる。
• 一画面で完結するアプリなら MVVM でもいいが、ナビゲーション ( 画面遷移 ) を含む場合は MVPVM の方がオススメ
わんくま同盟 東京勉強会 #90
今回扱わなかったこと
• XAML環境でデータバインディングが必須な理由• コレクションコントロール• エラー処理• Command の CanExecute()• 非同期処理 (Model)• View に合わせた、 ViewModel,Presenterそれぞれツリー
の構造