かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

INotifyPropertyChangedの実装を簡易化するNotifyPropertyWeaverを試してみた

先日ぼそっとTwitterに呟こうと思ったことをBlogに書いた程度のエントリーのINotifyPropertyChangedとかを簡単に実装できるようにしてほしいよねですがコメントいただきました。PostSharpとかUnity使ったAOPでの実装とかは、前にも見たことあったのですが、今回、NotifyPropertyWeaverという知らない名前のものがあったので試してみました。

インストール

インストールといってもソリューションフォルダに適当なフォルダを置いてNotifyPropertyWeaverMsBuildTask.dllをコピーするだけで完了。ここではLibsというフォルダを作って、その中にコピーしました。

プロジェクトファイルの書き換え

使用するアセンブリ名からもわかるように、こいつはMSBuildのタスクなのでプロジェクトファイルを手動で書き換えます。MSBuildの書き換えをVisual Studio 2010上でスムーズにやるには、プロジェクトを右クリックしてプロジェクトのアンロードを選択してプロジェクトを開かれていない状態にします。

こうすると、右クリックメニューからプロジェクトファイルが編集できるようになります。

そして、Projectタグの下にNotifyPropertyWeaverのタスクが実行されるような記述を追記します。

<Project ... >
  ......
  <!-- タスクの読み込み -->
  <UsingTask 
    TaskName="NotifyPropertyWeaverMsBuildTask.WeavingTask" 
    AssemblyFile="$(SolutionDir)Libs\NotifyPropertyWeaverMsBuildTask.dll" />
  <Target Name="AfterBuild">
    <!-- INotifyPropertyChangedを実装している全ての型に対して
         RaisePropertyChangedの呼び出しを追加するように試みる。
         EventInvokerNameを指定しなかったらデフォでOnPropertyChangedになる -->
    <NotifyPropertyWeaverMsBuildTask.WeavingTask 
      TargetPath="$(TargetPath)"
      TryToWeaveAllTypes="true"
      EventInvokerName="RaisePropertyChanged" />
  </Target>
</Project>

プログラム書いてみる

これで準備完了なので、簡単にプログラムを書いてみましょう。

namespace ConsoleApplication21
{
    using System;
    using System.ComponentModel;

    // INotifyPropertyChangedの単純実装
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void RaisePropertyChanged(string name)
        {
            var h = this.PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs(name));
            }
        }
    }

    // 自動プロパティを定義するだけ
    public class Person : ViewModelBase
    {
        public int Age { get; set; }

        public string Name { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var p = new Person();
            // 変更があったプロパティ名を出力するように仕込んで
            p.PropertyChanged += (s, e) =>
                {
                    Console.WriteLine(e.PropertyName);
                };

            // 適当に値を代入してみる
            p.Age = 10;
            p.Age = 100;
            p.Name = "たろ";
            p.Name = "たろ";
        }
    }
}

何のひねりもない単純なプログラムです。PropertyChangedが呼ばれるのはAgeに10と100を入れたときと、Nameに"たろ"を最初に代入したときの3回のはずです。2回目の"たろ"の代入は同じ値の代入なのでPropertyChangedは呼ばれないはずです。

ということで、本当にこれでPropertyChangedイベントが期待通り発行されるのか実行してみます。実行すると・・・。

ちゃんと実行してるみたいです。

その他の機能

一応、今回使ったMSBuildのタスク以外にもNotifyPropertyWeaver.dllというアセンブリも提供されていて、こいつを使うと関連するプロパティの変更イベントも同時に発行出来たり、このプロパティに対してはPropertyChangedイベント発行しないようにとか細かい指定が出来るみたいです。
今回は、全クラスを自動でPropertyChanged生成の対象にしましたが、NotifyProperty属性で指定したものだけ生成させることも出来ます。

仕組みは・・・?

ちょっとしかコードを見てませんが、Mono.Cecilを使ってたりするので、コンパイル後のアセンブリを書き換えてるというとても黒魔術的なことをしているようです。
Mono.Cecilについては、こちらを見てください。

感想

PostSharpは、チームで使おうと思うと思うと全員にインストールが必要ですが、今回試したNotifyPropertyWeaverはソリューションに仕込んでしまえば、あとはチーム内で共有してしまえば使えてしまうお手軽さがある上に、ILを書き換えるのでAOPみたいな実行時のオーバーヘッドもないので素敵だと思います。
思いますが、黒魔術を使ってるのでコードの見た目と実際の動作が異なるというもろ刃の剣でもあります。使うとしたら[NotifyProperty]属性をつけたプロパティだけ対象にするみたいな使い方のほうがいいかもしれません。

ただ、実績がないとちょっと怖いっちゃぁ怖いですね。