Livet を使ってみた (by VB)

国内有数の MVVMer である尾上さんが開発中の Livet を触ってみました。



現在はまだドキュメントが整備されておらずどこから手を付けていいか判らない状態なので、まずはサンプルプロジェクトを触ってみようということに。ちなみに Livet って何という人のために、以下公式から引用しときます。

Livet (リベット) は WPF4 のための MVVM (Model/View/ViewModel) パターン用インフラストラクチャです。.NET Framework 4 Client Profile 以上で動作し、zlib/libpng ライセンスで提供しています。zlib/libpng ライセンスでは、ライブラリとしての利用に留めるのであれば再配布時にも著作権表示などの義務はありません。しかし、ソースコードを改変しての再配布にはその旨の明示が義務付けられます。


Livet WPF4 MVVM インフラストラクチャ


サンプルは C# だけなので、またいつもの悪い癖がふつふつと沸いてきました。そう VB 化ですw  VB に移植しながら MVVM パターンとそのインフラである Livet について学ぶ。一石二鳥ということで、早速チャレンジしてみました。


まず Model から

Livet の有難いところは、0.97RC から VB にも対応したので VB ユーザーでも利用できることです。VB ユーザーはこと MVVM インフラにおいておきざりにされてる感じが強かっただけに VB 対応は嬉しいですね。「新しいプロジェクト」→ 「Visual Vasic」と選択すると、テンプレート一覧の先頭に「Livet WPF4 MVVM アプリケーション」と表示されています。 VB 版のサンプルを作るにあたり、プロジェクト名は「LivetVBSampleProject」としました。



まず Model から実装することにします。C# のコードを見ながらせこせこ移植します。Livet 独自のクラス、NotificationObject から派生してるのが判ると思います。解説によると「変更通知機能を持つオブジェクトです。主にModelの基底クラスとして利用されます」とのことです。
ちなみに Option Explicit ステートメントはテンプレートでは付きませんが、個人的には付けるようにしてます。また Main というクラス名が何とも引っかかりますが、ここは気にせず実装することにいたしましょうw

' Main.vb
Option Explicit On
Option Strict On

Imports System.Linq
Imports System.Collections.ObjectModel

Public Class Main
    Inherits NotificationObject

    Public Sub New()
        Members = New ObservableCollection(Of Member)()
        Members.Add(New Member(Me) With {.Name = "neuecc", .Birthday = New DateTime(1983, 12, 30), .Memo = "LINQ星人です。"})
        Members.Add(New Member(Me) With {.Name = "ugaya40", .Birthday = New DateTime(1983, 10, 23), .Memo = "僕です。"})
    End Sub

    Public _Members As ObservableCollection(Of Member)
    Public Property Members As ObservableCollection(Of Member)
        Get
            Return _Members
        End Get
        Private Set(value As ObservableCollection(Of Member))
            _Members = value
        End Set
    End Property

End Class


次は Member クラス・・・これも同じく NotificationObject から継承してます。変更通知用プロパティが三つ程でてきますので注目。

' Member.vb
Option Explicit On
Option Strict On

Public Class Member
    Inherits NotificationObject

    Public Sub New(parent As Main)
        Main = parent
    End Sub

    Private _Main As Main
    Public Property Main() As Main
        Get
            Return _Main
        End Get
        Private Set(ByVal value As Main)
            _Main = value
        End Set
    End Property

    Private _Name As String
    Public Property Name() As String
        Get
            Return _Name
        End Get
        Set(ByVal value As String)
            If (_Name = value) Then Return
            _Name = value
            RaisePropertyChanged("Name")
        End Set
    End Property

    Private _Birthday As DateTime
    Public Property Birthday() As DateTime
        Get
            Return _Birthday
        End Get
        Set(ByVal value As DateTime)
            If (_Birthday = value) Then Return
            _Birthday = value
            RaisePropertyChanged("Birthday")
        End Set
    End Property

    Private _Memo As String
    Public Property Memo() As String
        Get
            Return _Memo
        End Get
        Set(ByVal value As String)
            If (_Memo = value) Then Return
            _Memo = value
            RaisePropertyChanged("Memo")
        End Set
    End Property

    Public Function IsIncludedInMainCollection() As Boolean
        Return Main.Members.Contains(Me)
    End Function

    Public Sub AddThisToMainCollection()
        Main.Members.Add(Me)
    End Sub

    Public Sub RemoveThisFromMainCollection()
        Main.Members.Remove(Me)
    End Sub

