INotifyDependencyChanged:解开计算依赖关系






4.75/5 (3投票s)
管理 INotifyPropertyChanged 属性之间依赖关系的直观机制
引言
INotifyPropertyChanged
是一个很有用的接口。它提供了一种机制,允许你的类通知依赖于它们的类(尤其是 WPF 用户界面)它们已经发生变化,因此其他东西也需要相应地改变。实现起来非常容易理解和完成,而且与 DependencyProperty
实例不同,你不需要担心线程亲和性(至少在抛出异常方面)。
然而,它们有两个主要问题:
- 调用
PropertyChanged
时,你需要指定属性的名称。如果没有提前规划,很容易导致你的代码中充斥着大量的“魔术字符串”。 - 计算属性从方便地提供一个始终有效的值(在语义上是属性,即使它从未存储在字段中)变成了一个十足的维护噩梦。
本文概述了一种解决这些问题的方案,该方案相对于其他方案而言,在语法上更直观,易于使用(只需引用该解决方案),并且性能相对较好(例如,对反射的依赖最小)。
以前的方法
这个解决方案绝不是第一个试图为问题提供便捷方法的。例如,Emiel Jongerius 在他的博客文章 On Developing Pochet 中概述的方法为我提供了一个有效解决方案一段时间,并且确实强调了许多我可能忽略的重要考虑因素。这为我提供了一个很好的起点,但也有一些重要的限制:
- 要使用核心实现,一个类需要继承自
BindableObjectBase3
,这在某些情况下是可以的,但并非总是如此。毕竟,C# 不允许多重继承,所以有时这是一个很大的要求。 - 调试起来有些痛苦,因为每次检索属性时,都需要创建和销毁一个
PropertyTracker
实例,并且用于防止通知风暴的大部分代码在调试时相对不直观。 - 对我而言,使用的语法相对不直观。
- 它表面上存在线程安全问题。
这些限制绝不意味着该方法不合适;它确实有效,而且效果相当不错。然而,它们足以促使我尝试自己解决这个问题,以避免这些问题(特别是前三个)。
术语
描述一种解决此问题的方法时,一个问题在于,尽管很容易描述 Cat.IsAlive
与 Cat.Heart.IsBeating
之间的关系(它是依赖于它的,但并非完全如此),但描述反向关系要困难得多。在本文中,我将这种关系称为基础关系——即,IsBeating
的值构成了 IsAlive
值的基础。
此外,作为 IsAlive
的载体的 Cat
将被称为 IsAlive
的依赖宿主(即,承载依赖属性的实体),而 Heart
将被称为基础宿主。在所有情况下,此解决方案都假设特定的依赖宿主和特定的基础宿主之间存在关系,并且例如,基础宿主可以在运行时更改(例如,如果调用了 Operations.HeartTransplant(PussInBoots, LionHeart)
)。
方法
虽然先前的方法侧重于维护一个属性更改处理程序的列表,当基础属性更改时需要调用这些处理程序,然后仅订阅此处理程序,但我的方法通过维护事件处理程序的链本身来规避这一点。我的意思是:
- 在对象构造之前,依赖项以类似于
DependencyProperty
实例功能的方式进行注册。这由DependencyManager
类通过一个或多个DependencyDelclaration
实例来处理,并产生一个与类类型对应的ConcurrentBag<DependencyDeclaration>
。 - 当对象构造时,会查询注册的依赖项列表,并用于构建应该在创建时附加到对象
PropertyChanged
处理程序的事件处理程序。 - 这些处理程序执行三个功能:
- 它们确认处理程序的发送者仍然与依赖宿主保持正确的关系。如果关系失败,则它们会分离并返回(并且应该在适当的时候被处置)。
- 如果它们是中间事件处理程序(即,它们之所以被触发是因为,例如,心脏移植),它们会将后续事件处理程序附加到链中下一个实体的新值。例如,心脏移植后:
- 触发
Cat.PropertyChanged
,通知Heart
已更改。 - 旧的
Heart
现在漂浮到虚空中(因此需要上面的验证或分离)。 - 新的
Heart
不知道它应该在IsBeating
更改时通知Cat
。我们需要对此做些什么,因此心脏更改的事件处理程序识别出一个合适的事件处理程序来响应Heart.IsBeating
更改,并将其附加到新的心脏。
- 触发
- 最后,调用
Cat.OnPropertyChanged
以指示依赖属性IsAlive
可能已经更改。
每次属性更改时,这都需要做大量工作,为了尽量减少工作量,所有事件处理程序都在类构造时构建,并存储在 HandlerGenerator
类中,使它们能够正确地相互交互。特别是,处理程序可以识别它们是否为中间处理程序,下一个处理程序是什么,并且可以附加和分离发送者,而无需强加对 Cat
或自身的任何不必要的引用,并且保持的时间比必要的更长。
另请注意,在模型中实现任何内容除了注册依赖项外,负担很小。这是通过将大部分代码放置在 ISupportsDependencyManagerExtensions
中作为 ISupportsDependencyManager
的扩展方法来实现的。这样,非常简单的实现就足够了。
Using the Code
使用此解决方案意味着采用几个相当简单的代码模式。这些模式包括依赖项注册、构造函数中的额外一行以及派生类的静态构造函数。我将分别介绍它们。
依赖项注册
注册依赖项涉及使用 DependencyManager.
RegisterDependency
或 DependencyManager.RegisterDependencies
。它们会返回传入的 DependencyDeclaration
,可以是单个的,也可以是数组。这主要是为了提供灵活性,以便将依赖项注册在提供信息的属性旁边,通常作为 Private Static
成员。采取这种方法在某种程度上模拟了使用基于 Attribute
的解决方案(由于属性无法接受 lambda 表达式作为参数,因此受到严重限制)。
依赖项注册还包括一个可选的 overrideBaseIdentifier
标志,它指定依赖项是添加到现有基类的依赖项,还是替换它们。当重写虚拟方法时,这可能很重要,因为与虚拟方法相关的依赖项可能不再适用于重写。
在我们检查如何声明 DependencyDeclaration
之前,让我们看一下属性定义。
public class Cat : ISupportsDependencyManager
{
public bool IsAlive
{
get {
return this.Heart.IsBeating && this.IsBreathing;
}
}
private static DependencyDeclaration[] IsAliveDependencies =
DependencyManager.RegisterDependencies<Cat>(
new DependencyDeclaration<Cat, Cat>(cat=>cat.IsAlive, cat=>cat.IsBreathing),
new DependencyDeclaration<Cat, Heart>(cat=>cat.IsAlive, heart=>heart.IsBeating, cat=>cat.Heart)
);
public bool IsBreathing {
get { return _IsBreathing; }
set
{
_IsBreathing = value;
this.OnPropertyChanged(()=>this.IsBreathing);
}
}
private bool _IsBreathing;
/* Other property definitions, including Cat.Heart */
}
这里有几点需要注意:
IsBreathing
通过一个重构友好的OnPropertyChanged
方法进行通知。一个简单的OnPropertyChanged(string PropertyName)
是实现ISupportsDependencyManager
所需的成员,但接受 lambda 的版本作为扩展方法提供。DependencyDeclaration
接受两个类型参数。第一个用于标识依赖宿主——即,拥有依赖属性的对象。第二个标识基础宿主类型,即,基础属性所属的对象。依赖宿主必须实现ISupportsDependencyManager
,而基础宿主必须实现INotifyPropertyChanged
。- 它还接受三个表达式参数。这些是:
- 一个表达式,用于从任意
TDependantHost
实例标识依赖属性。 - 一个表达式,用于从任意
TFoundationHost
实例标识基础属性。 - 一个表达式,用于标识如何从
TDependantHost
实例获取到TFoundationHost
实例。
- 一个表达式,用于从任意
其中,第三个最棘手。为了使其正常工作,链中的每个属性都必须也实现 INotifyPropertyChanged
。如果依赖项是内部的,如 IsBreathing
的情况,则不需要此参数。
该方法将在类首次被访问之前自动调用(至少,除了通过反射之外的任何机制),这将聚合所有 DependencyDeclaration
以用于构建类实例。
关于声明依赖项的最后一点。根据您是否使用属性 getter 来按需构建昂贵的属性(而不是在类构造时),您可能希望在创建声明后调用 .SetCascades()
。这表示,而不是只将最明显的事件处理程序附加到 Cat
,然后等到 Heart
更改(例如,从头开始分配)时才为其分配处理程序,而是在一开始就为链中的每个成员分配适当的处理程序。如果 Heart
已经存在(例如,private Heart _Heart = new Heart()
),这一点很重要,因为否则,除非进行移植,否则它将不会附加适当的处理程序。
.SetCascades()
是一个流畅的方法,返回相同的 DependencyDeclaration
,并将 Cascades
设置为 true
。
构造函数
类构造函数不需要做很多工作,但需要一种代码模式。这是一个初始调用 this.AttachAllHandlers(this.PropertyChanged)
。理想情况下,这应该放在构造函数的开头,特别是如果您不使用 .SetCascades()
,但只要在用于依赖项声明的字段被赋值之前发生,它仍然可以工作。
请注意,从基类构造函数直接调用 AttachAllHandlers
将在派生类构造函数之前执行,因此,如果您想在派生类的构造函数中为字段赋值,然后再调用 AttachAllHandlers
,您可能需要对代码进行一些调整。
静态构造函数
仅在继承自已实现 ISupportsDependencyManager
的类时才需要 static
构造函数。这是为了确保已注册到基类的依赖项也被导入——否则,继续以 cat
示例为例,将 Cat.Heart.IsBeating
链接到 Cat.IsAlive
的依赖项将不会自动应用于 SiameseCat
——毕竟,当 Cat
注册其依赖项时,它无法知道它是否会被派生,更不知道这些派生将是什么样的。
因此,SiameseCat
需要一个 static
构造函数来导入其基类依赖项,如下所示:
class SiameseCat : Cat
{
static SiameseCat
{
DependencyManager.RegisterBaseDependencies<SiameseCat>();
}
}
关注点
这段代码中有两个特别值得注意的元素。第一个是分层事件处理程序如何能够适当地识别“下一个”要附加的事件处理程序。将事件处理程序存储在有序列表中使之成为可能,因为 thisLevel + 1
对应于 nextLevel
。显然,当定义事件处理程序时,下一个级别尚不存在,但当它们执行时,它就存在了。
此外,由于所有事件处理程序都需要为实际目标对象触发 OnPropertyChanged
,因此存在内存泄漏的风险,即事件处理程序在所有其他实例都被消除后仍然保持对目标的引用。因此,在代码中的几个位置使用了 WeakReference
来确保这种情况不会发生。
最后,使用 Concurrent
类应该使此解决方案比某些先前的方法更具线程安全性。
历史
- 12/04/2015:首次草稿文章提交 - TJacobs
- 14/04/2015:正确附加项目;延迟表示歉意 - TJacobs