最近知ったのですが、尾上 雅則さんが中心になって構築を進めている Livet という WPF4 のための MVVM パターン用のインフラストラクチャがあります。Expression Blend SDK(Expression Blendを持っていなくてもダウンロード・使用が可能。再配布可能なアセンブリを含んでいる)の使用が前提になりますが、ビヘイビアとアクションが豊富に提供されていたり、メッセンジャーが提供されているなど、ウィンドウとビューモデルの分離を行うのにかなり便利な機能を持っています。
前回の記事でモードレスなウィンドウの作成と作成したモードレスなウィンドウをすべて閉じる機能を持つプログラムを検証のために作りましたが、この機能を Livet を用いて作ってみました(提供されている利用例がモーダルなウィンドウのみだったので 🙂 )。なお、利用した Livet のバージョンは 0.99 です。
(2014年1月23日追記)
Livet は利用していませんが、ウィンドウの遷移や複数ウィンドウの表示を行うことができる TransitionViewModelBase の記事を投稿しました。
新しいプロジェクトの作成で、テンプレートから「Livet WPF4 MVVM アプリケーション」を選択してからプロジェクト名を入力して新しいプロジェクトを作成します。プロジェクトが生成されたら、一度ビルドしておきましょう。そうしないとウィンドウを表示したときにアセンブリが見つからないというエラーでデザイナーにウィンドウを読み込むことができません。
次に、同じ機能を実現するということで、デバッグウィンドウに情報を表示する NotifyDebugInfo クラスを作ります。プロジェクトに AppUtil フォルダを作り、そこにクラスを追加します。
using System;
using System.Diagnostics;
namespace LivetViewModelNotifyTest001.AppUtil
{
static class NotifyDebugInfo
{
public static void WriteLine(string message)
{
var dispMessage = string.Format("{0:HH:MM:ss} {1}", DateTime.Now, message);
Debug.WriteLine(dispMessage);
}
}
}
次にモデルですが、今回はウィンドウ表示だけなので、モデルに中身はありません コンストラクタとデストラクタだけです。
using Livet;
using LivetViewModelNotifyTest001.AppUtil;
namespace LivetViewModelNotifyTest001.Models
{
public class Model : NotificationObject
{
/*
* NotificationObjectはプロパティ変更通知の仕組みを実装したオブジェクトです。
*/
/*
* ModelからViewModelへのイベントを発行する場合はNotificatorを使用してください。
*
* Notificatorはイベント代替手段です。コードスニペット lnev でCLRイベントと同時に定義できます。
*
* ViewModelへNotificatorを使用した通知を行う場合はViewModel側でViewModelHelperを使用して受信側の登録をしてください。
*/
public Model()
{
NotifyDebugInfo.WriteLine("Create Model");
}
~Model()
{
NotifyDebugInfo.WriteLine("Destructor Model");
}
}
}
次にビューモデルですが、次のような構成をとることにしました。
- ビューとビューモデルは一対一で対応させる
- ウィンドウのクローズはビューモデルからウィンドウへ Close メッセージを送ることで行う
- ViewModelManager を作成し、「すべて閉じる」を行うために開いているウィンドウに対応するビューモデルを管理する
- 「すべて閉じる」機能は ViewModelManager が提供し、各ビューモデルの RaiseCloseMessage メソッドを起動する
- 上記機能のインターフェイスとして、「すべて閉じる」の対象となるビューモデルは IRaiseCloseMessage を実装する
なお、ビューモデルの管理をモデル側で行うことも考えましたが、
- モデル側がビューモデルの情報を保持することになってしまうこと
- ビューモデルの管理は「すべて閉じる」機能のためにのみ必要な機械的な機構であること
から、ビューモデルの層で持つことにしました。
まず、IRaiseCloseMessage です。
namespace LivetViewModelNotifyTest001.ViewModels
{
/// <summary>
/// ウィンドウを閉じるためのビューモデルのインターフェイスです。
/// </summary>
public interface IRaiseCloseMessage
{
/// <summary>
/// ビューモデルからウィンドウへ Close メッセージを通知するメソッドです。
/// </summary>
void RaiseCloseMessage();
}
}
次に ViewModelManager です。ビューモデルの型ごとに生成された(開いているウィンドウに対応するビューモデル)インスタンスを管理するようにしています。インスタンスの登録・削除の際、呼び出し側はインスタンスへの参照だけ渡せば、マネージャ側がよろしくやってくれるように構成しています(パラメータの型はビューモデルの基底クラスである ViewMode になっています)。問い合わせなどを行う場合にはビューモデルの型(基底クラスではなく、生成したインスタンスの型)を渡します。
using System;
using System.Collections.Generic;
using System.Linq;
using Livet;
namespace LivetViewModelNotifyTest001.ViewModels
{
internal static class ViewModelManager
{
private static Dictionary<Type, List<ViewModel>>
_viewModels = new Dictionary<Type, List<ViewModel>>();
/// <summary>
/// ビューモデルのインスタンスを登録します。
/// 初めて登録されるビューモデルの場合には、自動的に登録するビューモデルのエントリを作成します。
/// </summary>
/// <param name="viewModel"></param>
public static void AddEntryViewModel(ViewModel viewModel)
{
var type = viewModel.GetType();
if (!_viewModels.ContainsKey(type))
{
var list = new List<ViewModel>();
_viewModels.Add(type, list);
}
_viewModels[type].Add(viewModel);
}
/// <summary>
/// 指定されたビューモデルのインスタンスの数を返します。
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static int Count(Type type)
{
if (_viewModels.ContainsKey(type))
{
var count = _viewModels[type].Count;
return count;
}
else
{
return 0;
}
}
/// <summary>
/// ビューモデルのインスタンスの登録を解除します。
/// ウィンドウの Closed イベントが発生した際に呼び出されるようにしてください。
/// </summary>
/// <param name="viewModel"></param>
public static void RemoveEntryViewModel(ViewModel viewModel)
{
var type = viewModel.GetType();
if (!_viewModels.ContainsKey(type)) return;
_viewModels[type].Remove(viewModel);
}
/// <summary>
/// 指定されたビューモデルのインスタンスの IRaiseCloseMessage インターフェイス の
/// RaiseCloseMessage メソッドを実行します。
/// </summary>
/// <returns></returns>
public static void CloseViewModels(Type type)
{
if (!_viewModels.ContainsKey(type) || _viewModels[type].Count == 0) return;
if (_viewModels[type].First() as IRaiseCloseMessage == null)
{
throw new InvalidCastException(
"オブジェクトは IRaiseCloseMessage インターフェイスを実装していません。: " + type.ToString());
}
var list = new List<ViewModel>();
_viewModels[type].ForEach(n => list.Add(n));
list.ForEach(n => (n as IRaiseCloseMessage).RaiseCloseMessage());
}
}
}
次にウィンドウに対応する DetailWindowViewModel クラスを追加します。「新しい項目の追加」ウィンドウのテンプレートの選択で、「Livet WPF4 ビュー・モデル」を選択してください。
using System.Collections.Generic;
using Livet;
using Livet.Commands;
using Livet.Messaging;
using Livet.Messaging.Windows;
using LivetViewModelNotifyTest001.Models;
using LivetViewModelNotifyTest001.AppUtil;
namespace LivetViewModelNotifyTest001.ViewModels
{
public class DetailWindowViewModel : ViewModel, IRaiseCloseMessage
{
/*コマンド、プロパティの定義にはそれぞれ
*
* lvcom : ViewModelCommand
* lvcomn : ViewModelCommand(CanExecute無)
* llcom : ListenerCommand(パラメータ有のコマンド)
* llcomn : ListenerCommand(パラメータ有のコマンド・CanExecute無)
* lprop : 変更通知プロパティ
*
* を使用してください。
*/
/*ViewModelからViewを操作したい場合は、
* Messengerプロパティからメッセージ(各種InteractionMessage)を発信してください。
*/
/*
* UIDispatcherを操作する場合は、DispatcherHelperのメソッドを操作してください。
* UIDispatcher自体はApp.xaml.csでインスタンスを確保してあります。
*/
/*
* Modelからの変更通知などの各種イベントをそのままViewModelで購読する事はメモリリークの
* 原因となりやすく推奨できません。ViewModelHelperの各静的メソッドの利用を検討してください。
*/
private Model _model;
private bool _canDoSomething = true;
public DetailWindowViewModel(Model model, MainWindowViewModel parent)
{
_model = model;
Parent = parent;
ViewModelManager.AddEntryViewModel(this);
NotifyDebugInfo.WriteLine("Create ViewModel: DetailWindowViewModel");
_TestMessage = "テスト";
}
public MainWindowViewModel Parent { get; private set; }
#region OnClosedWindowCommand
private ViewModelCommand _OnClosedWindowCommand;
// Window で Closed イベントが起きた時に実行されるコマンド
public ViewModelCommand OnClosedWindowCommand
{
get
{
if (_OnClosedWindowCommand == null)
{
_OnClosedWindowCommand = new ViewModelCommand(OnClosedWindow);
}
return _OnClosedWindowCommand;
}
}
public void OnClosedWindow()
{
// ViewModelManager への登録を解除します。
ViewModelManager.RemoveEntryViewModel(this);
if (ViewModelManager.Count(typeof(DetailWindowViewModel)) == 0)
{ // MainWindowViewModel の「すべて閉じる」ボタンの実行可否が変化したことを通知します
Parent.CloseDetailWindowsCommand.RaiseCanExecuteChanged();
}
}
#endregion
#region DoSomethingCommand
private ViewModelCommand _DoSomethingCommand;
public ViewModelCommand DoSomethingCommand
{
get
{
if (_DoSomethingCommand == null)
{
_DoSomethingCommand = new ViewModelCommand(DoSomething, CanDoSomething);
}
return _DoSomethingCommand;
}
}
public bool CanDoSomething()
{
return _canDoSomething;
}
public void DoSomething()
{
// インスタンスのメッセンジャーを通じて DetailWindow へメッセージを通知します。
Messenger.Raise(new InformationMessage("ボタンがクリックされました。", "確認",
System.Windows.MessageBoxImage.Information, "Info"));
_canDoSomething = false;
// 「ボタン」ボタンの実行可否が変化したことを通知します
DoSomethingCommand.RaiseCanExecuteChanged();
}
#endregion
#region TestMessage変更通知プロパティ
private string _TestMessage;
public string TestMessage
{
get
{ return _TestMessage; }
set
{
if (EqualityComparer<string>.Default.Equals(_TestMessage, value))
return;
_TestMessage = value;
RaisePropertyChanged("TestMessage");
}
}
#endregion
#region IRaiseCloseMessage インターフェイス
public void RaiseCloseMessage()
{
Messenger.Raise(new WindowActionMessage("Close", WindowAction.Close));
}
#endregion //IRaiseCloseMessage インターフェイス
~DetailWindowViewModel()
{
NotifyDebugInfo.WriteLine("Destructor ViewModel: DetailWindowViewModel");
}
}
}
次に MainWindowViewModel です。
using System;
using Livet;
using Livet.Commands;
using Livet.Messaging;
using LivetViewModelNotifyTest001.Models;
using LivetViewModelNotifyTest001.AppUtil;
namespace LivetViewModelNotifyTest001.ViewModels
{
public class MainWindowViewModel : ViewModel
{
/*コマンド、プロパティの定義にはそれぞれ
*
* lvcom : ViewModelCommand
* lvcomn : ViewModelCommand(CanExecute無)
* llcom : ListenerCommand(パラメータ有のコマンド)
* llcomn : ListenerCommand(パラメータ有のコマンド・CanExecute無)
* lprop : 変更通知プロパティ
*
* を使用してください。
*/
/*ViewModelからViewを操作したい場合は、
* Messengerプロパティからメッセージ(各種InteractionMessage)を発信してください。
*/
/*
* UIDispatcherを操作する場合は、DispatcherHelperのメソッドを操作してください。
* UIDispatcher自体はApp.xaml.csでインスタンスを確保してあります。
*/
/*
* Modelからの変更通知などの各種イベントをそのままViewModelで購読する事はメモリリークの
* 原因となりやすく推奨できません。ViewModelHelperの各静的メソッドの利用を検討してください。
*/
private Model _model;
public MainWindowViewModel()
{
NotifyDebugInfo.WriteLine("Create ViewModel: MainWindowViewModel");
_model = new Model();
}
#region DoGcCommand
private ViewModelCommand _DoGcCommand;
public ViewModelCommand DoGcCommand
{
get
{
if (_DoGcCommand == null)
{
_DoGcCommand = new ViewModelCommand(DoGc);
}
return _DoGcCommand;
}
}
public void DoGc()
{
NotifyDebugInfo.WriteLine("=== GC 実行 ===");
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
#endregion
#region CreateDetailWindowCommand
private ViewModelCommand _CreateDetailWindowCommand;
public ViewModelCommand CreateDetailWindowCommand
{
get
{
if (_CreateDetailWindowCommand == null)
{
_CreateDetailWindowCommand = new ViewModelCommand(CreateDetailWindow);
}
return _CreateDetailWindowCommand;
}
}
public void CreateDetailWindow()
{
// インスタンスのメッセンジャーを通じて MainWindow へメッセージを通知します。
Messenger.Raise(new TransitionMessage(new DetailWindowViewModel(_model, this), "Transition"));
// 「すべて閉じる」ボタンの実行可否が変化したことを通知します
CloseDetailWindowsCommand.RaiseCanExecuteChanged();
}
#endregion
#region CloseDetailWindowsCommand
private ViewModelCommand _CloseDetailWindowsCommand;
public ViewModelCommand CloseDetailWindowsCommand
{
get
{
if (_CloseDetailWindowsCommand == null)
{
_CloseDetailWindowsCommand = new ViewModelCommand(CloseDetailWindows, CanCloseDetailWindows);
}
return _CloseDetailWindowsCommand;
}
}
public bool CanCloseDetailWindows()
{
return (ViewModelManager.Count(typeof(DetailWindowViewModel)) != 0);
}
public void CloseDetailWindows()
{
ViewModelManager.CloseViewModels(typeof(DetailWindowViewModel));
}
#endregion
~MainWindowViewModel()
{
NotifyDebugInfo.WriteLine("Destructor ViewModel: MainWindowViewModel");
}
}
}
次にウィンドウですが、まず、Closed イベントの際にビューモデルのコマンドを起動できようにするために、プロジェクトに Behavior フォルダを追加して CommandExecuteAction を作ります。
using System.Windows;
using System.Windows.Input;
using System.Windows.Interactivity;
namespace LivetViewModelNotifyTest001.Behavior
{
public class CommandExecuteAction : TriggerAction<DependencyObject>
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register(
"Command",
typeof(ICommand),
typeof(CommandExecuteAction),
new PropertyMetadata());
public ICommand Command
{
get { return (ICommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
protected override void Invoke(object parameter)
{
if (Command == null) return;
Command.Execute(parameter);
}
}
}
次に DetailWindow を追加します。
「新しい項目の追加」ウィンドウのテンプレートの選択で、「Livet WPF4 ウィンドウ」を選択してください。
<Window x:Class="LivetViewModelNotifyTest001.Views.DetailWindow"
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:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
xmlns:v="clr-namespace:LivetViewModelNotifyTest001.Views"
xmlns:vm="clr-namespace:LivetViewModelNotifyTest001.ViewModels"
xmlns:b="clr-namespace:LivetViewModelNotifyTest001.Behavior"
Title="DetailWindow" Height="200" Width="300">
<i:Interaction.Triggers>
<l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Info">
<l:InformationDialogInteractionMessageAction />
</l:InteractionMessageTrigger>
<l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="Close">
<l:WindowInteractionMessageAction />
</l:InteractionMessageTrigger>
<i:EventTrigger EventName="Closed">
<b:CommandExecuteAction Command="{Binding OnClosedWindowCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<StackPanel>
<TextBlock
Text="詳細ウィンドウ" FontSize="14" Margin="0 0 0 20" HorizontalAlignment="Center" />
<Button Content="ボタン" Command="{Binding Path=DoSomethingCommand}" Margin="5" Padding="30 5" />
<TextBlock Text="{Binding TestMessage}" Margin="5" />
</StackPanel>
</Window>
本来コードビハインドには何も記述しないのですが、インスタンス生成と破棄のメッセージをデバックウィンドウに出すコードを追加します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using LivetViewModelNotifyTest001.AppUtil;
namespace LivetViewModelNotifyTest001.Views
{
/// <summary>
/// DetailWindow.xaml の相互作用ロジック
/// </summary>
public partial class DetailWindow : Window
{
public DetailWindow()
{
InitializeComponent();
NotifyDebugInfo.WriteLine("Create Window: DetailWindow");
}
~DetailWindow()
{
NotifyDebugInfo.WriteLine("Destructor Window: DetailWindow");
}
}
}
最後に MainWindow です。
<Window x:Class="LivetViewModelNotifyTest001.Views.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:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
xmlns:v="clr-namespace:LivetViewModelNotifyTest001.Views"
xmlns:vm="clr-namespace:LivetViewModelNotifyTest001.ViewModels"
Title="MainWindow" Height="200" Width="400">
<Window.DataContext>
<vm:MainWindowViewModel/>
</Window.DataContext>
<i:Interaction.Triggers>
<l:InteractionMessageTrigger MessageKey="Transition" Messenger="{Binding Messenger}">
<l:TransitionInteractionMessageAction WindowType="{x:Type v:DetailWindow}" Mode="Normal" />
</l:InteractionMessageTrigger>
</i:Interaction.Triggers>
<StackPanel>
<TextBlock
Text="Livet でのウィンドウ操作のテスト" FontSize="14"
Margin="0 0 0 20" HorizontalAlignment="Center" />
<Button
Content="GC 実行" Command="{Binding DoGcCommand}" Margin="5" Padding="30 5" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="ウィンドウの操作" FontSize="12" VerticalAlignment="Center" Margin="5" />
<Button Content="開く" Command="{Binding CreateDetailWindowCommand}" Margin="5" Padding="30 5" />
<Button Content="すべて閉じる" Command="{Binding CloseDetailWindowsCommand}" Margin="5" Padding="30 5" />
</StackPanel>
</StackPanel>
</Window>
こちらも、本来コードビハインドには何も記述しないのですが、インスタンス生成と破棄のメッセージをデバックウィンドウに出すコードを追加します。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using LivetViewModelNotifyTest001.AppUtil;
namespace LivetViewModelNotifyTest001.Views
{
/// <summary>
/// MainWindow.xaml の相互作用ロジック
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
NotifyDebugInfo.WriteLine("Create Window: MainWindow");
}
~MainWindow()
{
NotifyDebugInfo.WriteLine("Destructor Window: MainWindow");
}
}
}
長くなりましたが、以上で完了です。動かしたり、弄ったりして遊んでみてください 😉