End Class


モデル Member は NotificationObject の派生クラスです。「Name」「Birthday」「Memo」の各プロパティはバインド用プロパティのため、セッターで RaisePropertyChanged メソッドがコールされています。
Livet ならこのバインド用プロパティも簡単に実装できます。コードスニペット「lprop」を書いて TAB キーを二回押すとバインド用プロパティのスニペットが挿入されます。


プロパティ名を変更すると、フィールドと RaisePropertyChanged に渡すメソッド名も同時に変わるのでかなり便利。


現在のバージョン 0.97RC では既定の型が Object になってますが、VB の場合、プロパティのコードスニペット同様 String 型の方がいいように思えますね。

ViewModel です。

お次は ViewModel です。C# のサンプル見ながらしこしこ移植します。

' MainWindowViewModel.vb
Option Explicit On
Option Strict On

Imports System.Collections.ObjectModel
Imports System.ComponentModel
Imports Livet

Public Class MainWindowViewModel
    Inherits ViewModel

    'コマンド、プロパティの定義にはそれぞれ 
    ' 
    '  ldcom   : DelegateCommand(パラメータ無)
    '  ldcomn  : DelegateCommand(パラメータ無・CanExecute無)
    '  ldcomp  : DelegateCommand(型パラメータ有)
    '  ldcompn : DelegateCommand(型パラメータ有・CanExecute無)
    '  lprop   : 変更通知プロパティ
    '  
    'を使用してください。

    'ViewModelからViewを操作したい場合は、
    'Messengerプロパティからメッセージ(各種InteractionMessage)を発信してください。

    'UIDispatcherを操作する場合は、DispatcherHelperのメソッドを操作してください。
    'UIDispatcher自体はApplication.xaml.vbでインスタンスを確保してあります。

    Private _model As Main

    Public Sub New()

        _model = New Main()

        ' 変更通知をDispatcher経由で行うコレクションを、Modelのコレクションから生成しています。
        ' 作成されたコレクションは、ソースのコレクションと同期されます。
        ' 弱イベントを使用してリークの心配のない方法で同期されています。
        Members = ViewModelHelper.CreateReadOnlyNotificationDispatcherCollection(
            _model.Members,
            Function(m) New MemberViewModel(m, Me), DispatcherHelper.UIDispatcher)
    End Sub

    Private newPropertyValue As ReadOnlyNotificationDispatcherCollection(Of MemberViewModel)
    Public Property Members() As ReadOnlyNotificationDispatcherCollection(Of MemberViewModel)
        Get
            Return newPropertyValue
        End Get
        Private Set(ByVal value As ReadOnlyNotificationDispatcherCollection(Of MemberViewModel))
            newPropertyValue = value
        End Set
    End Property

#Region "EditNewCommand"

    Private _EditNewCommand As DelegateCommand

    Public ReadOnly Property EditNewCommand() As DelegateCommand
        Get
            If _EditNewCommand Is Nothing Then
                _EditNewCommand = New DelegateCommand(AddressOf EditNew)
            End If
            Return _EditNewCommand
        End Get
    End Property

    Private Sub EditNew()
        ' Viewに画面遷移用メッセージを送信しています。
        ' Viewは対応するメッセージキーを持つInteractionTransitionMessageTriggerでこのメッセージを受信します。
        Messenger.Raise(New TransitionMessage(New MemberViewModel(New Member(_model), Me), "Transition"))
    End Sub

#End Region

#Region "RemoveCommand"

    Private _RemoveCommand As DelegateCommand

    Public ReadOnly Property RemoveCommand() As DelegateCommand
        Get
            If _RemoveCommand Is Nothing Then
                _RemoveCommand = New DelegateCommand(AddressOf Remove, AddressOf CanRemove)
            End If
            Return _RemoveCommand
        End Get
    End Property

    Private Function CanRemove() As Boolean
        Return Members.Any(Function(m) m.Checked)
    End Function

    Private Sub Remove()
        Members.Where(Function(m) m.Checked).ToList().ForEach(Sub(m) m.RemoveCommand.Execute())
    End Sub

