VB.NETでもリフレクションとT4を使ってINotifyPropertyChangedを自動実装したい

はじめに

INotifyPropertyChangedってあるじゃないですか。あのひたすら実装が面倒なアレです。

今回はそのクッソ面倒なINotifyPropertyChanged*1の実装をリフレクションとT4を使って自動実装した時の記事です。

先人たちが100000000回くらい辿った内容なのでもしかしたらネタが被ってるかもしれませんが、被ってたらごめんなさい。

実際ILレベルでコードを注入する実装があるので二番煎じ感が半端ないし、実際に既に誰かがやってそうな内容ではありますが、VB × リフレクション × T4というかなりニッチな領域ってことで許してください。

T4 テキストテンプレート

T4テキストテンプレートの書き方や、定義をアセンブリにまとめてリフレクションで読み取る元ネタはこちらです。

qiita.com

有益な情報を公開してくださって感謝です。

これを見て、弊社としてはプレーンにプロパティを定義しているクラス(のアセンブリ)からINotifyPropertyChangedの実装に必要なコードをT4とリフレクションを使って補完できないかなと思ったのがきっかけです。

つまり、

Public Class Person

    Public Property Id As Integer
    Public Property FirstName As String
    Public Property LastName As String
    Public Property Address As String

End Class

から

Imports System.ComponentModel

Public Class PersonViewModel
    Implements INotifyPropertyChanged
    
    Private _id As System.Int32
    Public Property Id
        Get
            Return _id
        End Get
        Set
            If Value <> _id Then
                _id = Value
                NotifyPropertyChanged("Id")
            End If
        End Set
    End Property
    
    Private _firstName As System.String
    Public Property FirstName
        Get
            Return _firstName
        End Get
        Set
            If Value <> _firstName Then
                _firstName = Value
                NotifyPropertyChanged("FirstName")
            End If
        End Set
    End Property
    
    Private _lastName As System.String
    Public Property LastName
        Get
            Return _lastName
        End Get
        Set
            If Value <> _lastName Then
                _lastName = Value
                NotifyPropertyChanged("LastName")
            End If
        End Set
    End Property
    
    Private _address As System.String
    Public Property Address
        Get
            Return _address
        End Get
        Set
            If Value <> _address Then
                _address = Value
                NotifyPropertyChanged("Address")
            End If
        End Set
    End Property
    

#Region "INotifyPropertyChanged Support"

    Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged
    
    Private Sub NotifyPropertyChanged(ByVal info As String)
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(info))
    End Sub

#End Region

End Class

を生成できないかな〜と考えたわけです。

大まかな流れとしては、

  1. エンティティの定義となるクラスをアセンブリヘビルドする
  2. 1のアセンブリからリフレクションを用いてクラスのプロパティ定義をぶっこぬく
  3. プロパティ情報からプロパティの生成に必要な情報を生成する
  4. 3の情報をテンプレートに突っ込んでINotifyPropertyChangedを実装した新しいクラスを作成する
  5. RaiseEventし放題
  6. たのしい ✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌

アセンブリへビルドして、それを参照する方法とかは完全にリンク先の元ネタそのままなのでここでは割愛します。

2及び3をT4内で完結させようとすると死ぬので、取り合えずヘルパクラスを定義します。

Public Class GeneratorHelper

    Public Shared Function GetProperties(targetType As Type) As IEnumerable(Of PropertyGeneratorInfo)
        Return targetType.GetProperties(BindingFlags.Public Or BindingFlags.Instance).
            Where(Function(it) it.CanRead AndAlso it.CanWrite).
            Where(Function(it) it.GetIndexParameters().Length = 0).
            Select(Function(it) New PropertyGeneratorInfo(it.Name, GetBackingFieldName(it.Name), it.PropertyType.ToString))
    End Function

    Private Shared Function GetBackingFieldName(propertyName As String) As String
        Return "_" + propertyName.Substring(0, 1).ToLower + propertyName.Substring(1, propertyName.Length - 1)
    End Function

End Class

Public Class PropertyGeneratorInfo

    Public ReadOnly Property PropertyName As String
    Public ReadOnly Property BackingFieldName As String
    Public ReadOnly Property TypeName As String

    Friend Sub New(propertyName As String, backingFieldName As String, TypeName As String)
        Me.PropertyName = propertyName
        Me.BackingFieldName = backingFieldName
        Me.TypeName = TypeName
    End Sub

End Class

ヘルパクラス内では、プロパティ名とプロパティの型の抽出とバッキングフィールドの名前の生成を行ってます。

このヘルパクラスを定義クラスと一緒にアセンブリにぶち込んでおきます。

次に、アセンブリを参照に加えつつINotifyPropertyChangedを生成するテンプレートをポンッと定義します。 アドオンを入れてない素のVisual StudioではT4の編集が一番つらかったです。

<#@ template debug="false" hostspecific="false" language="VB" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ assembly name="$(ProjectDir)..\T4.UI\ReferenceAssembly\T4.GenerateDefine.dll" #>
<#@ import namespace="T4.GenerateDefine" #>
<#@ output extension=".generated.vb" #>
Imports System.ComponentModel

Public Class PersonViewModel
<# PushIndent("    ") #>
Implements INotifyPropertyChanged

<# PopIndent() #>
<# PushIndent("    ") #>
<# For Each it As PropertyGeneratorInfo In GeneratorHelper.GetProperties(GetType(Person)) #>
Private <#= it.BackingFieldName #> As <#= it.TypeName #>
Public Property <#= it.PropertyName #>
<# PushIndent("    ") #>
Get
<# PushIndent("    ") #>
Return <#= it.BackingFieldName #>
<# PopIndent() #>
End Get
Set
<# PushIndent("    ") #>
If Value <> <#= it.BackingFieldName #> Then
<# PushIndent("    ") #>
<#= it.BackingFieldName #> = Value
NotifyPropertyChanged("<#= it.PropertyName #>")
<# PopIndent() #>
End If
<# PopIndent() #>
End Set
<# PopIndent() #>
End Property

<# Next #>
<# PopIndent() #>

#Region "INotifyPropertyChanged Support"

<# PushIndent("    ") #>
Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

Private Sub NotifyPropertyChanged(ByVal info As String)
<# PushIndent("    ") #>
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(info))
<# PopIndent() #>
End Sub
<# PopIndent() #>

#End Region
<# PushIndent("    ") #>
<# PopIndent() #>

End Class

あとは、カスタムツールで変換してあげれば目的のINotifyPropertyChangedが実装されたクラスの出来上がりとなります。やったね。

おわりに

ここまで書いておいてアレですが、この方法は使いませんでした。 さすがにいろんなところが素朴すぎて、実用にならないと判断したためです。

じゃあ、どうやったって? 『エディタマクロ』『繰り返し』『コピペ』う、頭が・・・。

次回からは素直に既存品を使います。ハイ

余談ですが、T4内のVBは変数の型推論が使えなかったと思います。 VBのバージョンではいくつ位に相当するんでしょうかね。 調べる予定もありませんが。

おわり

*1:INotifyPropertyChangedって書くだけで面倒