VB のたまご

作成日: 2017/03/20, 更新日: 2017/03/20



変更通知機能を持ったプロパティ

    イメージ
  •  ここでは、変更通知プロパティについて学習していきます。どこの部分かと言うと ViewModel → View へプロパティ値変更の連絡をする部分です。 Model → ViewModel の場合もですね。

  • スポンサーリンク


  •  以前コードビハインド編では、「依存関係プロパティ」という Xaml ( View ) 用に拡張したプロパティを紹介しました。 今回説明する「変更通知プロパティ」は、ViewModel や Model 用に拡張したプロパティになります。 それぞれ役目が違いますので、「変更通知プロパティ」は「依存関係プロパティ」とは別物のプロパティになります。

  •  変更を通知するプロパティは、言葉通りなら、プロパティにセットされた値が変更された場合(現在値と新しい値が違っていた場合)、通知する機能を持っているということになります。 「変更されたかどうか」の判断は、セッターの処理時にできそうです。

  • Public Class Class1
    
        Private _MyName As String = String.Empty
        Public Property MyName As String
            Get
                Return _MyName
            End Get
            Set(value As String)
    
                ' 変更されたかどうかを判定
                If _MyName = value Then
                    Return
                End If
    
                _MyName = value
                ' ???
    
            End Set
        End Property
    
    End Class
    

  •  この後の「通知する」は、どうやって「通知」するのでしょうか。答えは「イベント発生させる」です。 イベントと言ってもイベントにはたくさんの種類がありますが、プロパティ値が変更されたときに発生させる妥当なイベントは、 「INotifyPropertyChanged」インターフェースが提供している「PropertyChanged」イベントです。 プロパティ値が変更された時に発生するイベントになります。

  • Namespace System.ComponentModel
        '
        ' 概要:
        '     プロパティ値が変更されたことをクライアントに通知します。
        Public Interface INotifyPropertyChanged
            '
            ' 概要:
            '     プロパティ値が変更されたときに発生します。
            Event PropertyChanged As PropertyChangedEventHandler
        End Interface
    End Namespace
    

  •  それでは、「INotifyPropertyChanged」インターフェースを継承したデータクラスを作ってみます。

  • Imports System.ComponentModel
    
    Public Class Class2
        Implements INotifyPropertyChanged
    
        Private _MyName As String = String.Empty
        Public Property MyName As String
            Get
                Return _MyName
            End Get
            Set(value As String)
    
                ' 変更されたかどうかを判定
                If _MyName = value Then
                    Return
                End If
    
                _MyName = value
                ' イベント発生
                RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("MyName"))
    
            End Set
        End Property
    
        Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    
    End Class
    

  •  「Implements INotifyPropertyChanged」と書いたタイミングで、PropertyChanged イベントの定義が自動追加されたと思います。 PropertyChanged イベントは、第一引数にイベント発生させたクラスのインスタンス、第二引数に変更されたプロパティ名を登録した PropertyChangedEventArgs クラスのインスタンスをセットして使います。

  •  それでは PropertyChanged イベントを体験するため、このクラスを使う側、使われる側でやり取りしてみましょう。 以下のサンプルはコンソールアプリケーションで作成したものです。

  • Imports System.ComponentModel
    
    Module Module1
    
        Sub Main()
    
            Dim class2Instance = New Class2
            AddHandler class2Instance.PropertyChanged, AddressOf Class2_PropertyChanged
    
            Console.WriteLine(class2Instance.MyName)
    
            Console.WriteLine($"変更します1")
            class2Instance.MyName = "taro"
            Console.WriteLine($"変更しました1")
    
            Console.WriteLine($"変更します2")
            class2Instance.MyName = "jiro"
            Console.WriteLine($"変更しました2")
    
            Console.WriteLine(class2Instance.MyName)
    
            Console.Read()
        End Sub
    
        Private Sub Class2_PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
    
            Console.WriteLine($"  PropertyChanged イベントが変更を検知しました")
            Console.WriteLine($"  {sender.ToString()}")
            Console.WriteLine($"  {e.PropertyName}")
    
        End Sub
    
    End Module
    

    出力結果
    taro
    変更します1
    変更しました1
    変更します2
      PropertyChanged イベントが変更を検知しました
      w03_DataBinding2.Class2
      MyName
    変更しました2
    jiro
    

  •  なんとなく、PropertyChanged イベントの特徴が分かったでしょうか。これが「変更」を「通知」する機能を持った「プロパティ」の正体です。 ところでこれ、データクラスを作るたびにいちいちインターフェースを継承してやらないといけないのでしょうか。面倒くさいですね。

  •  通常は、変更通知プロパティのベースクラスを用意して、この中にヘルパーメソッドとして閉じ込めてしまいます。 Livet では、NotificationObject クラスが受け持っているので同じ名前で作成します。

  • Imports System.ComponentModel
    Imports System.Runtime.CompilerServices
    
    Public Class NotificationObject
        Implements INotifyPropertyChanged
    
        Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    
        ' System.Runtime.CompilerServices 名前空間の CallerMemberName 属性を付けると、
        ' 呼び出し元のプロパティ名が自動的にセットされる(ので、呼び出し元ではいちいち文字列指定しなくてもセットしてくれるので楽)
        Protected Sub RaisePropertyChanged(<CallerMemberName> Optional propertyName As String = "")
    
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
    
        End Sub
    
    End Class
    

    Public Class Class3
        Inherits NotificationObject
    
        Private _MyName As String = "taro"
        Public Property MyName As String
            Get
                Return _MyName
            End Get
            Set(value As String)
    
                ' 変更されたかどうかを判定
                If _MyName = value Then
                    Return
                End If
    
                _MyName = value
                ' イベント発生
                Me.RaisePropertyChanged()
    
            End Set
        End Property
    
    End Class
    

    Imports System.ComponentModel
    
    Module Module1
    
        Sub Main()
    
            Dim class3Instance = New Class3
            AddHandler class3Instance.PropertyChanged, AddressOf Class3_PropertyChanged
    
            Console.WriteLine(class3Instance.MyName)
    
            Console.WriteLine($"変更します1")
            class3Instance.MyName = "taro"
            Console.WriteLine($"変更しました1")
    
            Console.WriteLine($"変更します2")
            class3Instance.MyName = "jiro"
            Console.WriteLine($"変更しました2")
    
            Console.WriteLine(class3Instance.MyName)
    
            Console.Read()
        End Sub
    
        Private Sub Class3_PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
    
            Console.WriteLine($"  PropertyChanged イベントが変更を検知しました")
            Console.WriteLine($"  {sender.ToString()}")
            Console.WriteLine($"  {e.PropertyName}")
    
        End Sub
    
    End Module
    

    出力結果
    taro
    変更します1
    変更しました1
    変更します2
      PropertyChanged イベントが変更を検知しました
      w03_DataBinding2.Class3
      MyName
    変更しました2
    jiro
    

  •  先程と同じですね。1つのプロパティを準備するのに結構書くことが多いですが、今後は大量に書くことになりますので、今のうちに慣れておくといいかもしれません。 さて、単一データに対する変更通知機能があるということは、複数データ(コレクションデータ)に対する変更通知機能もあるということです。

  • スポンサーリンク


  •  コレクションデータ版は「INotifyCollectionChanged」インターフェースが提供している「CollectionChanged」イベントです。そのままですね!

  • Imports System.Runtime.CompilerServices
    
    Namespace System.Collections.Specialized
        '
        ' 概要:
        '     項目が追加、削除されたときやリスト全体が更新されたときなど、動的な変更をリスナーに通知します。
        <TypeForwardedFrom("WindowsBase, Version=3.0.0.0, Culture=Neutral, PublicKeyToken=31bf3856ad364e35")>
        Public Interface INotifyCollectionChanged
            '
            ' 概要:
            '     コレクションが変更された場合に発生します。
            Event CollectionChanged As NotifyCollectionChangedEventHandler
        End Interface
    End Namespace
    

  •  しかも、.net 標準のクラスに、このインターフェースを継承済みで後は使うだけの状態のクラスが提供されています。 それが「ObservableCollection」クラスです。

  • イメージ
  •  実装していく中で、自前で「INotifyCollectionChanged」インターフェースを継承して利用するような場面は少ないと思いますので、ここでは ObservableCollection を学習します。

  • Imports System.Collections.ObjectModel ' ObservableCollection
    Imports System.Collections.Specialized ' NotifyCollectionChangedEventArgs
    
    Module Module1
    
        Sub main()
    
            Dim obj = New ObservableCollection(Of String)
            AddHandler obj.CollectionChanged, AddressOf ObservableCollection_CollectionChanged
    
            ' データの追加
            Console.WriteLine("データを追加します1")
            obj.Add("aaa")
            Console.WriteLine("データを追加しました1")
    
            Console.WriteLine("データを追加します2")
            obj.Add("bbb")
            Console.WriteLine("データを追加しました2")
    
            ' データの変更
            Console.WriteLine("データを変更します")
            obj.Item(0) = "aaa-1"
            Console.WriteLine("データを変更しました")
    
            ' データの削除
            Console.WriteLine("データを削除します")
            obj.RemoveAt(1)
            Console.WriteLine("データを削除しました")
    
            ' データの削除、その2
            ' イベント発生はするが、e.OldItems が Nothing になるので、何らかの個別の解放処理を考えている場合は注意
            Console.WriteLine("全データをクリアします")
            obj.Clear()
            Console.WriteLine("全データをクリアしました")
    
            ' データの登録
            ' コンストラクタではイベント発生しない
            Console.WriteLine("初期データを登録します")
            Dim items = Enumerable.Range(1, 3).Select(Function(i) "item-" & i.ToString())
            obj = New ObservableCollection(Of String)(items)
            Console.WriteLine("初期データを登録しました")
    
            Console.Read()
        End Sub
    
        Private Sub ObservableCollection_CollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs)
    
            Console.WriteLine($"  CollectionChanged イベントが変更を検知しました")
    
            If e.OldItems IsNot Nothing AndAlso 0 < e.OldItems.Count Then
                For Each item In e.OldItems
                    Console.WriteLine($"  {item} is replace or removed.")
                Next
            End If
    
            If e.NewItems IsNot Nothing AndAlso 0 < e.NewItems.Count Then
                For Each item In e.NewItems
                    Console.WriteLine($"  {item} is replace or added.")
                Next
            End If
    
        End Sub
    
    End Module
    

    出力結果
    データを追加します1
      CollectionChanged イベントが変更を検知しました
      aaa is replace or added.
    データを追加しました1
    データを追加します2
      CollectionChanged イベントが変更を検知しました
      bbb is replace or added.
    データを追加しました2
    データを変更します
      CollectionChanged イベントが変更を検知しました
      aaa is replace or removed.
      aaa-1 is replace or added.
    データを変更しました
    データを削除します
      CollectionChanged イベントが変更を検知しました
      bbb is replace or removed.
    データを削除しました
    全データをクリアします
      CollectionChanged イベントが変更を検知しました
    全データをクリアしました
    初期データを登録します
    初期データを登録しました
    

  •  CollectionChanged イベント内で取得できる NotifyCollectionChangedEventArgs クラスの扱い方ですが、 データ追加・変更した場合、NewItems という IList 型に該当データが入ってきます。追加データ、または変更後データです。 逆に、データ削除・変更した場合(変更前の値)、OldItems という IList 型に該当データが入ってきます。削除データ、または変更前データです。

  •  コード上ではイベントをハンドルして扱いましたが、Xaml にバインドしたデータが通知すると、Xaml 側が応対して変更後の値に表示更新される仕組みとなります。 最後にそれを見てみましょう。以下は標準の WPF アプリケーションのサンプルです。

  • Imports System.ComponentModel
    
    Public Class ObservableObject
        Implements INotifyPropertyChanged
    
        Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    
        Private _MyName As String = String.Empty
        Public Property MyName As String
            Get
                Return _MyName
            End Get
            Set(value As String)
    
                If _MyName = value Then
                    Return
                End If
    
                _MyName = value
                RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs("MyName"))
    
            End Set
        End Property
    
    End Class
    

    Imports System.ComponentModel
    Imports System.Collections.ObjectModel
    
    Public Class Model1
    
        Public Property Person As ObservableObject
        Public Property Persons As ObservableCollection(Of String)
    
        Public Sub New()
    
            Me.Person = New ObservableObject
            Me.Persons = New ObservableCollection(Of String)
    
        End Sub
    
    End Class
    

    <Window x:Class="MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:w04_DataBinding3"
            mc:Ignorable="d"
            Title="MainWindow" Height="350" Width="525">
    
        <Window.DataContext>
            <local:Model1 />
        </Window.DataContext>
        
        <StackPanel>
    
            <Button Content="新しいデータ" Click="Button_Click" />
            <TextBlock Text="{Binding Person.MyName}" />
            <ListBox ItemsSource="{Binding Persons}" />
    
        </StackPanel>
        
    </Window>
    

    Class MainWindow
    
        Private Sub Button_Click(sender As Object, e As RoutedEventArgs)
    
            ' バインドデータを取得
            Dim bindData = TryCast(Me.DataContext, Model1)
            If bindData Is Nothing Then
                Return
            End If
    
            bindData.Person.MyName = "item-" & Guid.NewGuid().ToString()
            bindData.Persons.Add(bindData.Person.MyName)
    
        End Sub
    
    End Class
    

  •  実行後、ボタンを繰り返し押してください。変更処理が反映されて画面に表示更新されるのが分かると思います。