1. The problem
The INotifyPropertyChanged
interface is probably one of the most famous interfaces in the interface oriented programing history. It has been used extensively almost everywhere, especially UI
applications.
Because it’s so popular, developers have to spend time to keep writing similar codes for properties. A typical ViewModel
needs to contain at least below codes or equivalent logic, even when it has only one property, and as we can see that the property setter is the most tedious piece among all these codes.
Developers can use
[CallerMemberName]
to save the explicit passing of the property name, but it only works in.Net 4.0
and later versions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
NotifyPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
So various ways have been created to simplify the process of writing a ViewModel
:
- Put the common codes into a base class such that the child classes can focus on property
Getter
andSetter
. But it does not work for the case that base class cannot be changed. - Extract
Property Setter
into some generic method likeSetValue
and useEqualityComparer<T>.Equals()
to do the value difference check. This can help simplify the property setter code to one line. - Dynamically generate codes.
Since the pattern to implement the INotifyPropertyChanged
interface and raise event in the property setter is pretty common, so if things come true obviously auto-generated codes can simplify the efforts of writing classes like ViewModel
.
2. InterceptionBehavior
The Interception
in Unity Container
is one of the ways to simplify the codes. One of the Unity
series articles on Microsoft docs Implement INotifyPropertyChanged Example provides a demo implementation. Assume ViewModel
and its interface are like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface IViewModel
{
string Name { get; set; }
int Age { get; set; }
}
public class ViewModel : IViewModel
{
public string Name { get; set; }
public int Age { get; set; }
}
We can use InterceptionBehavior
to intercept the calls to property setters and add/remove to the PropertyChanged event to implicitly implement the INotifyPropertyChanged
interface.
1
2
3
4
5
6
var container = new UnityContainer();
container.AddNewExtension<Interception>();
var interceptor = new Interceptor<InterfaceInterceptor>();
var behavior = new InterceptionBehavior<NotifyPropertyChangedBehavior>();
container.RegisterType<IViewModel, ViewModel>(interceptor, behavior);
It works, but has problems:
Unity Container
actually creates a new class dynamically to implement the same interface and wrap the original be intercepted class, so theIViewModel
interface is required, otherwiseUnity Container
would throw exceptions of “Type is not interceptable
”.- Because event is a special delegate that can only be called from inside of a class,
NotifyPropertyChangedBehavior
actually provides its ownPropertyChanged
event to support add/remove of the event listeners. So listen to the property changes only works from outside of the class, event listeners attached from inside the class won’t work because the event is not used at all.
3. Reflection.Emit
The ultimate solution is to dynamically generate codes. One way is to generate codes at run time via Reflection.Emit
, another way is to run some MSBuild post process tasks to generate codes based configurations like attribute and edit the assemblies. AOP
libraries like PostSharp work in this way. In the end these two approaches are both manipulating IL, but we’ll only talk about the first approach in this blog. For the InterceptionBehavior
above, Unity Container
internally also creates a Proxy
class via Reflection.Emit
, just that its purpose is to expose something to allow developers put custom logic in the behaviors.
PostSharp
also provides support to implementINotifyPropertyChanged
, butPostSharp
is not free and its approach is similar to what we’re going to talk about.
So write our own codes is the best way to properly generate implementations for the INotifyPropertyChanged
interface.
- No need to define interface for the source class.
- Allow attaching
PropertyChanged
event listeners from inside and outside of the class.
This article Dynamically generating types to implement INotifyPropertyChanged gives a relatively complete implementation. It creates a dynamic class inheriting from the existing class (so no interface is needed), the existing class just need to mark the properties as virtual
so they can be overridden.
I have created a project to integrate the codes with Unity Container
and put on github. As long as a class implement the IAutoNotifyPropertyChanged
interface (any other way is also fine), when being resolved by the Container, a dynamic class would be created with INotifyPropertyChanged
interface implemented.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class NotifyPropertyChangedImplStrategy : BuilderStrategy
{
public override void PreBuildUp(ref BuilderContext context)
{
base.PreBuildUp(ref context);
if (context.Type.IsSubclassOf<IAutoNotifyPropertyChanged>())
{
var implType = NotifyPropertyChangedImpl.GetType(context.Type);
context.Type = implType;
}
}
}
4. There’re still problems
Although the Reflection.Emit
approach can save us repeated works, but there’re still rooms to improve:
First of all, it’s not debug friendly. Since the property setter is emitted IL
codes, so there’re no source codes to set break points in the setter, there’re workarounds but not convenient.
Second, the INotifyPropertyChanged
interface is a very high level abstraction, so it might cause problems if not used properly:
- Performance impact. Say if we’re only interested in changes of several properties, we need to match the property name in the event handler. If the object has many properties then the event handler would be called many times because of irrelevant property changes, it can cause performance problems in extreme cases.
- Inconvenience. There’re cases we want to know the previous and current value of the property, one common way is letting the object implement the
INotifyPropertyChanging
interface, then we subscribe to it and store previous value of the properties. It works but apparently not quality coding.
There should be better solutions to fix these problems.
1. 问题
如果说面向接口编程的历史上接口的著名程度有一个排名,INotifyPropertyChanged
排前三应该没有太大争议。INotifyPropertyChanged
高度抽象化了对象的“属性发生改变”这一行为,在应用程序尤其是包含UI
的应用程序中可以说无处不在。
然而正因为其无处不在,开发人员不得不耗费大量时间来编写重复的代码。就算是只包含一个属性的ViewModel
也至少需要包含下面的代码或等价的逻辑,其中以Property Setter
的编写尤其繁琐。
在
.Net 4.0
以后的版本中已经可以通过[CallerMemberName]
来省去对属性名的重复。在早期版本中每个调用NotifyPropertyChanged
的地方还需要显式传递属性名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ViewModel : INotifyPropertyChanged
{
private string _name;
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
NotifyPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
聪明的开发人员想出了各种办法来简化ViewModel
的编写过程:
- 把基本的实现放到一个基类中,子类只需要实现属性的
Getter
和Setter
。但不能解决基类不能更改的情况。 - 把
Property Setter
的代码提取到一个泛型方法比如SetValue
中,用EqualityComparer<T>.Equals()
来代替值的比较,这种方法可以把Setter
简化到一行代码。 - 自动生成代码。
由于INotifyPropertyChanged
接口的实现以及触发属性变化的过程对每个类来说都是类似的逻辑,自动生成代码如果能够实现显然会带来简化的效果。
2. InterceptionBehavior
比较简单的一种方法是使用Unity Container
中的Interception
。Microsoft docs 上Unity
系列文章里有一篇Implement INotifyPropertyChanged Example给出了一种实现。假设ViewModel
及其接口定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface IViewModel
{
string Name { get; set; }
int Age { get; set; }
}
public class ViewModel : IViewModel
{
public string Name { get; set; }
public int Age { get; set; }
}
可以通过InterceptionBehavior
来截获对属性的Setter
以及添加删除PropertyChanged
事件订阅的调用来隐式实现INotifyPropertyChanged
接口,具体实现可以参考这里。
1
2
3
4
5
6
var container = new UnityContainer();
container.AddNewExtension<Interception>();
var interceptor = new Interceptor<InterfaceInterceptor>();
var behavior = new InterceptionBehavior<NotifyPropertyChangedBehavior>();
container.RegisterType<IViewModel, ViewModel>(interceptor, behavior);
这种方法基本可行,但存在不小的缺陷:
Unity Container
实际上创建了一个实现相同接口的类来封装现有类,所以接口IViewModel
是必须的,否则会抛出”Type is not interceptable
“的异常。- 由于事件是只能在一个类的内部调用的特殊委托,
NotifyPropertyChangedBehavior
实际上自己定义了一个PropertyChanged
事件来替代被截获类的PropertyChanged
事件,因此监听属性改变只对外部代码有效,任何在类的内部监听属性改变的尝试都是徒劳的,因为被监听类自己的PropertyChanged
事件根本没有被用到。
3. Reflection.Emit
终极的解决方案是动态生成代码。一种方法是在运行时通过Reflection.Emit
动态生成代码,另一种是在MSBuild
的post process
阶段根据Attribute
之类的配置生成代码并修改Assembly
。PostSharp之类AOP
库使用的就是第二种方法。两种方法归根到底都是操作IL
代码,本文只讨论在运行时生成代码的方式。实际上在上述Interception
方法里Unity Container
也使用Reflection.Emit
动态生成了一个Proxy
类,只不过其目的是为了允许开发人员添加自定义代码。
PostSharp
对实现INotifyPropertyChanged
接口也提供了支持,不过PostSharp
并非免费,其实现也与我们将要提到的方法大同小异。
为了完美地自动实现INotifyPropertyChanged
接口,最好的办法是自己生成代码:
- 无需定义接口。
- 可在类的外部和内部监听
PropertyChanged
事件。
这篇文章Dynamically generating types to implement INotifyPropertyChanged就给出了一个相对完整并可用的实现——动态生成一个类来继承现有类,所以无需接口,唯一的要求是把属性标记为virtual
以便动态生成代码来重载Property Setter
。
我把整理了代码并与Unity Container
简单整合了一下放到了github——通过一个IAutoNotifyPropertyChanged
接口(也可以用别的方法)来标记一个类,一旦检测到该接口的存在就创建动态一个类来实现INotifyPropertyChanged
接口并重载所有属性的Setter
。
1
2
3
4
5
6
7
8
9
10
11
12
13
public class NotifyPropertyChangedImplStrategy : BuilderStrategy
{
public override void PreBuildUp(ref BuilderContext context)
{
base.PreBuildUp(ref context);
if (context.Type.IsSubclassOf<IAutoNotifyPropertyChanged>())
{
var implType = NotifyPropertyChangedImpl.GetType(context.Type);
context.Type = implType;
}
}
}
4. 还有问题
虽然动态生成代码的方法帮助我们节省了不少重复劳动,但还有改进的空间:
首先代码调试不方便。动态生成的类Emit
了IL
代码来重载属性的Setter
,所以没有源代码也无法直接添加断点进行调试,只能通过添加事件处理函数的方式来绕道而行。
其次,前面说过INotifyPropertyChanged
接口高度抽象了属性发生改变的行为,这一高度抽象一定程度上也带来了两个问题:
- 性能损失——假如我们只希望监听某几个属性的改变,只能在
PropertyChanged
事件处理函数里检查属性名是否匹配。如果被监听的对象属性众多那么事件处理函数会因为无关属性的改变被不必要地调用很多次,极端情况会带来性能问题。 - 缺乏便利性——有时候我们需要在事件触发时知道属性的当前值和之前的值。原生的做法是让对象实现另一个
INotifyPropertyChanging
接口,然后监听其PropertyChanging
事件来记录属性之前的值。这可行但不是好的代码。
因此我们需要一套更好的实现来改进这几个缺陷……
-
Previous
Unity Container (1) Injecting type aware ILogger -
Next
Unity Container (3) Property Container