#End Region

End Class


お次は MemberViewModel クラスです。IDataErrorInfo.Item プロパティのインターフェイスC# とは異なり ReadOnly になってるので、RaisePropertyChanged("Error") の呼び出し場所を プロパティの方に移動しています。この辺りは注意が必要ですね。また Item が ReadOnly のため、Me("InputBirthday") = "名前は必須です" と書けません。_errors("InputBirthday") = "名前は必須です" という具合に _errors フィールドを直接呼ぶ必要があります。

' MemberViewModel.vb
Option Explicit On
Option Strict On

Imports System.Collections.Generic
Imports System.ComponentModel

Public Class MemberViewModel
    Inherits ViewModel
    Implements IDataErrorInfo

    Private _model As Member
    Private _errors As Dictionary(Of String, String) = New Dictionary(Of String, String)()

    Public Sub New(m As Member, parent As MainWindowViewModel)
        _model = m
        MainWindowViewModel = parent

        ' 入力値やエラー情報の初期化
        InitializeInput()

        ' ModelのPropertyChangedイベントはBindNotifyChangedを使用してハンドルします。
        ' こうする事で弱イベントによるModelイベントの購読が行われます。
        ' イベントハンドラのライフサイクルはこのViewModelと同じなのでリークしませんし、
        ' イベントハンドラがViewModelより先に消えません。
        ViewModelHelper.BindNotifyChanged(
            _model, Me,
            Sub(sender, e)
                RaisePropertyChanged(e.PropertyName)
                If (e.PropertyName = "Birthday") Then
                    RaisePropertyChanged("Age")
                End If
            End Sub)
    End Sub

#Region "Modelプロパティのラッパー"

    Public Property Name() As String
        Get
            Return _model.Name
        End Get
        Set(ByVal value As String)
            _model.Name = value
        End Set
    End Property

    Public Property Birthday() As DateTime
        Get
            Return _model.Birthday
        End Get
        Set(ByVal value As DateTime)
            _model.Birthday = value
        End Set
    End Property

    Public Property Memo() As String
        Get
            Return _model.Memo
        End Get
        Set(ByVal value As String)
            _model.Memo = value
        End Set
    End Property

#End Region

#Region "ViewModelオリジナルのプロパティ"

    Public ReadOnly Property Age As Integer
        Get
            Return CInt((DateTime.Now - Birthday).Days / 365)
        End Get
    End Property

    Private _Checked As Boolean
    ' チェックされているかを取得、または設定します。
    Public Property Checked() As Boolean
        Get
            Return _Checked
        End Get
        Set(ByVal value As Boolean)
            If (_Checked = value) Then Return
            _Checked = value
            RaisePropertyChanged("Checked")
        End Set
    End Property

    Private _MainWindowViewModel As MainWindowViewModel
    Public Property MainWindowViewModel() As MainWindowViewModel
        Get
            Return _MainWindowViewModel
        End Get
        Private Set(ByVal value As MainWindowViewModel)
            _MainWindowViewModel = value
        End Set
    End Property

#End Region

#Region "入力用プロパティ"

    Private _inputName As String
    ' 名前の入力用プロパティです。
    Public Property InputName() As String
        Get
            Return _inputName
        End Get
        Set(ByVal value As String)
            _inputName = value
            If (String.IsNullOrEmpty(_inputName.Trim())) Then
                _errors("InputName") = "名前は必須です"
            Else
                _errors("InputName") = Nothing
            End If
            RaisePropertyChanged("Error")
        End Set
    End Property

    Private _inputBirthday As String
    ' 生年月日の入力用プロパティです。
    Public Property InputBirthday() As String
        Get
            Return _inputBirthday
        End Get
        Set(ByVal value As String)
            _inputBirthday = value

            Dim inputDateTime As DateTime
            If (String.IsNullOrEmpty(_inputBirthday)) Then
                _errors("InputBirthday") = "生年月日は必須です"
            ElseIf (Not DateTime.TryParse(_inputBirthday, inputDateTime)) Then
                _errors("InputBirthday") = "年月日として不正な形式です"
            ElseIf (inputDateTime > DateTime.Now) Then
                _errors("InputBirthday") = "未来の日付は指定できません"
            Else
                _errors("InputBirthday") = Nothing
            End If
            RaisePropertyChanged("Error")
        End Set
    End Property

    ' 備考の入力用プロパティです
    Public Property InputMemo As String

