Unity で INotifyPropertyChanged

データバインディングを利用したアプリケーション開発で、双方向バインディング(View→DataSourceおよびDataSource→Viewに変更を通知)を実現するためデータソースとなるオブジェクトにINotifyPropertyChanged を実装する方法がああります。そのための基本的な方法は各プロパティのセッターでPropertyChangedイベントを発生させるコードを記述することです。

1
2
3
4
5
6
7
8
9
10
11
12
13
int testProperty;

public int TestProperty
{
get { return testProperty; }
set
{
if (value != testProperty) {
testProperty = value;
OnPropertyChanged(new PropertyChangedEventArgs("TestProperty"));
}
}
}

この方法の問題点は

  • 記述量が多く、時間の掛かる単純作業となること
  • ドメインコード(アプリケーション固有コード)とインフラストラクチャコードが混在すること

です。単純作業はスニペット・T4テンプレート・コードジェネレーションなどの方法で回避できます。

ドメインの記述の自由度を保つためにはなるべくインフラストラクチャの制限を受けたくありません。POCOエンティティが見直されているのはそのためだと理解しています。ドメインコードとINotifyPropertyChangedを実現するコードを分離するため、MicrosoftのUnity依存性注入コンテナを使うことが考えられます(下記コード参照)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.InterceptionExtension;

namespace UnityAdditionalInterfaceTest
{
class NotifyPropertyChangedBehavior : IInterceptionBehavior
{
static readonly MethodInfo addPropertyChangedMethodInfo =
typeof(INotifyPropertyChanged).GetMethod("add_PropertyChanged");
static readonly MethodInfo removePropertyChangedMethodInfo =
typeof(INotifyPropertyChanged).GetMethod("remove_PropertyChanged");

public object Target { get; set; }

event PropertyChangedEventHandler PropertyChanged;

void RaisePropertyChanged(string name)
{
if (PropertyChanged != null) PropertyChanged(Target, new PropertyChangedEventArgs(name));
}

public IEnumerable<Type> GetRequiredInterfaces()
{
return Type.EmptyTypes;
}

public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
if (input.MethodBase == addPropertyChangedMethodInfo) {
PropertyChanged += (PropertyChangedEventHandler)input.Arguments[0];
return input.CreateMethodReturn(null);
} else if (input.MethodBase == removePropertyChangedMethodInfo) {
PropertyChanged -= (PropertyChangedEventHandler)input.Arguments[0];
return input.CreateMethodReturn(null);
} else if (input.MethodBase.Name.StartsWith("set_")) { //プロパティ設定
RaisePropertyChanged(input.MethodBase.Name.Substring(4));
}
return getNext()(input, getNext);
}

public bool WillExecute
{
get { return true; }
}
}

public class Person
{
public virtual string Name { get; set; }
public virtual int Age { get; set; }
}

public static class Program
{
static void AcceptNotifyPropertyChanged(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine("NotifyPropertyChangedOccured:" + e.PropertyName);
}

public static void Main()
{
var behavior = new NotifyPropertyChangedBehavior();
var target = Intercept.NewInstanceWithAdditionalInterfaces<Person>
(new VirtualMethodInterceptor(),
new IInterceptionBehavior[] { behavior },
new[] { typeof(INotifyPropertyChanged) }, new object[] { });
behavior.Target = target;
(target as INotifyPropertyChanged).PropertyChanged += AcceptNotifyPropertyChanged;
target.Name = "test";
target.Age = 1;
Console.ReadKey();
}
}
}

PersonはDataSourceに指定するエンティティ、NotifyPropertyChangedBehaviorクラスはINotifyPropertyChangedを実現するクラスです。Personは自動実装プロパティを使用していて単純な記述になっています。またセッターに割り込むためにvirtualを付けています。Intercept.NewInstanceWithAdditionalInterfaces<Person>を使ってインスタンスを作成すると、元のクラスがインタフェースを実装していないにもかかわらず、実装しているかのように扱うことができます。特定の基本クラスを必要としませんので、インフラストラクチャコードを気にしないで継承を使用できます。また表示用には直接Personのコンストラクタを呼び出し、編集用にはUnityを使ってインスタンスを作成するといった使い分けをすることもできます。

上記コードではエンティティ内でプロパティ変更時アクションを記述する場所がありませんので、外部から使用するのと同様にイベントハンドラを追加するか、特定の名前のメソッドを決めておきプロパティ変更時に呼び出すなどの工夫が必要になります。