ラベル MVVM の投稿を表示しています。 すべての投稿を表示
ラベル MVVM の投稿を表示しています。 すべての投稿を表示

■ VisualBasic+WPF+YAML #13:Modelの変更をViewへ通知する 2

VBからYMALファイルを読み込みたい。
前回記事:VisualBasic+WPF+YAML #12:Modelの変更をViewへ通知する



Modelの変更をViewへ通知するには?

Model(TestDataクラス)の値をプログラム内部で変更したとしてもView(MainWindow)には反映されない。
反映させるには、ModelからViewへ変更を通知しなければならない。

前回の誤りとして、
・ItemDataクラスからTestDataクラスへの通知は不要。
(前回はTestDataクラスにINotifyインターフェースを実行、ItemDataからTestDataへプロパティの変更通知をしていた)

そんな事は不要で、単にItemDataクラスに System.CompornentModelのINotifyPropertyChangedインターフェースを実装し、変更のあったプロパティの変更通知を出せばよい。
後は何も考えずに、Viewが勝ってに変更通知を受け取り画面を更新してくれる。
■ItemData.vb
Imports System.ComponentModel

Public Class ItemData
    Implements INotifyPropertyChanged

    Public Property id As Integer
    Public Property name As String
    Public Property subitems As New List(Of SubItemData)
    Private _checked As Boolean = True

    Public Property checked As Boolean
        Get
            Return _checked
        End Get
        Set(value As Boolean)
            If (value <> _checked) Then
                _checked = value
                NotifyPropertyChanged("checked")
            End If
        End Set
    End Property

    Private Sub NotifyPropertyChanged(ByVal PropertyName As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(PropertyName))
    End Sub

#Region "INotifyPropertyChanged"
    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
#End Region

End Class    

次に TestDataクラス、このクラスも同様でTestDataからViewModelへの通知は不要。
なので、前回修正した箇所を元に戻す。
■TestData.vb
Imports YamlDotNet.RepresentationModel

Public Class TestData

    Public Property name As String
    Public Property items As New List(Of ItemData)

    Public Sub Load()
        Dim yaml As New YamlStream()
        Using sr As New System.IO.StreamReader("test.yaml")
            yaml.Load(sr)
        End Using

        Dim children = DirectCast(yaml.Documents(0).RootNode, YamlMappingNode).Children
        name = DirectCast(children.Item(New YamlScalarNode("name")), YamlScalarNode).Value

        For Each itemNode In DirectCast(children.Item(New YamlScalarNode("items")), YamlSequenceNode)
            Dim item As New ItemData()
            item.id = DirectCast(itemNode.Item(New YamlScalarNode("id")), YamlScalarNode).Value
            item.name = DirectCast(itemNode.Item(New YamlScalarNode("name")), YamlScalarNode).Value
            items.Add(item)

            For Each subItemNode In DirectCast(itemNode.Item(New YamlScalarNode("subitems")), YamlSequenceNode)
                Dim subitem As New SubItemData()
                subitem.name = DirectCast(subItemNode.Item(New YamlScalarNode("name")), YamlScalarNode).Value
                item.subitems.Add(subitem)
            Next

        Next

    End Sub

End Class

ViewModelも前回の修正を元に戻す。
■ ViewModel.vb
Public Class ViewModel

Public Property Model As New TestData()

Private WithEvents _ClickEvent As ClickEventCommand
Public ReadOnly Property ClickEvent As ClickEventCommand
    Get
        If (_ClickEvent Is Nothing) Then
            _ClickEvent = New ClickEventCommand()
        End If
        Return _ClickEvent
    End Get
End Property

Private Sub _ClickEvent_ShowMessage(id As Integer) Handles _ClickEvent.ShowMessage
    For Each item In Model.items.Where(Function(x) x.id = id)
        item.checked = item.checked Xor True
    Next
End Sub

End Class

プログラムを実行し、DataGrid上のボタンを押すことでチェックボックスの状態が変化するか確認する。



次回記事:

-- 以上 --


VisualBasic, WPF, DataGrid, YAML, MVVM

■ VisualBasic+WPF+YAML #12:Modelの変更をViewへ通知する

VBからYMALファイルを読み込みたい。
前回記事:VisualBasic+WPF+YAML #11:DataGridのイベント 4

この回の手順は失敗。解決法が分かったので次回記事にまとめる。



Modelの変更をViewへ通知するには?

Model(TestDataクラス)の値をプログラム内部で変更したとしてもView(MainWindow)には反映されない。
反映させるには、ModelからViewへ変更を通知しなければならない。

例えば、前回までのソースで、ボタンをクリックする毎にchecked値を反転させる処理に変更する。
■ViewModel.vb
    Private Sub _ClickEvent_ShowMessage(id As Integer) Handles _ClickEvent.ShowMessage
        For Each item In Model.items.Where(Function(x) x.id = id)
            item.checked = item.checked Xor True
        Next
    End Sub
