Windows Formsで配列の要素に対してバインドする方法


はじめに

Windows Formsで配列の要素に対してバインドしたい時が結構あります。
同じコントロールを複数配置した場合も、配列で管理した方が楽な時があります。

前提

  • Window.Forms
  • VB 2017
  • .NET Framework 4.6.1

ソースコード

こんな画面をぱぱっと作ります。

ささっと以下のコード。

Form1.vb
Imports System.ComponentModel

Public Class Form1

    Private VM As New ViewModel

    Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        TextBox1.DataBindings.Add(New Binding("Text", VM.Inputs(0), "Value"))
        TextBox2.DataBindings.Add(New Binding("Text", VM.Inputs(1), "Value"))
        TextBox3.DataBindings.Add(New Binding("Text", VM.Inputs(2), "Value"))
    End Sub

    Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        VM.Update()
    End Sub

    Private Class ViewModel

        Public Inputs As t()

        Sub New()
            Inputs = {
                 New t With {.Value = "a"},
                 New t With {.Value = "b"},
                 New t With {.Value = "c"}
            }
        End Sub

        Sub Update()
            Inputs(0).Value = "1"
            Inputs(1).Value = "2"
            Inputs(2).Value = "3"
        End Sub

        Class t
            Implements INotifyPropertyChanged

            Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

            Private _Value As String
            Property Value As String
                Get
                    Return _Value
                End Get
                Set
                    _Value = Value
                    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(Value)))
                End Set
            End Property

        End Class

    End Class

End Class

試す

起動時

ボタン押下

ばっちり

なにがおきたか

配列の用意

ViewModelクラス内では、Inputs要素に対してクラスを指定します。そして、Inputs要素に対してバインドする為、Inputs自身はただのメンバでよいです。

バインド先であるViewModel内
    Private Class ViewModel

        Public Inputs As t()

        ' ~~中略~~

    End Class

配列の「要素」となるプロパティの用意

代わりに、Inputs要素に対して指定したクラスとそのプロパティに対しては、プロパティを用意し、INotifyPropertyChangedによるPropertyChangedイベントを発火(Raise)させるようにします。
はい、要素自身をINotifyPropertyChangedを継承したクラスにすることで、個々の要素に対してバインド可能なプロパティを設定することに成功しています

バインド先であるViewModel内
    Private Class ViewModel

        Public Inputs As t()

        ' ~~中略~~

        Class t
            Implements INotifyPropertyChanged

            Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

            Private _Value As String
            Property Value As String
                Get
                    Return _Value
                End Get
                Set
                    _Value = Value
                    RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(Value)))
                End Set
            End Property

        End Class

    End Class

配列とその要素の初期化

バインド先がnullだと普通にヌルポされちゃいます。ちゃんと配列とその要素を初期化しましょう。ついでに

配列の初期化
' ~~前略~~

    Private Class ViewModel

        Public Inputs As t()

        Sub New()
            Inputs = {
                 New t With {.Value = "a"},
                 New t With {.Value = "b"},
                 New t With {.Value = "c"}
            }
        End Sub

        Sub Update()
            Inputs(0).Value = "1"
            Inputs(1).Value = "2"
            Inputs(2).Value = "3"
        End Sub

        ' ~~中略~~

    End Class

バインドの設定

フォームロード時に、各テキストボックスに対して、ViewModelクラスのメンバにある配列Inputs要素と結び付けます。もちろん個々の要素に対してバインドするので、配列へのインデックスはきちんと決めてあげましょう。

フォームロード時に処理されるバインドの設定
' ~~前略~~

Public Class Form1

    Private VM As New ViewModel

    Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        TextBox1.DataBindings.Add(New Binding("Text", VM.Inputs(0), "Value"))
        TextBox2.DataBindings.Add(New Binding("Text", VM.Inputs(1), "Value"))
        TextBox3.DataBindings.Add(New Binding("Text", VM.Inputs(2), "Value"))
    End Sub

    ' ~~中略~~

End Class

この時、指定されたインデックスに当てはまる要素がない場合、エラーが起きます。
あと、変な設定すると正しくバインドされませんでした(3敗)

だめな例__バインドの設定
' ~~前略~~
Public Class Form1

    Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

        ' エラーになる:存在しない(ようにした)インデックスを指定
        TextBox1.DataBindings.Add(New Binding("Text", VM.Inputs(5000), "Value"))

        ' エラーになる:プロパティ「名」としてのInputs(2)は存在しない。特に「(2)」が存在しない
        TextBox3.DataBindings.Add(New Binding("Text", VM, "Inputs(2).Value"))

        ' エラーにはならないし、初回だけ値は設定されるけど、実はバインドされてない
        TextBox2.DataBindings.Add(New Binding("Text", VM.Inputs(1).Value, "")) 
    End Sub

    ' ~~中略~~

End Class