VB のたまご

作成日: 2017/04/02, 更新日: 2017/04/02



ViewModelとModelの会話

    イメージ

  •  ここではViewModel と Model のやり取りを学習します。 と言っても図にある通り、プロパティ宣言したモデルのメソッドを呼び出すか、INotifyPropertyChanged.PropertyChanged イベントを購読してイベント処理をおこなうかだけなので、難しくはありません。

  • 以降のサンプルは GitHub にもアップしています。

  • GitHub
    https://github.com/sutefu7/LearnToMemoryLeakAboutEvent
    Src/ConsoleApplication0.ForWPF.VB
    または
    Src/ConsoleApplication0.ForWPF
    

    スポンサーリンク


  •  それでは、どう動かせばいいかサンプルを見ていきましょう。 以下は、「Livet WPF4.5 MVVM アプリケーション」で作成しています。

  • Namespace Models
        Public Class Model ' Friend → Public
            Inherits NotificationObject
    
    #Region "Name変更通知プロパティ"
            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()
                End Set
            End Property
    #End Region
    
    #Region "LastUpdateTime変更通知プロパティ"
            Private _LastUpdateTime As DateTime
    
            Public Property LastUpdateTime() As DateTime
                Get
                    Return _LastUpdateTime
                End Get
                Set(ByVal value As DateTime)
                    If (_LastUpdateTime = value) Then Return
                    _LastUpdateTime = value
                    RaisePropertyChanged()
                End Set
            End Property
    #End Region
    
            ' DB からデータ取得(したつもり)
            Public Sub LoadData()
    
                Me.Name = "taro"
                Me.LastUpdateTime = DateTime.Now
    
            End Sub
    
            ' DB へデータ更新(したつもり)
            Public Sub UpdateData()
    
                ' String 型を受け取り、DB 更新結果として True / False を返却するメソッド
                Dim executeUpdate As Func(Of String, Boolean) = Function(s) True
    
                ' Name プロパティを、DB 更新処理をしたとする
                If executeUpdate(Me.Name) Then
                    Me.LastUpdateTime = DateTime.Now
                Else
                    Throw New Exception("DB 更新に失敗しました")
                End If
    
            End Sub
    
        End Class
    End Namespace
    

    Imports LivetWPFApplication1.Models
    Imports System.ComponentModel
    
    Namespace ViewModels
        Public Class MainWindowViewModel
            Inherits ViewModel
    
    
    #Region "Person変更通知プロパティ"
            Private _Person As Model
    
            Public Property Person() As Model
                Get
                    If _Person Is Nothing Then
                        _Person = New Model
                    End If
                    Return _Person
                End Get
                Set(ByVal value As Model)
                    If (_Person.Name = value.Name) Then Return
                    _Person = value
                    RaisePropertyChanged()
                End Set
            End Property
    #End Region
    
    #Region "Name変更通知プロパティ"
            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()
                End Set
            End Property
    #End Region
    
    #Region "UpdateCommand"
            Private _UpdateCommand As ViewModelCommand
    
            Public ReadOnly Property UpdateCommand() As ViewModelCommand
                Get
                    If _UpdateCommand Is Nothing Then
                        _UpdateCommand = New ViewModelCommand(AddressOf Update)
                    End If
                    Return _UpdateCommand
                End Get
            End Property
    
            ' ViewModel → Model への会話
            ' 実際におこないたい処理はモデルクラスに書いておき、ViewModel では処理の委譲のみする
            Private Sub Update()
    
                Me.Person.UpdateData()
    
            End Sub
    #End Region
    
            ' WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます
            ' つまり画面の初期化時におこないたい処理を書きます
            Public Sub Initialize()
    
                AddHandler Me.Person.PropertyChanged, AddressOf Me.Person_PropertyChanged
                Me.Person.LoadData()
    
            End Sub
    
            ' Model → ViewModel への会話
            Private Sub Person_PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
    
                Dim value = Me.Person.GetType().GetProperty(e.PropertyName).GetValue(Me.Person, Nothing)
                Console.WriteLine($"{e.PropertyName} = {value} に変更されました")
    
                ' 管理用データから表示向けデータ形式に微調整して表示
                If e.PropertyName = "Name" Then
                    Me.Name = $"{value} 様"
                End If
    
            End Sub
    
        End Class
    End Namespace
    

    <Window x:Class="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:LivetWPFApplication1.Views"
            xmlns:vm="clr-namespace:LivetWPFApplication1.ViewModels"
            Title="MainWindow" Height="350" Width="525">
        
        <Window.DataContext>
            <vm:MainWindowViewModel/>
        </Window.DataContext>
        
        <i:Interaction.Triggers>
            
            <!--WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます-->
            <i:EventTrigger EventName="ContentRendered">
                <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
            </i:EventTrigger>
    
            <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
            <i:EventTrigger EventName="Closed">
                <l:DataContextDisposeAction/>
            </i:EventTrigger>
    
            <!--WindowのCloseキャンセル処理に対応する場合は、WindowCloseCancelBehaviorの使用を検討してください-->
    
        </i:Interaction.Triggers>
    
        <StackPanel>
    
            <Button Content="データ更新" Command="{Binding UpdateCommand}" />
            <TextBox Text="{Binding Person.Name, UpdateSourceTrigger=PropertyChanged}" />
            <TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" />
            <TextBox Text="{Binding Person.LastUpdateTime, StringFormat={}{0:yyyy/MM/dd HH:mm:ss.fff}, UpdateSourceTrigger=PropertyChanged}" />
    
        </StackPanel>
    
    </Window>
    

  •  ViewModel と Model の会話は、ViewModel でおこないます。 そして、今回のモデルクラスには状態の他に振る舞いも追加していますが、(画面処理に関わらない限り)基本的に何かをしたい時は、その処理はモデルクラスが担当します。

  •  ViewModel から Model への会話は、モデルクラスに定義したメソッドを呼び出しておこないます。ViewModel はモデル層を知っているからできることです。Update メソッドのところですね。

  •  逆に、Model から ViewModel への会話はイベント発生を使っておこないます。あらかじめ ViewModel でモデルクラスとイベント登録しておき、イベント通知をキャッチしておこないます。 ソースで言うと、Initialize メソッドでイベントを購読しておき、モデルからの変更通知イベントの発生により、Person_PropertyChanged イベントハンドラが実行される流れになります。

  •  このプログラムでは、モデル層で定義したデータの他に、表示用データを ViewModel に持っており、モデル層のデータ変更時に表示用データを加工して、画面に表示させています。 名前の後ろに「様」を付けている部分です。

  • スポンサーリンク