これでボタンクリックすれば選択のチェックボックスが付いたり消えたりする筈だが、そうはならない。

ということで、ItemDataクラスでcheckedプロパティの変更があれば、親クラス(TestDataクラス)へ通知、 さらにTestDataはViewModelへ変更通知を、ViewModelからViewへの変更通知を行うようにする。

ItemDataはコレクション(List)として格納されている。
ItemDataからTestDataへの変更通知をどうすれば良いか?
いい案が浮かばないので、ItemDataに親のTestDataオブジェクトを持たせ、ItemDataからTestDataのメソッドを呼び出すことにした。
このためのインターフェースクラス(INotify.vb)を作成、
メソッドとしては子からの変更を受け取るメソッド "ChildrenPropertyChanged" を作成する。
■INotify.vb
Public Interface INotify

    Sub ChildrenPropertyChanged(Name As String, Value As Object)

End Interface
※ 引数はプロパティ名(Name)だけでも良いが、何か使うかもしれないのでプロパティ値(Value)も付けておいた。

ItemDataクラスでcheckedプロパティ値に変更が発生したらINofityインターフェースのChildrenPropertyChangedメソッドを呼び出すように修正。
また親オブジェクトをコンストラクタで取得するように修正。
■ItemData.vb
Public Class ItemData

    Private _Parent As INotify

    Public Property id As Integer
    Public Property name As String
    Public Property subitems As New List(Of SubItemData)
    Private _checked As Boolean = True
    Public Property checked As Boolean
        Get
            Return _checked
        End Get
        Set(value As Boolean)
            If (_checked <> value) Then
                _checked = value
                _Parent.ChildrenPropertyChanged("checked", _checked)
            End If
        End Set
    End Property

    Public Sub New(Parent As INotify)
        _Parent = Parent
    End Sub

End Class

TestDataクラスはINotifyインターフェースを継承する。
またTestDataからViewModelへの変更通知は System.ComponentModelのINotifyPropertyChangedインターフェースを継承することで対応。
ItemDataオブジェクト生成時に自身のインスタンスを渡すように修正。
■TestData.vb
Imports YamlDotNet.RepresentationModel
Imports System.ComponentModel

Public Class TestData
    Implements INotify, INotifyPropertyChanged
 :
    Public Sub Load()
     :
        For Each itemNode In DirectCast(children.Item(New YamlScalarNode("items")), YamlSequenceNode)
            Dim item As New ItemData(Me)
            :
        Next

    End Sub

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    Public Sub ChildrenPropertyChanged(Name As String, Value As Object) Implements INotify.ChildrenPropertyChanged
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(Name))
    End Sub

ViewModelはSystem.ComponentModelのINotifyPropertyChangedインターフェースを継承、Model変数はWithEvents付きで宣言し、Modelからの変更を受信できるようにする。
■ViewModel.vb
Imports System.ComponentModel

Public Class ViewModel
    Implements INotifyPropertyChanged

    Private WithEvents _Model As New TestData()
    Public ReadOnly Property Model As TestData
        Get
            Return _Model
        End Get
    End Property
    Private Sub _Model_PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Handles _Model.PropertyChanged
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(e.PropertyName))
    End Sub
 :



次回記事:VisualBasic+WPF+YAML #13:Modelの変更をViewへ通知する 2

-- 以上 --


VisualBasic, WPF, DataGrid, YAML, MVVM

■ VisualBasic+WPF+YAML #11:DataGridのイベント 4

VBからYMALファイルを読み込みたい。
前回記事:VisualBasic+WPF+YAML #8:DataGridのイベント



チェック状態がModelに反映されないのはなぜ?

Viewでチェックした値がModelに反映しない

これは既定値の動作のようで、OneWay という一方通行(ViewModel→View)の動作であった。
これを双方向(VIewModel⇔View)とするには、チェックボックスのBindingに"Mode=TwoWay"を指定する。
■MainWindow.xaml
<DataGridCheckBoxColumn Header="選択" Binding="{Binding checked, Mode=TwoWay}" />

これで、画面でチェックした個数がメッセージボックスに表示される筈であるが・・・
ちょっと動きがおかしい
チェック値を変更したあと、フォーカスを別行に移動させないと正しいチェック数で表示されない。

これは UpdateSourceTriggerプロパティが関係してるようで、既定は "LostFocus"(フォーカスを失うたびにバインドソースを更新) となっている為。
ということで、プロパティ値変更のたびにバインドソースを変更するように"PropertyChanged"を設定する。
■MainWindow.xaml
<DataGridCheckBoxColumn Header="選択" Binding="{Binding checked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

