VB.NETでもリフレクションとT4を使ってINotifyPropertyChangedを自動実装したい
はじめに
INotifyPropertyChanged
ってあるじゃないですか。あのひたすら実装が面倒なアレです。
今回はそのクッソ面倒なINotifyPropertyChanged
*1の実装をリフレクションとT4を使って自動実装した時の記事です。
先人たちが100000000回くらい辿った内容なのでもしかしたらネタが被ってるかもしれませんが、被ってたらごめんなさい。
実際ILレベルでコードを注入する実装があるので二番煎じ感が半端ないし、実際に既に誰かがやってそうな内容ではありますが、VB × リフレクション × T4というかなりニッチな領域ってことで許してください。
T4 テキストテンプレート
T4テキストテンプレートの書き方や、定義をアセンブリにまとめてリフレクションで読み取る元ネタはこちらです。
有益な情報を公開してくださって感謝です。
これを見て、弊社としてはプレーンにプロパティを定義しているクラス(のアセンブリ)から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のアセンブリからリフレクションを用いてクラスのプロパティ定義をぶっこぬく
- プロパティ情報からプロパティの生成に必要な情報を生成する
- 3の情報をテンプレートに突っ込んで
INotifyPropertyChanged
を実装した新しいクラスを作成する RaiseEvent
し放題- たのしい ✌('ω'✌ )三✌('ω')✌三( ✌'ω')✌
アセンブリへビルドして、それを参照する方法とかは完全にリンク先の元ネタそのままなのでここでは割愛します。
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って書くだけで面倒