#End Region

    ' 入力値用プロパティを初期化します
    Private Sub InitializeInput()
        _inputName = _model.Name
        If (_model.Birthday <> DateTime.MinValue) Then
            _inputBirthday = _model.Birthday.ToString("yyyy/MM/dd")
        End If
        _InputMemo = _model.Memo
        _errors.Clear()
    End Sub

#Region "SaveCommand"
    Private _SaveCommand As DelegateCommand

    Public ReadOnly Property SaveCommand() As DelegateCommand
        Get
            If _SaveCommand Is Nothing Then
                _SaveCommand = New DelegateCommand(AddressOf Save, AddressOf CanSave)
            End If
            Return _SaveCommand
        End Get
    End Property

    Private Function CanSave() As Boolean
        If (Not String.IsNullOrEmpty(Me.Error)) Then
            Return False
        End If
        If (String.IsNullOrEmpty(InputName) OrElse String.IsNullOrEmpty(InputBirthday)) Then
            Return False
        End If
        Return True
    End Function

    Private Sub Save()
        Name = InputName
        Birthday = DateTime.Parse(InputBirthday)
        Memo = InputMemo

        If (Not _model.IsIncludedInMainCollection) Then
            _model.AddThisToMainCollection()
        End If

        ' Viewに画面遷移用メッセージを送信しています。
        ' Viewは対応するメッセージキーを持つInteractionTransitionMessageTriggerでこのメッセージを受信します。
        Messenger.Raise(New WindowActionMessage("Close", WindowAction.Close))
    End Sub

#End Region

#Region "CancelCommand"
    Private _CancelCommand As DelegateCommand

    Public ReadOnly Property CancelCommand() As DelegateCommand
        Get
            If _CancelCommand Is Nothing Then
                _CancelCommand = New DelegateCommand(AddressOf Cancel)
            End If
            Return _CancelCommand
        End Get
    End Property

    Private Sub Cancel()
        ' 入力情報初期化
        InitializeInput()

        ' Viewに画面遷移用メッセージを送信しています。
        ' Viewは対応するメッセージキーを持つInteractionTransitionMessageTriggerでこのメッセージを受信します。
        Messenger.Raise(New WindowActionMessage("Close", WindowAction.Close))
    End Sub
#End Region

#Region "RemoveCommand"
    Private _RemoveCommand As DelegateCommand

    Public ReadOnly Property RemoveCommand() As DelegateCommand
        Get
            If _RemoveCommand Is Nothing Then
                _RemoveCommand = New DelegateCommand(AddressOf Remove, AddressOf CanRemove)
            End If
            Return _RemoveCommand
        End Get
    End Property

    Private Function CanRemove() As Boolean
        Return Checked
    End Function

    Private Sub Remove()
        _model.RemoveThisFromMainCollection()
    End Sub

#End Region

#Region "IDataErrorInfo"

    Public ReadOnly Property [Error] As String Implements IDataErrorInfo.Error
        Get
            Dim errorPropertyList = New List(Of String)()

            If (Not String.IsNullOrEmpty(Me("InputName"))) Then
                errorPropertyList.Add("名前")
            End If
            If (Not String.IsNullOrEmpty(Me("InputBirthday"))) Then
                errorPropertyList.Add("生年月日")
            End If
            If (errorPropertyList.Count <> 0) Then
                Return String.Join("・", errorPropertyList.ToArray()) + "が不正です"
            End If

            Return Nothing
        End Get
    End Property

    Default Public ReadOnly Property Item(columnName As String) As String Implements IDataErrorInfo.Item
        Get
            If (_errors.Keys.Contains(columnName)) Then
                Return _errors(columnName)
            Else
                Return Nothing
            End If
        End Get
    End Property

#End Region

End Class