これで即時正しい値で表示されるようになった。



次回記事:

-- 以上 --


VisualBasic, WPF, DataGrid, YAML, MVVM

■ VisualBasic+WPF+YAML #10:DataGridのイベント 3

VBからYMALファイルを読み込みたい。
前回記事:VisualBasic+WPF+YAML #9:DataGridのイベント 2



イベントのパラメータ

DataGridからButtonをクリックした際、Commandオブジェクトではパラメータが1つしか受け取れないため、
(複数パラメータを受け取る方法は存在する筈だがうまく行っていない)
とりあえずはキーとなる項目(id)を渡し、Commandオブジェクト側(ViewModel側)でModel(TestData)より対象データを抽出することで対処する。

ClickEventCommandクラスでid値は取得できるところまでは出来ている。
さてこれからどうするか?
いい案が浮かばないが、ClickEventCommandからViewModelへイベント(ShowMessageイベント)を通知することにする。
■ClickEventCommand.vb
Public Class ClickEventCommand
    Implements ICommand

    Public Event ShowMessage(id As Integer)

    Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged

    Public Sub Execute(parameter As Object) Implements ICommand.Execute
        RaiseEvent ShowMessage(parameter)
    End Sub
    :

ViewModelでは、_ClickEventオブジェクトをWithEvents付きに変更、
ShowMessageイベントを受信できるようにする。
Public Class ViewModel

    Public Property Model As New TestData()

    Private WithEvents _ClickEvent As ClickEventCommand
    Public ReadOnly Property ClickEvent As ClickEventCommand
        Get
            If (_ClickEvent Is Nothing) Then
                _ClickEvent = New ClickEventCommand()
            End If
            Return _ClickEvent
        End Get
    End Property

    Private Sub _ClickEvent_ShowMessage(id As Integer) Handles _ClickEvent.ShowMessage
        MessageBox.Show(id)
    End Sub

End Class

プログラムを実行、ボタンClickでクリックした行のidがメッセージボックスで表示されることを確認。

LINQを使ってアイテムを抽出

ViewModeでクリック行のidが取得できたので、この値を元にModel(TestDataクラス)の対象アイテム(ItemDataクラス)を取得してみる。
せっかくなのでLINQというのを使ってみる。
LINQはまだ学習し始めなので雑に言うと「From x In 配列 Where 条件 x.id = id」のように書く。
xの名前は何でも良く、配列から抽出した1要素を表し、In に続き配列(コレクション)、Whereに条件を指定。
結果はコレクションで返る。

ということで、id値よりitemsを検索し、見つかったnameを表示させてみる。
■ViewModel.vb
    Private Sub _ClickEvent_ShowMessage(id As Integer) Handles _ClickEvent.ShowMessage
        Dim Result = From x In Model.items Where x.id = id

        For Each Item In Result
            MessageBox.Show(Item.name)
        Next

    End Sub
プログラムを実行し、クリック行のname値がメッセージボックスで表示されることを確認。

ちなみに、LINQは次のように書くこともできる。
    ' Dim Result = From x In Model.items Where x.id = id
    Dim Result = Model.items.Where(Function(x) x.id = id)

ついでに 選択欄のチェックした個数も表示させてみる。
    Private Sub _ClickEvent_ShowMessage(id As Integer) Handles _ClickEvent.ShowMessage
        Dim Result = From x In Model.items Where x.id = id
        Dim CheckedCount = (From x In Model.items Where x.checked = True).Count()

        For Each Item In Result
            MessageBox.Show(Item.name & " チェック数:" & CheckedCount)
        Next

    End Sub

しかし、チェックを外そうが付けようが常に "チェック数:2" となってしまう・・・



次回記事:VisualBasic+WPF+YAML #11:DataGridのイベント 4

-- 以上 --


VisualBasic, WPF, DataGrid, YAML, MVVM, LINQ

■ VisualBasic+WPF+YAML #9:DataGridのイベント 2

VBからYMALファイルを読み込みたい。
前回記事:VisualBasic+WPF+YAML #8:DataGridのイベント



イベントのパラメータ

DataGridに配置したButtonのイベントを処理することはできた。
が、どの行のボタンが押されたかを知るにはどうするか?

ButtonのCommandParameter属性に渡す値を指定すれば良い。
例えば items の id を渡す場合は以下のように指定する。
■MainWindow.vb
<Button Content="..."  Command="{Binding DataContext.ClickEvent, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" CommandParameter="{Binding id}" />

イベントを処理するClickEventCommandクラスではExecuteメソッドの引数 parameter で、CommandParameterで指定した値を取得できる。
試しに parameterの値を表示するように変更してみる。
■ClickEventCommand.vb
Public Sub Execute(parameter As Object) Implements ICommand.Execute
    MessageBox.Show(parameter)