イベント解除もしないと、忘れた頃にメモリリークしちゃうかも

  •  メモリリークは OutOfMemoryException 例外エラーのことです。詳しくは イベントに関するメモリリークを学ぶ をご参照ください(長いですorz)。 WPF では ViewModel よりも Model の方が寿命(生存期間)が長いことが多いです。 ViewModel に定義したイベントハンドラを Model に紐づけていると、見た目的に ViewModel が破棄されたとしても、Model が生きている間は ViewModel もまだ生きています (ただアクセスできなくなっているので、破棄しようにも破棄できない、解放できない不要メモリになってしまいます)。

  •  上記のサンプルでは、Model も ViewModel も生存期間が同じなのでこのような状態にはなりません。 そこで、次のようなシナリオのアプリケーションをサンプルに見ていきましょう。 画面を表示すると、現在管理中のデータが読み取り専用で表示されます。変更するには、ボタンを押して、編集画面を開くことで変更することができます。 表示データが連動してしまうのはちょっとおかしいですが、まあここでは気にしないことにします。

  •  ここでは1つ目の画面に表示するデータ、2つ目の画面に表示するデータは同じものです。つまり1つのデータを持ちつつ、画面遷移するサンプルです。

  • Namespace Models
        Public Class Model ' Friend → Public
            Inherits NotificationObject
    
    #Region "Name変更通知プロパティ"
            Private _Name As String = "default"
    
            Public Property Name() As String
                Get
                    Return _Name
                End Get
                Set(ByVal value As String)
                    If (_Name = value) Then Return
                    _Name = value
                    RaisePropertyChanged()
                End Set
            End Property
    #End Region
    
    #Region "Age変更通知プロパティ"
            Private _Age As Integer = 32
    
            Public Property Age() As Integer
                Get
                    Return _Age
                End Get
                Set(ByVal value As Integer)
                    If (_Age = value) Then Return
                    _Age = value
                    RaisePropertyChanged()
                End Set
            End Property
    #End Region
    
            Public Shared Function ValueEquals(self As Model, other As Model) As Boolean
    
                If (self Is Nothing) OrElse (other Is Nothing) Then
                    Return False
                End If
    
                If self.Name <> other.Name Then
                    Return False
                End If
    
                If self.Age <> other.Age Then
                    Return False
                End If
    
                Return True
    
            End Function
    
        End Class
    End Namespace
    

    Imports LivetWPFApplication2.Models
    Imports System.ComponentModel
    
    Namespace ViewModels
        Public Class ModalWindow1ViewModel
            Inherits ViewModel
    
    #Region "Person変更通知プロパティ"
            Private _Person As Model
    
            Public Property Person() As Model
                Get
                    Return _Person
                End Get
                Set(ByVal value As Model)
                    If Model.ValueEquals(_Person, value) Then Return
                    _Person = value
                    RaisePropertyChanged()
                End Set
            End Property
    #End Region
    
    #Region "Name変更通知プロパティ"
            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()
                End Set
            End Property
    #End Region
    
            Public Sub New(person As Model)
    
                Me.Person = person
    
                ' モデルの変更通知イベントを購読
                AddHandler Me.Person.PropertyChanged, AddressOf Me.Person_PropertyChanged
    
                ' 初回のみ手動設定
                Me.Person_PropertyChanged(Me, New PropertyChangedEventArgs("Name"))
    
            End Sub
    
            Public Sub Initialize()
            End Sub
    
            ' Model → ViewModel への会話
            Private Sub Person_PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
    
                ' 管理用データから表示向けデータ形式に微調整して表示
                If e.PropertyName = "Name" Then
    
                    Dim value = Me.Person.GetType().GetProperty(e.PropertyName).GetValue(Me.Person, Nothing)
                    Me.Name = $"{value} 様"
    
                End If
    
            End Sub
    
        End Class
    
    End Namespace
    

    Imports LivetWPFApplication2.Models
    
    Namespace ViewModels
        Public Class MainWindowViewModel
            Inherits ViewModel
    
    #Region "Person変更通知プロパティ"
            Private _Person As Model
    
            Public Property Person() As Model
                Get
                    If _Person Is Nothing Then
                        _Person = New Model
                    End If
                    Return _Person
                End Get
                Set(ByVal value As Model)
                    If Model.ValueEquals(_Person, value) Then Return
                    _Person = value
                    RaisePropertyChanged()
                End Set
            End Property
    #End Region
    
    #Region "ShowModalCommand"
            Private _ShowModalCommand As ViewModelCommand
    
            Public ReadOnly Property ShowModalCommand() As ViewModelCommand
                Get
                    If _ShowModalCommand Is Nothing Then
                        _ShowModalCommand = New ViewModelCommand(AddressOf ShowModal)
                    End If
                    Return _ShowModalCommand
                End Get
            End Property
    
            Private Async Sub ShowModal()
    
                Using vm = New ModalWindow1ViewModel(Me.Person)
    
                    Await Messenger.RaiseAsync(New TransitionMessage(vm, "ShowModalWindow"))
    
                End Using
    
            End Sub
    #End Region
    
            Public Sub Initialize()
            End Sub
    
        End Class
    End Namespace
    

    <Window x:Class="Views.ModalWindow1"
            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:LivetWPFApplication2.Views"
            xmlns:vm="clr-namespace:LivetWPFApplication2.ViewModels"
            Title="ModalWindow1" Height="250" Width="425"
            WindowStartupLocation="CenterOwner">
        
         <i:Interaction.Triggers>
         
            <!--WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます-->
            <i:EventTrigger EventName="ContentRendered">
                <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
            </i:EventTrigger>
    
            <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
            <i:EventTrigger EventName="Closed">
                <l:DataContextDisposeAction/>
            </i:EventTrigger>
    
            <!--WindowのCloseキャンセル処理に対応する場合は、WindowCloseCancelBehaviorの使用を検討してください-->
    
        </i:Interaction.Triggers>
    
        <StackPanel Margin="20">
    
            <TextBox Text="{Binding Person.Name, UpdateSourceTrigger=PropertyChanged}" />
            <TextBox Text="{Binding Person.Age, UpdateSourceTrigger=PropertyChanged}" />
            <TextBlock Text="{Binding Name}" />
    
        </StackPanel>
        
    </Window>
    

    <Window x:Class="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:LivetWPFApplication2.Views"
            xmlns:vm="clr-namespace:LivetWPFApplication2.ViewModels"
            Title="MainWindow" Height="350" Width="525">
        
        <Window.DataContext>
            <vm:MainWindowViewModel/>
        </Window.DataContext>
        
        <i:Interaction.Triggers>
            
            <!--WindowのContentRenderedイベントのタイミングでViewModelのInitializeメソッドが呼ばれます-->
            <i:EventTrigger EventName="ContentRendered">
                <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>
            </i:EventTrigger>
    
            <!--Windowが閉じたタイミングでViewModelのDisposeメソッドが呼ばれます-->
            <i:EventTrigger EventName="Closed">
                <l:DataContextDisposeAction/>
            </i:EventTrigger>
    
            <!--WindowのCloseキャンセル処理に対応する場合は、WindowCloseCancelBehaviorの使用を検討してください-->
    
            <!-- モーダル表示する画面 -->
            <l:InteractionMessageTrigger MessageKey="ShowModalWindow" Messenger="{Binding Messenger}">
                <l:TransitionInteractionMessageAction WindowType="{x:Type v:ModalWindow1}" Mode="Modal" />
            </l:InteractionMessageTrigger>
            
        </i:Interaction.Triggers>
    
        <StackPanel Margin="10">
    
            <Button Content="モーダル画面を表示" Command="{Binding ShowModalCommand}" />
            <TextBlock Text="{Binding Person.Name}" />
            <TextBlock Text="{Binding Person.Age}" />
    
        </StackPanel>
    
    </Window>
    

  •  ここで注目するのは、MainWindowViewModel クラスの ShowModal メソッドです。 ここではモーダル画面を表示する処理をしていますが、ModalWindow1ViewModel クラスを生成、破棄(Using ステートメント)しています。

  •  ModalWindow1ViewModel クラスのコンストラクタでは、イベントを購読していて、どこにもイベント解除は記載していません。 これを開いて閉じて、開いて閉じて、するわけです。

  •  長くなったのでいったん区切ります。次回に続きます。