コマンドの実装もコードスニペットを使います。「ldcom」と入力して TAB キー二回でコマンドのスニペットが挿入されます。


コマンド名のプレフィックス部分を書き換えると、#Regionディレクティブ・フィールド・ヘルパーメソッドのプレフィックスをすべて置き換えます。これもいい感じです。

最後は View でんがな。

DetailWindow です。基本的に XAML コピってクラスと参照だけ書き換えてるだけですが、実は本日現在(2011/07/20) bitbucket に公開されているサンプルは古いやつだそうで、クラス名が一部変わってしまっててビルドできないため修正しました。XAML コピペすりゃすぐ終わるだろうと思ってただけに一瞬焦りましたw でも bitbucket でコミット履歴の詳細が閲覧できるため、どのクラス名がどう置き換わったかすぐ調べることができたので助かりました。

<Window x:Class="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"
        Title="メンバー詳細" Height="300" Width="300"
        WindowStartupLocation="CenterScreen">
    <Window.Resources>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="HorizontalAlignment" Value="Right"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
        </Style>
        <Style TargetType="{x:Type TextBox}">
            <Setter Property="HorizontalAlignment" Value="Stretch"/>
            <Setter Property="VerticalAlignment" Value="Center"/>
            <Setter Property="Height" Value="30"/>
            <Setter Property="Margin" Value="5"/>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip" 
                            Value="{Binding RelativeSource={RelativeSource Self}, 
                            Path=(Validation.Errors)[0].ErrorContent}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <!--Closeというメッセージキーを持つメッセージがViewModelから届いた際に起動するトリガーです-->
    <i:Interaction.Triggers>
        <l:InteractionMessageTrigger MessageKey="Close" Messenger="{Binding Messenger}">
            <l:WindowInteractionMessageAction/>
        </l:InteractionMessageTrigger>
    </i:Interaction.Triggers>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Row="0" Grid.Column="0">名前:</TextBlock>
        <TextBlock Grid.Row="1" Grid.Column="0">誕生日:</TextBlock>
        <TextBlock Grid.Row="2" Grid.Column="0">備考:</TextBlock>

        <TextBox Grid.Row="0" Grid.Column="1" Text="{Binding InputName,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged}"/>
        <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding InputBirthday,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged}"/>
        <TextBox Grid.Row="2" Grid.Column="1" Text="{Binding InputMemo,ValidatesOnDataErrors=True,UpdateSourceTrigger=PropertyChanged}"/>
        <TextBlock Grid.Row="3" Grid.ColumnSpan="2" TextWrapping="Wrap" FontSize="16" Foreground="Red" FontWeight="Bold" Text="{Binding Error}" HorizontalAlignment="Center"/>

        <StackPanel Height="30" Grid.Row="4" Grid.ColumnSpan="2" Margin="5" Orientation="Horizontal" HorizontalAlignment="Right">
            <Button Width="70" Command="{Binding SaveCommand}">確定</Button>
            <Button Width="70" Command="{Binding CancelCommand}">キャンセル</Button>
        </StackPanel>
    </Grid>
</Window>


とりは MainWindow です。View クラス名・参照および Livert のクラス名を一部最新のものに書き換えてます。


この XAML、Livet の機能をよく表しています。ViewModelを経由せずにメッセージを生成して Windowを閉じてたり、ViewModel を経由せず詳細 View を表示してたりしてます。XAML 中のコメントに注目してください。