End Sub
ボタンを押すと、押した行のid値がメッセージボックスで表示される。

パラメータを複数指定したい場合はどうするか?
MultiBindingってのがあるらしいが、どうやってもうまくいかない・・・
ので調査保留中。



次回記事:VisualBasic+WPF+YAML #10:DataGridのイベント 3

-- 以上 --


VisualBasic, WPF, DataGrid, YAML, MVVM

■ VisualBasic+WPF+YAML #8:DataGridのイベント

VBからYMALファイルを読み込みたい。
前回記事:VisualBasic+WPF+YAML #7:DataGridへの表示 5



イベント処理

DataGridに配置したButtonのClickイベントを処理するには? View(MainWindow)にButtonのイベント処理を書けば良さそうだが、MVVMの場合はイベントもBindingでやった方が良いらしい。
まずイベントを処理するクラス(ClickEventCommandクラスを作成。
ボタンクリックでは "OK" のMessageBoxを表示させる。

ClickEventCommand.vb
Public Class ClickEventCommand
    Implements ICommand

    Public Event CanExecuteChanged As EventHandler Implements ICommand.CanExecuteChanged

    Public Sub Execute(parameter As Object) Implements ICommand.Execute
        MessageBox.Show("OK")
    End Sub

    Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
        Return True
    End Function
End Class
Executeメソッドに処理すべき内容を記述、
CanExecuteメソッドは、Executeが実行可能かをBooleanで返す。今回は実行したいので何も考えずにTrueを返している。

ViewModel.vb
ClickEventCommandのオブジェクトを返すプロパティを追加する。
Public Class ViewModel

    Public Property Model As New TestData()

    Private _ClickEvent As ClickEventCommand
    Public ReadOnly Property ClickEvent As ClickEventCommand
        Get
            If (_ClickEvent Is Nothing) Then
                _ClickEvent = New ClickEventCommand()
            End If
            Return _ClickEvent
        End Get
    End Property

End Class

MainWindow.xaml
ButtonにCommand属性を追加、属性値にはViewModelに追加したClickEventプロパティを指定。
ただ単純に指定しただけではダメなようで、"Binding DataContext.ClickEvent" と指定しなければならない。 また、RelativeSourceの指定も必要。理由は良く分からない。
<Button Content="..."  Command="{Binding DataContext.ClickEvent, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" />

プログラムを実行し、DataGridのボタンクリックで "OK" のメッセージが表示されることを確認する。



次回記事:VisualBasic+WPF+YAML #9:DataGridのイベント 2

-- 以上 --


VisualBasic, WPF, DataGrid, YAML, MVVM

■ 用語:MVVM

Model View ViewModel - Wikipedia


MVVM
Model View ViewModel

MVC(Model-View-Controller)の派生パターン

Model
ドメインを担う(データのこと?)

View
ユーザーインターフェースのこと
複雑なロジックと状態は持たないこと。
ViewはViewModelのデータをバインドという仕組みで描画、または入力したデータをViewModelに通知する。

ViewModel
Viewを描画するための状態の保持とViewから受け取った入力を適切な形に変換したModelに伝達する

-- 以上 --


用語, MVVM, WPF

■ VisualBasic+WPF+YAML #7:DataGridへの表示 5

VBからYMALファイルを読み込みたい。
前回記事:VisualBasic+WPF+YAML #6:DataGridへの表示 4



MVVMデザインパターン

WPFでは、MVVMデザインパターンで作る必要がある。
用語:MVVM

正直、MVVMの意味良く分からないが、とりあえず前回までに作ったプログラムに ViewMode を追加、
View(MainWindow)からはViewModelにバインドするように修正する。

まずはViewModelの作成から。
ViewModel.vb:
Public Class ViewModel

    Public Property Model As New TestData()

End Class
MainWindow.xaml.vbのTestDataはViewModelを使用するように修正。(変数名も_TestDataから_ViewModelへ変更した)
Class MainWindow

    Private _ViewModel As New ViewModel()

    Private Sub Window_Initialized(sender As Object, e As EventArgs)

        _ViewModel.Model.Load()

        Me.DataContext = _ViewModel

    End Sub

End Class
※ LoadメソッドはModelを経由して実行しているが、本来であれば_ViewModeにLoadメソッドを実装した方が良いかも

MainWindow.xaml DataGridのItemsSourceを items から Model.items へ変更
    <DataGrid ItemsSource="{Binding Model.items}" AutoGenerateColumns="False" CanUserAddRows="False" HeadersVisibility="Column">
実行結果:

修正前と同じ実行結果であることを確認。



次回記事:VisualBasic+WPF+YAML #8:DataGridのイベント

-- 以上 --


VisualBasic, WPF, DataGrid, YAML, MVVM

その他の記事