<Window x:Class="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:local="clr-namespace:LivetVBSampleProject"
        Title="メンバー管理" Height="350" Width="525">

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <i:Interaction.Triggers>

        <!--ViewからのTransitionというメッセージキーを持つメッセージを受信します-->
        <!--TransitionInteractionMessageAction で画面遷移を行っています-->
        <l:InteractionMessageTrigger MessageKey="Transition" Messenger="{Binding Messenger}">
            <l:TransitionInteractionMessageAction WindowType="{x:Type local:DetailWindow}" Mode="Modal"/>
        </l:InteractionMessageTrigger>

    </i:Interaction.Triggers>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <Grid Grid.Row="0" VerticalAlignment="Center">

            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition/>
                <ColumnDefinition Width="80"/>
                <ColumnDefinition Width="80"/>
                <ColumnDefinition Width="50"/>
            </Grid.ColumnDefinitions>

            <TextBlock Grid.Column="0" FontSize="17">メンバー管理</TextBlock>

            <Button Grid.Column="2" Command="{Binding EditNewCommand}">追加</Button>

            <!--
            DelegateCommand.LatestCanExecuteResultプロパティは最新のCanExecuteの結果をboolで保持します。
            コントロールのCommandプロパティを使用しない場合の、コマンドの実行可否状態によるコントロールの制御に使用します。
            -->
            <Button Grid.Column="3" Content="削除" IsEnabled="{Binding RemoveCommand.LatestCanExecuteResult}">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <l:ConfirmationDialogInteractionMessageAction>
                            <!--
                            DirectInteractionMessageのCallbackCommandプロパティにコマンドを設定する事で
                            Viewで生成したメッセージを元にアクション実行後、コマンドを実行させる事ができます。
                            その場合、コマンドには引数としてメッセージが渡ります
                            -->
                            <l:DirectInteractionMessage CallbackCommand="{Binding RemoveCommand}">
                                <l:ConfirmationMessage Button="OKCancel" 
                                                  Caption="確認"
                                                  Text="本当にチェックの付けられたメンバー情報を削除しますか?"
                                                  Image="Information"/>
                            </l:DirectInteractionMessage>
                        </l:ConfirmationDialogInteractionMessageAction>
                    </i:EventTrigger>
                </i:Interaction.Triggers>

            </Button>

            <!--ViewModelを経由せずにメッセージを生成し、Windowを閉じています-->
            <!--Livetでは、ViewModelを経由する必要のない相互作用をこの様にView内で完結させられます-->
            <Button Grid.Column="4" Content="終了">
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="Click">
                        <l:WindowInteractionMessageAction>
                            <l:DirectInteractionMessage>
                                <l:WindowActionMessage Action="Close"/>
                            </l:DirectInteractionMessage>
                        </l:WindowInteractionMessageAction>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </Button>

        </Grid>

        <ListView Grid.Row="1" ItemsSource="{Binding Members}">
            <ListView.View>
                <GridView>

                    <GridViewColumn Width="30">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <CheckBox IsChecked="{Binding Checked}"/>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <GridViewColumn Header="名前" Width="100">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding Name}"/>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <GridViewColumn Header="年齢" Width="40">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding Age}"/>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <GridViewColumn Header="生年月日" Width="85">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding Birthday,StringFormat=yyyy/MM/dd}" />
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <GridViewColumn Header="備考" Width="170">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Text="{Binding Memo}" />
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>

                    <GridViewColumn Width="65">
                        <GridViewColumn.CellTemplate>
                            <DataTemplate>
                                <Button Width="50" Content="変更">
                                    <i:Interaction.Triggers>
                                        <i:EventTrigger EventName="Click">
                                            <!--Viewから詳細ウィンドウを表示させています。ViewModelを経由させていません-->
                                            <l:TransitionInteractionMessageAction Mode="Modal" WindowType="{x:Type local:DetailWindow}">
                                                <l:DirectInteractionMessage>
                                                    <l:TransitionMessage TransitionViewModel="{Binding}"/>
                                                </l:DirectInteractionMessage>
                                            </l:TransitionInteractionMessageAction>
                                        </i:EventTrigger>
                                    </i:Interaction.Triggers>
                                </Button>
                            </DataTemplate>
                        </GridViewColumn.CellTemplate>
                    </GridViewColumn>
                </GridView>
            </ListView.View>
        </ListView>

    </Grid>
</Window>

まとめ


このサンプルを見てわかるとおり、Window を表示するのも、ViewModel と Model 間の通信をするのも、どこにもイベントハンドラは出てきません。バインディングと WeakEvent パターンを使っているためメモリリークの心配がなく、また Livet を使うユーザーは WeakEvent パターンの実装に頭を悩める必要もありません。
しかも純国産インフラのため英語で悩む必要もないという素晴らしい MVVM インフラです。しかも VB ユーザーでも使える!MVVM は難度が高いうえ生産性が低すぎるという方に是非試して頂きたいツールですね。