链式属性观察器






4.96/5 (42投票s)
一组实用类,用于观察INotifyPropertyChanged对象的链。
引言
我对写这篇文章的心情很复杂;一半的我认为这是一个非常酷的想法,另一半的我倾向于同意我的老团队负责人Fredrik Bornander的看法,当我向他解释我想做什么时,他只是简单地说了一个词“TrainWreck”,然后给我发了一个维基百科文章的链接
Fredrik曾经是我的团队负责人,这是有原因的,总的来说,他通常都很对,但我只是觉得我给他看的那段代码有一些有趣/有用的地方,所以我继续努力,写出了我在这篇文章中演示的代码。对不起Fredrik,无意冒犯。
就像我说的,我在这里展示的一些东西可能有些人不喜欢,但我认为这篇文章附带的代码确实有一些优点,即使你决定不使用我在这篇文章中介绍的通用实用类,它也展示了一些很好的.NET技术。
既然你已经读到这里,我想我应该告诉你这篇文章是关于什么的,这样你就可以决定是否继续阅读……那么我们开始吧。
你们中的一些人可能知道一个在Windows中几乎所有UI编程中都非常常见的接口,那就是INotifyPropertyChanged
接口,它允许实现此接口的对象引发更改通知事件,其他对象/框架子系统(例如绑定子系统)可以监听这些事件。
本文将展示我们如何创建一个INotifyPropertyChanged
观察器,该观察器能够监视INotifyPropertyChanged
对象链中的任何对象,这起初听起来可能很奇怪,但请考虑以下场景
我们需要一个允许存储人员的数据库,其中每个人都有一个地址,并且地址通过一些地理位置数据链接。这对我来说听起来非常有效,一个良好规范化的数据库将充满类似的场景。
现在,常识和数据库规范化定律将规定我们最终会得到这样的结果
所以我们会将其转换为对象图,像这样
public class Person : INotifyPropertyChanged
{
private Address address;
public Address Address
{
...
}
...
...
}
public class Address : INotifyPropertyChanged
{
private GeoLocation geoLocaation;
public GeoLocation GeoLocaation
{
...
}
...
...
}
public class GeoLocation : INotifyPropertyChanged
{
...
...
}
如果我们手动编写对象图,或者如果我们使用LINQ to SQL或LINQ to Entities,我们就会得到这样的图?
关于这一点,在我看来,数据库是正确规范化的,我们正在以我们应该的方式和地点存储数据,这又在我看来使得对象图层次结构正确,但正如你所看到的,那里肯定有一个相关对象的链。
因此,既然我们有这样一个对象层次结构(或者你能想到的任何其他例子),那么(以我的例子为例)当Person
的详细信息发生变化时,我们应该执行一些操作,这并不是一个太离谱的要求。
为了澄清我所说的“当Person
的数据发生变化”是什么意思,我真正的意思是如果Person
的数据发生变化,或者Person
的Address
发生变化,或者Person
的Address
的地理位置数据发生变化,我们都应该做些什么,这就是我真正的意思。
想象一下,当我们使用Person
对象作为更大过程的一部分时,当Person
的任何数据发生变化时,我们都想发送一张新Invoice
。现在,使用传统方法,我必须在一个持有Person
对象实例的顶层类中连接三个监听器(一个用于Person
,一个用于Person
的Address
,一个用于Address
的地理位置),这本身并没有那么糟糕,但是如果一个新Address
对象在某个我们无法预测的时间点被分配给该Person
对象呢?
我们实际上会有一个INPC事件处理程序(用于Person
对象的旧Address
对象)指向一个我们本应取消挂钩的过时对象,但我们什么时候会这样做呢?我们怎么会知道呢?我们必须监听Person
对象上的变化,其中被更改的属性是“Address”,并在那里取消挂钩旧的地址INPC监听器,这意味着我们随后需要为Person
的Address
重新挂钩一个新的INPC事件处理程序。
好的,弱事件可以在这里提供帮助,所以我们不一定需要担心取消挂钩,但我们仍然需要重新挂钩到新的Person
的Address
对象......如你所见,需要做相当多的内务管理。我对此的想法是,我为什么要进行这些内务管理?当然,我可以编写一些智能属性观察器来为我完成所有这些工作。
本文演示了一组类,可以更容易地监控对象链中任何地方的更改。实际上,您可以选择要监控链的哪些部分的更改。因此,如果这听起来对您有用,请继续阅读,否则不用担心,希望在下一篇文章中见到您。
类似的想法
虽然据我所知,目前还没有与本文中介绍的代码功能相同的现有解决方案,但本文确实从另一个出色的项目获得了灵感,那就是我的WPF同道Philip Sumi的优秀lambda 绑定文章,他提出了允许将两个lambda表达式传递给一个小类,然后这些表达式可以进行单向或双向绑定,并且还支持ValueConverters。
尽管我们在处理lambda表达式方面存在某些相似之处(如下所示),但我们试图实现的功能却大相径庭;我的代码是关于链式INPC回调的,而Philip的代码则是关于将一个lambda表达式绑定到另一个表达式。
目录
本文将涵盖以下内容
代码如何解决问题
处理大部分工作的主要类是“ChainPropertyObserver
”。它如何工作,用文字说明如下
- 我们创建一个初始节点链,这些节点观察lambda表达式提供的链中的对象。这些在内部表示为
List<ChainNode>
。List<ChainNode>
是通过获取lambda表达式中访问过的属性的属性名称的字符串值获得的,然后使用一点反射来获取实际对象,我们将这些对象存储在实际的ChainNode
实例中。我们还设置了一个整体监听器,当链中任何节点发生变化时,它都会被回调。 - 我们还接受 n 个特定回调处理程序,以监视链中特定的属性更改。这些在内部表示为
List<PropertyNode>
。List<PropertyNode>
是通过获取 lambda 表达式中访问过的属性的属性名称的字符串值获得的,然后使用少量反射来获取实际对象,我们将这些对象存储在实际的ChainNode
实例中。 - 添加特定的回调处理程序时,我们查找与回调具有相同对象类型和属性名称的
ChainNode
,并将为特定属性更改注册的特定回调委托分配给匹配的ChainNode
。基本上,我们只获取一组触发回调委托的节点,即List<ChainNode>
,因此我们必须找到正确的节点并为其分配正确的回调委托。 - 当检测到节点值发生变化时,我们获取其在链中的位置,从整个
List<ChainNode>
中移除其后(包括其本身)的所有节点,然后使用新的对象图和一点反射重新创建链的其余部分。我们还会重新将任何特定属性更改挂钩到ChainNode
,如第3步所述。
本质上,ChainPropertyObserver
内部包含一组节点(称为ChainNode
)来表示传入CreateChain(..)
方法的原始lambda,以及另一组节点来表示使用RegisterChainLinkHandler(..)
方法传入的lambdas。
让我们看一两张简图。
链创建节点
当我们第一次启动并使用ChainPropertyObserver.CreateChain(..)
方法创建链时,我们最终会得到(取决于传入CreateChain(..)
方法的lambda表达式)一个List<ChainNode>
,该列表保存在ChainPropertyObserver
内部。其中每个内部ChainNode
都持有对原始对象树中由传入的lambda表达式指定的属性的引用。
所以举例来说,如果我们传入一个Person.Address.GeoLocation
的lambda,我们最终会得到一个内部的List<ChainNode>
来表示这个lambda表达式,它将包含三个配置如下的ChainNode
ChainNode
,带有一个对象引用,指向持有Person
实例的任何对象的Person
对象属性ChainNode
,带有一个对象引用,指向前一个ChainNode
中Person
对象的Address
对象属性ChainNode
,带有一个对象引用,指向前一个ChainNode
中Address
对象的GeoLocation
对象属性
INPC 回调节点
当我们使用ChainPropertyObserver.RegisterChainLinkHandler(..)
方法注册特定回调时,我们几乎遵循与之前相同的过程,只不过这次,当解析传入ChainPropertyObserver.RegisterChainLinkHandler(..)
方法的lambda表达式时,我们最终会创建一个List<PropertyNode>
。
例如,如果我们传入一个Person.Address.GeoLocation
的lambda,并且在两次调用ChainPropertyObserver.RegisterChainLinkHandler(..)
方法时提供了以下内容,我们最终将在内部创建以下List<PropertyNode>
。
对于RegisterChainHandler(() => Person.Address)
PropertyNode
,带有一个对象引用,指向持有Person
实例的任何对象的Person
对象属性PropertyNode
,带有一个对象引用,指向前一个PropertyNode
中Person
对象的Address
对象属性
对于RegisterChainHandler(() => Person.Address.GeoLocation)
PropertyNode
,带有一个对象引用,指向持有Person
实例的任何对象的Person
对象属性PropertyNode
,带有一个对象引用,指向前一个PropertyNode
中Person
对象的Address
对象属性PropertyNode
,带有一个对象引用,指向前一个PropertyNode
中Address
对象的GeoLocation
对象属性
所以我希望这有点道理。总之,这就是它全部的工作原理,用文字来说,但我想你们现在可能想看一些代码,对吗?让我们继续看看。
解析原始链
正如我们现在所知,ChainPropertyObserver
上有一个方法必须用于设置初始链,该方法的用法(来自一个演示)如下所示
MyPerson = new Person();
MyPerson.Age = 12;
MyPerson.Name = "sacha";
MyPerson.Address = new Address()
{
Addressline1 = "21 maple street",
Addressline2 = "Hanover",
GeoLocation = new GeoLocation { Longitude=0.5, Latitude=0.5 }
};
//create chainPropertyObserver
chainPropertyObserver = new ChainPropertyObserver();
chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocation, Changed);
...
...
private void Changed(string propertyThatChanged)
{
lastChanges.Add(propertyThatChanged);
}
这里有几点需要注意,即我们有一个名为MyPerson
的Person
类型属性,它是这个测试类,并且我们向ChainPropertyObserver.CreateChain(..)
提供了一个lambda表达式,它接收一个整体链和一个整体更改处理委托。那之后会发生什么呢?
好吧,我们从头开始,也就是ChainPropertyObserver.CreateChain(..)
方法
public class ChainPropertyObserver : ExpressionVisitor
{
public void CreateChain<TSource>(Expression<Func<TSource>> source,
Action<string> overallChangedCallback)
{
this.overallChangedCallback =
new WeakDelegateReference(overallChangedCallback);
Visit(source);
CreateNodes();
isInitialised = true;
}
}
可以看出,ChainPropertyObserver
继承自 System.Linq.Expressions.ExpressionVisitor
类。因此,它允许我们 Visit(..)
一个表达式树,在本例中是提供给 ChainPropertyObserver.CreateChain(..)
方法的 Expression<Func<TSource>> source
。在上面的 ChainPropertyObserver.CreateChain(..)
方法中可以看出,我们确实调用了基类(System.Linq.Expressions.ExpressionVisitor
类)的 Visit(..)
方法。那么,让我们看看当我们 Visit(..)
一些表达式时会发生什么。
这很简单;我们只需要为我们希望访问的表达式提供重写。它们如下
/// <summary>
/// Visits a member within a Expression tree
/// So for example if we had a console app called
/// "ExpressionTest.Program" that had a public property like
/// public Person MyPerson { get; set; }
// and we then passed in a overall expression to this
/// class of chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocation)
///
/// This method would visit the following members in this order :
/// 1. GeoLocation
/// 2. Address
/// 3. MyPerson
/// </summary>
protected override Expression VisitMember(MemberExpression node)
{
if (!isInitialised)
{
membersSeen.Insert(0, node.Member.Name);
}
else
{
propMemberSeen.Insert(0, node.Member.Name);
}
return base.VisitMember(node);
}
/// <summary>
/// Visits a constant within a Expression tree
/// So for example if we had a console app called
/// "ExpressionTest.Program" that had a public property like
/// public Person MyPerson { get; set; }
/// and we then passed in a overall expression to this
/// class of chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocaation)
///
/// This method would visit a constant of the ExpressionTest.Program type object
/// </summary>
protected override Expression VisitConstant(ConstantExpression node)
{
if (!isInitialised)
{
wrRoot = new WeakReference(node.Value);
}
else
{
wrPropRoot = new WeakReference(node.Value);
}
return base.VisitConstant(node);
}
访问成员表达式
可以看出,当我们访问MemberExpression
时,我们存储了两样东西。我们维护一个访问过的成员名称列表,这些实际上只是我们在表达式树中看到的属性的字符串名称。我们维护两个列表,因为我们使用这些相同的访问方法来创建ChainNode
和PropertyNode
,所以我们只是将东西存储在两个内部列表中,memberSeen
用于ChainNode
s,propMemberSeen
用于PropertyNode
s。
访问常量表达式
可以看出,当我们访问ConstantExpression
时,我们存储了两件事。我们维护两个对象,它们实际上只是持有我们在表达式树中看到的源对象的原始对象。我们维护两个对象,因为我们使用这些相同的访问方法来创建ChainNode
和PropertyNode
,所以我们只是将东西存储在两个内部对象中,wrRoot
用于ChainNode
s,wrPropRoot
用于PropertyNode
s。
在访问完整个表达式树之后,我们最终会在ChainPropertyObserver
中存储类似这样的内容
这假设我们在一个演示应用程序中;这就是它指向一个被WeakReference
包装的WPF应用程序对象的原因。
最后发生的事情是,使用一点反射创建了一个内部ChainNode
列表(它继承自NodeBase
),我们利用了wrRoot WeakReference
和membersSeen List<string>
。这两个信息足以让我们完全反射出实际的对象链,我们将其存储在List<ChainNode>
中,该列表内部也使用WeakReference
s来包装节点所代表的对象。
以下是CreateNodes
方法的工作原理
private void CreateNodes()
{
ChainNode parent = null;
bool chainBroken = false;
for (int i = 0; i < membersSeen.Count; i++)
{
parent = (ChainNode)CreateNode(parent, membersSeen[i],
NodeType.ChainNode, out chainBroken);
//only create new ChainNodes as long as the chain is entact,
//and has no null object references
if (!chainBroken)
{
nodes.Add(parent);
parent.DependencyChanged += Parent_DependencyChanged;
}
else
break;
}
}
private object CreateNode(NodeBase parent, string propNameForNewChild,
NodeType nodeType, out bool chainBroken)
{
object nodeValue;
if (parent == null)
{
object activeRoot = null;
if (wrRoot.IsAlive)
{
activeRoot = wrRoot.Target;
}
if (activeRoot == null)
throw new InvalidOperationException("Root has been garbage collected");
nodeValue = activeRoot.GetType().GetProperty(
propNameForNewChild).GetValue(activeRoot, null);
if (nodeValue== null)
{
chainBroken=true;
return null;
}
}
else
{
nodeValue = parent.NodeValue.GetType().GetProperty(propNameForNewChild)
.GetValue(parent.NodeValue, null);
if (nodeValue == null)
{
chainBroken = true;
return null;
}
}
if (!(nodeValue is INotifyPropertyChanged))
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("ChainedObserver is only able to work with objects " +
"that implement the INotifyPropertyChanged interface");
sb.AppendLine("All objects in a chain MUST implement " +
"the INotifyPropertyChanged interface");
throw new InvalidOperationException(sb.ToString());
}
NodeBase node = null;
switch (nodeType)
{
case NodeType.ChainNode:
node = new ChainNode((ChainNode)parent, nodeValue, propNameForNewChild);
break;
case NodeType.PropertyNode:
node = new PropertyNode((PropertyNode)parent,
nodeValue, propNameForNewChild);
break;
}
chainBroken = false;
return node;
}
你们中的一些人可能会注意到,CreateNode(..)
方法也有能力创建PropertyNode
s;我们稍后会讲到。
为了完整起见,这是ChainNode
以及它继承的其他节点类型的样子
using System;
using System.ComponentModel;
using ChainedObserver.Weak;
using ChainedObserver.WeakEvents;
namespace ChainedObserver
{
/// <summary>
/// Represents a node in the original Lambda expression that was
/// passed to the <c>ChainPropertyObserver</c>. This class also
/// provides an overall DependencyChanged weak event
/// (using Daniel Grunwalds<c>WeakEvent</c>) that is raised whenever
/// the property that this <c>ChainNode</c> is monitoring changes.
/// Basically this node hooks a <c>NotifyPropertyChanged</c>
/// listener to a source object to know when a overall dependency changes.
/// When the dependency changes the DependencyChanged is raied which
/// allows an overall callback delegate to be called when any node in the
/// chain within the original Lambda expression changes. There is also
/// a callback here may run a specific callback handler that was
/// registered within the <c>ChainPropertyObserver</c>. This callback
/// may or may not be null for any given <c>ChainNode</c>, it depends
/// on what property this <c>ChainNode</c> is monitoring from the original
/// Lambda expression, as to whether there will be an active specific
/// callback assigned for a given <c>ChainNode</c>
///
/// When Dispose() is called this class will unhook
/// any <c>NotifyPropertyChanged</c>
/// listener that it has previously subscribed to
/// </summary>
public class ChainNode : GenericNodeBase<ChainNode>, IDisposable
{
private WeakEventHandler<PropertyChangedEventArgs> inpcHandler;
public ChainNode(ChainNode parent, object value, String name) :
base(parent,value,name)
{
inpcHandler =
new WeakEventHandler<PropertyChangedEventArgs>(PropertyChanged);
((INotifyPropertyChanged)value).PropertyChanged += inpcHandler.Handler;
}
public WeakDelegateReference PropertyChangedCallback { get; set; }
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//raise overall dependencyChangedEvent
dependencyChangedEvent.Raise(this, new ChainNodeEventArgs(this));
//now callback specific handler for this Node type/name
if (PropertyChangedCallback != null)
{
Action callback = (Action)PropertyChangedCallback.Target;
if (callback != null)
{
callback();
}
}
}
private readonly WeakEvent<EventHandler<ChainNodeEventArgs>> dependencyChangedEvent =
new WeakEvent<EventHandler<ChainNodeEventArgs>>();
public event EventHandler<ChainNodeEventArgs> DependencyChanged
{
add { dependencyChangedEvent.Add(value); }
remove { dependencyChangedEvent.Remove(value); }
}
public void Dispose()
{
((INotifyPropertyChanged)base.NodeValue).PropertyChanged -= inpcHandler.Handler;
}
}
}
using System;
namespace ChainedObserver
{
/// <summary>
/// Generic base class for all Nodes used. Simply
/// introduces a parent of type T, and passes
/// value and name constructor params to <c>NodeBase</c>
/// base class
/// </summary>
public class GenericNodeBase<T> : NodeBase
{
public GenericNodeBase(T parent, object value, String name)
: base(value, name)
{
this.Parent = parent;
}
public T Parent { get; set; }
}
}
using System;
namespace ChainedObserver
{
/// <summary>
/// Provides a common base class for all nodes.
/// Provides 2 properties :
/// <list type="Bullet">
/// <item>PropertyName : which is the name of the
/// property the node is monitoring from the overall lambda
/// expression provided to the
/// <c>ChainedPropertyObserver</c></item>
/// <item>NodeValue : Holds a WeakReference to the property
/// value the node is monitoring from the overall lambda expression
/// provided to the <c>ChainedPropertyObserver</c></item>
/// </list>
/// </summary>
public class NodeBase
{
private WeakReference wrNodeValue;
public NodeBase(object value, String name)
{
this.PropertyName = name;
wrNodeValue = new WeakReference(value);
}
public String PropertyName { get; set; }
public object NodeValue
{
get
{
object target = wrNodeValue.Target;
if (target != null)
{
return target;
}
return null;
}
}
}
}
解析已注册的链回调
当用户代码调用ChainedPropertyObserver RegisterChainLinkHandler(Expression<Func<object>> handlerExpression, Action propertyChangedCallback)
方法时,处理程序表达式的解析方式与我们首次调用ChainedPropertyObserver.CreateChain(..)
方法时提供的原始表达式的解析方式大致相同。
还需要发生两件事
- 我们需要在内部
Dictionary
中存储一个条目,该条目表示在ChainedPropertyObserver.RegisterChainLinkHandler(..)
方法中提供的handlerExpression
中看到的最后一个对象。我们选择最后一个节点,因为这显然是用户在提供handlerExpression
时的意图。 - 我们还会遍历原始的
List<ChainNode>
,尝试找到与用户提供的特定处理程序匹配的ChainNode
,当我们找到原始的ChainNode
时,我们将回调委托分配给它。
与其他地方一样,我们实际上不持有委托(这将需要强引用),我们使用WeakDelegateReference
允许原始对象被GC。
现在让我们看看相关代码
public void RegisterChainLinkHandler(
Expression<Func<object>> handlerExpression,
Action propertyChangedCallback)
{
handlers.Add(handlerExpression,
new WeakDelegateReference(propertyChangedCallback));
RecreateHandlers();
}
所以我们所做的只是添加handlerExpression
,以及一个包装了原始回调委托的WeakDelegateReference
被添加到内部字典中,然后调用RecreateHandlers()
方法。现在我们来看看它。
private void RecreateHandlers()
{
existingPropHandlerNodes.Clear();
foreach (KeyValuePair<Expression<Func<object>>,
WeakDelegateReference> handler in handlers)
{
propMemberSeen.Clear();
Visit(handler.Key);
CreatePropertyHandlerNodes();
//only add in existing PropertyHandler if we managed to create an entire
//chain of nodes, ie no nulls in chain
if (workingPropHandlerNodes.Last().PropertyName == propMemberSeen.Last())
{
existingPropHandlerNodes.Add(workingPropHandlerNodes.Last(), handler.Value);
}
}
ReCreateChainLinkHandlers();
}
可以看出,此方法只是遍历已注册的特定回调的维护的Dictionary
中的每个键/值对。然后,我们以与之前大致相同的方式遍历handlerExpression
,并为访问过的handlerExpression
创建一个List<PropertyNode>
。
谜题的最后一部分是使用刚刚创建的List<PropertyNode>
来遍历原始的List<ChainNode>
,并使用预定义的匹配规则找到与最后一个PropertyNode
匹配的那个。当ChainNode
匹配时,它的PropertyChangedCallback
简单地被分配给WeakDelegateReference
包装的回调委托。这是通过ReCreateChainLinkHandlers()
方法完成的,如下所示
private void ReCreateChainLinkHandlers()
{
foreach (KeyValuePair<PropertyNode,
WeakDelegateReference> propCallBack in existingPropHandlerNodes)
{
var matchedNodes = (from n in nodes
where n.PropertyName == propCallBack.Key.PropertyName &&
n.NodeValue.GetType() == propCallBack.Key.NodeValue.GetType()
select n);
if (matchedNodes.Count() == 1)
{
ChainNode actualNode = matchedNodes.First();
actualNode.PropertyChangedCallback = propCallBack.Value;
}
}
}
为了完整起见,这是PropertyNode
的样子;你之前见过它的基类
using System;
namespace ChainedObserver
{
/// <summary>
/// A convience class that simply inherits from <c>GenericNodeBase of T</c>
/// where T is PropertyNode
/// </summary>
public class PropertyNode : GenericNodeBase<PropertyNode>
{
#region Ctor
public PropertyNode(PropertyNode parent, object value, String name)
: base(parent,value,name)
{
}
#endregion
}
}
监听链中的变化
ChainPropertyObserver
的一个相当巧妙之处在于,当一个新对象被分配到现有观察器链的任何部分时(我们使用内部的 List<ChainNode>
进行监视),它会修复断开的链接。
用文字来说,发生的情况如下
当检测到ChainNode nodeValue
发生变化时,它会触发其DependencyChanged
事件,该事件由ChainPropertyObserver
监听。当从任何内部持有的List<ChainNode>
中看到此事件时,我们获取更改的ChainNode
在内部持有的List<ChainNode>
中的位置,从整个List<ChainNode>
中移除其后(包括其本身)的所有节点,然后使用新的对象图和一点反射重新创建链的其余部分(基本上,与我们已经遍历membersSeen
列表来创建初始List<ChainNode>
的方式相同)。
我们还会将任何特定的属性更改重新挂钩到已注册的ChainNode
。
通过执行这些步骤,我们尝试重新创建我们希望观察的原始链(由原始lambda指定),并尝试将任何注册的特定回调连接到List<ChainNode>
中的当前对象。
我觉得是时候展示一些代码了。
如果我们看看当ChainPropertyObserver
检测到它观察的ChainNode
之一发生变化时会发生什么
private void Parent_DependencyChanged(object sender, ChainNodeEventArgs e)
{
int indexOfChangedNode = nodes.IndexOf(e.Node);
for (int i = indexOfChangedNode; i < nodes.Count; i++)
{
nodes[i].DependencyChanged -= Parent_DependencyChanged;
}
RecreateNodes((ChainNode)e.Node.Parent, indexOfChangedNode);
Action<string> callback = (Action<string>)overallChangedCallback.Target;
if (callback != null)
{
callback(e.Node.PropertyName);
}
}
我们可以看到,我们计算出发生变化的ChainNode
的索引,然后调用RecreateNodes(..)
方法,在该方法中,链的更改部分将被移除,并创建链的新部分以替换更改的部分。
private void RecreateNodes(ChainNode oldParent, int indexOfChangedNode)
{
//remove changed section of chain, and also clean the removed node up
for (int i = nodes.Count - 1; i >= indexOfChangedNode; i--)
{
ChainNode node = nodes[i];
nodes.Remove(node);
node.Dispose();
node = null;
}
//recreate new section of chain
ChainNode parent = oldParent;
bool chainBroken = false;
for (int i = indexOfChangedNode; i < membersSeen.Count; i++)
{
parent = (ChainNode)CreateNode(parent, membersSeen[i],
NodeType.ChainNode, out chainBroken);
//only carry on creating the chain while the chain is
//intact (ie no null references seen)
if (!chainBroken)
{
nodes.Add(parent);
}
else
break;
}
for (int i = indexOfChangedNode; i < nodes.Count; i++)
{
nodes[i].DependencyChanged += Parent_DependencyChanged;
}
//recreate and attach specific callback handlers
RecreateHandlers();
}
在ChainNode
s被重新创建并重新分配特定回调后,我们简单地调用用户在调用ChainPropertyObserver.CreateChain(..)
方法时提供的整体更改回调。
特定回调如何触发
好的,我们做了很多工作,但你可能想知道我们分配的特定回调(如前所述)实际上是如何被调用的。这很简单;这一切都归结于ChainNode
中的以下代码,它是唯一调用已注册的特定更改回调的节点类型。
public class ChainNode : GenericNodeBase<ChainNode>, IDisposable
{
private WeakEventHandler<PropertyChangedEventArgs> inpcHandler;
public ChainNode(ChainNode parent, object value, String name) : base(parent,value,name)
{
inpcHandler = new WeakEventHandler<PropertyChangedEventArgs>(PropertyChanged);
((INotifyPropertyChanged)value).PropertyChanged += inpcHandler.Handler;
}
public WeakDelegateReference PropertyChangedCallback { get; set; }
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//raise overall dependencyChangedEvent
dependencyChangedEvent.Raise(this, new ChainNodeEventArgs(this));
//now callback specific handler for this Node type/name
if (PropertyChangedCallback != null)
{
Action callback = (Action)PropertyChangedCallback.Target;
if (callback != null)
{
callback();
}
}
}
public void Dispose()
{
((INotifyPropertyChanged)base.NodeValue).PropertyChanged -= inpcHandler.Handler;
}
}
处理链中的空值
当我第一次发表这篇文章时,一位读者正确地指出,它只有在链中的所有对象都指向非空对象引用时才能工作,这是正确的,确实是一个疏忽。
我对此进行了一些思考,现在已经修改了代码以处理空值。我不会深入探讨其所有内部工作原理,但在所附代码中遵循了一些规则,我们将在下面进行介绍。理解这些规则应该足以向您展示其内部工作原理
假设你有一个像这样的对象图
Person p = new Person();
p.Age = 12;
p.Name = "sacha";
p.Address = new Address() {
Addressline1="21 maple street",
Addressline2 = "Hanover",
GeoLocation = new GeoLocation { Longitude=0.555, Latitude=0.787 } };
现在让我们看几个用例
- 如果我们有一个注册在
GeoLocation
上的特定监听器,并且Address
被设置为null,我们将不会在GeoLocation
特定回调上收到回调,直到链中有一个GeoLocation
对象被分配给一个非null的Address
对象 - 如果我们有一个注册在
Address
上的特定监听器,并且GeoLocation
被设置为null,我们仍然会收到Address
特定回调,因为GeoLocation
在链中更靠后。
我想这差不多总结了链中如何处理空对象引用;本文中的代码以及本文底部的控制台演示都展示了更多细节。
清理
ChainPropertyObserver
几乎在所有地方都使用 WeakReference
/WeakEvent
,但还有一个简单的内务处理方法,当您完全使用完 ChainPropertyObserver
时可以使用,它将简单地调用其当前持有的所有 List<ChainNode>
上的 Dispose()
。
您可以像这样简单地使用此内务清理功能
ChainPropertyObserver.Clense();
已知问题 / 弱引用的重要性
可以想象,维护一个对象链,其中链的任何部分都可能在任何时刻被替换,这已经足够糟糕了,但所附的演示代码不仅要做到这一点,它还需要知道何时发生这种情况,并重新将注册的回调处理程序委托引用附加到链中的新对象。
现在如果你读完那一段后觉得没什么问题,再想想。我们有一个A->B->C的对象链,我们不仅持有引用,而且我们还在这些对象上挂钩了回调处理程序,但是当链变为A->B1-C时会发生什么?我们仍然在某个地方持有对B的引用。这听起来不像我们这个所谓有用的实用程序正在持有对象引用吗?听起来确实如此,对吧?
如果你也这么认为,那你就对了,这是一个真正的问题,所以我们需要对此做些什么。我们能做什么?幸运的是,.NET Framework以WeakReference
类的形式提供了答案,这是一个非常有用的类,它允许我们包装一个对象引用,但我们包装的原始对象仍然可以被垃圾回收。
在本文章中,我所介绍的ChainPropertyObserver
代码中,几乎所有东西都是弱引用的,从链中持有对象引用(这使得旧的冗余链对象可以被垃圾回收),到持有回调委托,再到自定义事件……它们都是弱引用的。基本上,在ChainPropertyObserver
试图解决的场景中,弱引用是好的。我们来看一些例子,好吗?
Lambda 对象引用
当我们第一次访问表达式树常量节点时,我们使用WeakReference
s来持有常量(lambda所代表的对象图顶部的对象),这可以从ChainPropertyObserver
的以下代码中看出
protected override Expression VisitConstant(ConstantExpression node)
{
if (!isInitialised)
{
wrRoot = new WeakReference(node.Value);
}
else
{
wrPropRoot = new WeakReference(node.Value);
}
return base.VisitConstant(node);
}
ChainNode/PropertyNode 对象引用
我们知道,链中的每个NodeBase
项(ChainNode
和PropertyNode
都继承自NodeBase
)都将持有一个NodeValue
。在构建注册的回调链时也是如此,所以让我们看看这些对象是如何存储在NodeBase
对象上的。
实际的NodeBase
对象看起来像这样
public class NodeBase
{
private WeakReference wrNodeValue;
public NodeBase(object value, String name)
{
this.PropertyName = name;
wrNodeValue = new WeakReference(value);
}
public String PropertyName { get; set; }
public object NodeValue
{
get
{
object target = wrNodeValue.Target;
if (target != null)
{
return target;
}
return null;
}
}
}
当我们创建NodeBase
派生类(无论是PropertyNode
还是ChainNode
)时,我们会做类似这样的事情
new ChainNode((ChainNode)parent, nodeValue, propNameForNewChild);
其中nodeValue
是对原始链Expression
中对象的引用。所以我们确实需要将该对象引用包装在一个WeakReference
中。幸运的是,NodeBase
类为我们做到了这一点,如上面NodeBase
的代码片段所示。
注册的回调
另一个需要WeakReference
的地方是当我们注册最终将分配给ChainNode
的特定回调委托时。我们使用自定义的WeakDelegateReference
来处理这个问题。
以下是回调的注册方式
public void RegisterChainLinkHandler(
Expression<Func<object>> handlerExpression,
Action propertyChangedCallback)
{
handlers.Add(handlerExpression, new WeakDelegateReference(propertyChangedCallback));
.....
}
这是WeakDelegateReference
代码
using System;
using System.Reflection;
namespace ChainedObserver.Weak
{
/// <summary>
/// A simple weak Delegate helper, that will maintain a Delegate
/// using a WeakReference but allow the original Delegate to be
/// reconstrcuted from the WeakReference
/// </summary>
public class WeakDelegateReference
{
private readonly Type delegateType;
private readonly MethodInfo method;
private readonly WeakReference weakReference;
public WeakDelegateReference(Delegate @delegate)
{
if (@delegate == null)
{
throw new ArgumentNullException("delegate");
}
this.weakReference = new WeakReference(@delegate.Target);
this.method = @delegate.Method;
this.delegateType = @delegate.GetType();
}
public Delegate Target
{
get
{
return this.TryGetDelegate();
}
}
private Delegate TryGetDelegate()
{
if (this.@method.IsStatic)
{
return Delegate.CreateDelegate(this.@delegateType, null, this.@method);
}
object target = this.@weakReference.Target;
if (target != null)
{
return Delegate.CreateDelegate(this.@delegateType, target, this.@method);
}
return null;
}
}
}
链节点依赖性变更
另一个令人担忧的方面是,我们所持有的对象链中的每个项目(由于NodeBase
)都是一个基于INPC的对象,它暴露了一个正在被监听的PropertyChanged
事件。正如我也指出的,ChainPropertyObserver
也能够在链中引入新对象时重建链,但我们仍然将监听器连接到旧对象的INPC PropertyChanged
。这听起来像是麻烦。
所以我们必须问自己,如果我们正在连接到某个旧对象的PropertyChanged
事件,我们难道不应该也断开连接吗?或者更好的是,使用某种弱事件处理程序监听所有INPC对象。第二种方法是我选择采用的。
同样,这会自动为您完成,既通过在NodeBase
中为NodeValue
持有一个WeakReference
对象,又通过在ChainNode
中完成弱事件监听。如果我们查看ChainNode
代码中的相关部分,我们可以看到NodeBase
NodeValue
WeakReference
ed对象的INPC PropertyChanged
事件是如何使用弱事件监听器监听的。这个弱事件监听器是由WPF同道Paul Stovell编写的。
我还在ChainNode
实现的IDisposable
中取消挂钩了弱事件监听器。
在ChainNode
中还做了另一件事,那就是它还响应NodeBase NodeValue WeakReference
d对象的INPC PropertyChanged
事件的变化,暴露了一个DependencyChanged
事件。这个ChainNode.DependencyChanged
事件是ChainPropertyObserver
内部使用的。这个事件实际上是ChainNode
中的一个WeakEvent
。这个WeakEvent
来自Daniel Grunwald的优秀CodeProject文章:WeakEvents.aspx
总之,这是ChainNode
中处理所有这些的相关代码
using System;
using System.ComponentModel;
using ChainPropertyObserver.Weak;
using ChainPropertyObserver.WeakEvents;
namespace ChainPropertyObserver
{
/// <summary>
/// Represents a node in the original Lambda expression that was
/// passed to the <c>ChainPropertyObserver</c>. This class also
/// provides an overall DependencyChanged weak event
/// (using Daniel Grunwalds<c>WeakEvent</c>) that is raised whenever
/// the property that this <c>ChainNode</c> is monitoring changes.
/// Basically this node hooks a <c>NotifyPropertyChanged</c>
/// listener to a source object to know when a overall dependency changes.
/// When the dependency changes the DependencyChanged is raied which
/// allows an overall callback delegate to be called when any node in the
/// chain within the original Lambda expression changes. There is also
/// a callback here may run a specific callback handler that was
/// registered within the <c>ChainPropertyObserver</c>. This callback
/// may or may not be null for any given <c>ChainNode</c>, it depends
/// on what property this <c>ChainNode</c> is monitoring from the original
/// Lambda expression, as to whether there will be an active specific
/// callback assigned for a given <c>ChainNode</c>
///
/// When Dispose() is called this class will
/// unhook any <c>NotifyPropertyChanged</c>
/// listener that it has previously subscribed to
/// </summary>
public class ChainNode : GenericNodeBase<ChainNode>, IDisposable
{
private WeakEventHandler<PropertyChangedEventArgs> inpcHandler;
public ChainNode(ChainNode parent, object value, String name) :
base(parent,value,name)
{
inpcHandler =
new WeakEventHandler<PropertyChangedEventArgs>(PropertyChanged);
((INotifyPropertyChanged)value).PropertyChanged += inpcHandler.Handler;
}
public WeakDelegateReference PropertyChangedCallback { get; set; }
private void PropertyChanged(object sender, PropertyChangedEventArgs e)
{
//raise overall dependencyChangedEvent
dependencyChangedEvent.Raise(this, new ChainNodeEventArgs(this));
//now callback specific handler for this Node type/name
if (PropertyChangedCallback != null)
{
Action callback = (Action)PropertyChangedCallback.Target;
if (callback != null)
{
callback();
}
}
}
private readonly WeakEvent<EventHandler<ChainNodeEventArgs>>
dependencyChangedEvent =
new WeakEvent<EventHandler<ChainNodeEventArgs>>();
public event EventHandler<ChainNodeEventArgs> DependencyChanged
{
add { dependencyChangedEvent.Add(value); }
remove { dependencyChangedEvent.Remove(value); }
}
public void Dispose()
{
((INotifyPropertyChanged)base.NodeValue).PropertyChanged -= inpcHandler.Handler;
}
}
}
总之,我希望通过探讨这些关注点以及ChainPropertyObserver
如何处理这些情况,能让您安心。
如何使用它
我认为本文介绍的代码非常易于使用,基本上可以归结为这三个步骤
- 创建
ChainPropertyObserver
- 创建一个你希望监控的链,并带有一个整体的更改回调
- (可选)为链的一部分注册一个特定的回调处理程序
这就像这样完成
chainPropertyObserver = new ChainPropertyObserver();
这就像这样完成
chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocation, Changed);
这就像这样完成
//And now hook up some specific Chain Listeners
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson.Address,
() =>
{
//Do whatever
});
ChainPropertyObserver
还有一个 Clense()
方法,当您完全使用完 ChainPropertyObserver
时应该调用它。我没有在演示中展示它,因为何时调用此 Clense()
方法本质上由您决定;这取决于您。正如我也说过,所有对象引用/事件等都是使用 WeakReference
完成的,因此不应该出现内存被占用的问题,但如果可以的话,您应该调用此方法。
注意:有些人很可能会问我为什么没有让它实现IDisposable
,这样就可以使用using(..)
语句,但这会很奇怪,因为如果我通过实现IDisposable
来允许使用using(..)
语句,代码看起来会像这样
//create chainPropertyObserver
using (new ChainPropertyObserver())
{
chainPropertyObserver.CreateChain(() =>
MyPerson.Address.GeoLocation, Changed);
//And now hook up some specific Chain Listeners
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson,
() =>
{
personChanges.Add(MyPerson.ToString());
});
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson.Address,
() =>
{
addressChanges.Add(MyPerson.Address.ToString());
});
chainPropertyObserver.RegisterChainLinkHandler(() =>
MyPerson.Address.GeoLocation, () =>
{
geoLocationChanges.Add(MyPerson.Address.GeoLocation.ToString());
});
}
因此,在那里会发生的是,IDisposable.Dispose()
方法将在using(..)
语句的末尾被调用,而ChainPropertyObserver
将无法正常工作。所以我选择了手动清理方法,对此感到抱歉。
总之,既然您已经知道如何使用所附代码,让我们快速浏览一下我随下载代码提供的两个演示。
控制台演示
这是一个非常简单的控制台应用程序示例,我们遵循我刚刚描述的三个强制步骤。这是完整的演示代码
using System;
using System.Collections.Generic;
using CommonModel;
namespace ChainedObserver.Test
{
class Program
{
public Person MyPerson { get; set; }
public void Run()
{
//create INPC model
Person p = new Person();
p.Age = 12;
p.Name = "sacha";
p.Address = new Address() {
Addressline1="21 maple street",
Addressline2 = "Hanover",
GeoLocation = new GeoLocation { Longitude=0.555, Latitude=0.787 } };
MyPerson = p;
//create observer
ChainPropertyObserver chainPropertyObserver = new ChainPropertyObserver();
chainPropertyObserver.CreateChain(() =>
MyPerson.Address.GeoLocation, Changed);
List<string> personChanges = new List<string>();
List<string> addressChanges = new List<string>();
List<string> geoLocationChanges = new List<string>();
//create some INPC listeners
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson,
() =>
{
Console.WriteLine("The Person that changed is now : {0}",
MyPerson.ToString());
personChanges.Add(MyPerson.ToString());
});
chainPropertyObserver.RegisterChainLinkHandler(() => MyPerson.Address,
() =>
{
Console.WriteLine("The Address that changed is now : {0}",
MyPerson.Address.ToString());
addressChanges.Add(MyPerson.Address.ToString());
});
chainPropertyObserver.RegisterChainLinkHandler(() =>
MyPerson.Address.GeoLocation, () =>
{
Console.WriteLine("The GeoLocation that changed is now : {0}",
MyPerson.Address.GeoLocation.ToString());
geoLocationChanges.Add(MyPerson.Address.GeoLocation.ToString());
});
//Chain the Chain data, including setting new objects in the chain
MyPerson.Address = new Address()
{
Addressline1 = "45 there street",
Addressline2 = "Pagely",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude=0.5 }
};
MyPerson.Address = new Address()
{
Addressline1 = "101 new town road",
Addressline2 = "Exeter",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address = null;
MyPerson.Address = new Address()
{
Addressline1 = "12 fairweather Road",
Addressline2 = "Kent",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address = null;
MyPerson.Address = new Address()
{
Addressline1 = "45 plankton avenue",
Addressline2 = "bristol",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address.GeoLocation =
new GeoLocation { Longitude = 0.49, Latitude = 0.49 };
MyPerson.Address.GeoLocation =
new GeoLocation { Longitude = 0.49, Latitude = 0.49 };
MyPerson.Address.GeoLocation = null;
MyPerson.Address.GeoLocation =
new GeoLocation { Longitude = 0.66, Latitude = 0.71 };
MyPerson.Address.GeoLocation.Longitude = 0.51;
MyPerson.Address.GeoLocation.Longitude = 0.52;
MyPerson.Address.GeoLocation.Longitude = 0.53;
MyPerson.Address.GeoLocation.Longitude = 0.54;
MyPerson.Address.GeoLocation.Longitude = 0.54;
Console.ReadLine();
}
private void Changed(string propertyThatChanged)
{
Console.WriteLine("The property that changed was : {0}",
propertyThatChanged);
}
static void Main(string[] args)
{
Program p = new Program();
p.Run();
}
}
}
此演示代码最重要的部分是实际证明事情按预期工作,并随链中对象的更改而改变。回顾一下,我们是这样做的
//Chain the Chain data, including setting new objects in the chain
MyPerson.Address = new Address()
{
Addressline1 = "45 there street",
Addressline2 = "Pagely",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude=0.5 }
};
MyPerson.Address = new Address()
{
Addressline1 = "101 new town road",
Addressline2 = "Exeter",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address = null;
MyPerson.Address = new Address()
{
Addressline1 = "12 fairweather Road",
Addressline2 = "Kent",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address = null;
MyPerson.Address = new Address()
{
Addressline1 = "45 plankton avenue",
Addressline2 = "bristol",
GeoLocation = new GeoLocation { Longitude = 0.5, Latitude = 0.5 }
};
MyPerson.Address.GeoLocation = new GeoLocation { Longitude = 0.49, Latitude = 0.49 };
MyPerson.Address.GeoLocation = new GeoLocation { Longitude = 0.49, Latitude = 0.49 };
MyPerson.Address.GeoLocation = null;
MyPerson.Address.GeoLocation = new GeoLocation { Longitude = 0.66, Latitude = 0.71 };
MyPerson.Address.GeoLocation.Longitude = 0.51;
MyPerson.Address.GeoLocation.Longitude = 0.52;
MyPerson.Address.GeoLocation.Longitude = 0.53;
MyPerson.Address.GeoLocation.Longitude = 0.54;
MyPerson.Address.GeoLocation.Longitude = 0.54;
基于此,我预计会发生以下情况。请注意ChainedPropertyObserver
在链中看到空对象引用后是如何恢复的;它只是恢复了。
Person
因分配了新的Address
对象而改变(Person
改变 = 1)Person
因分配了一个新的Address
对象而改变(Person
改变 = 2)Person
因分配了一个空Address
对象而改变(Person
改变 = 3)Person
因分配了一个新的Address
对象而改变(Person
改变 = 4)Person
因分配了一个空Address
对象而改变(Person
改变 = 5)Person
因分配了一个新的Address
对象而改变(Person
改变 = 6)Person.Address
因分配了一个新的GeoLocation
对象而改变(Person.Address
改变 =1)- 第二个
Person.Address GeoLocation
更改被忽略,因为它与上一个GeoLocation
对象的数据相同(Person.Address
更改 =1) Person.Address.GeoLocation
因分配了一个空GeoLocation
对象而改变(Person
改变 = 2)Person.Address
因分配了一个新的GeoLocation
对象而改变(纬度0.66,经度0.71)(Person.Address
改变 =3)Person.Address.GeoLocation
因GeoLocation
数据对象值变为0.51而改变(Person.Address.GeoLocation
改变 =1)Person.Address.GeoLocation
因GeoLocation
数据对象值变为0.52而改变(Person.Address.GeoLocation
改变 =2)Person.Address.GeoLocation
因GeoLocation
数据对象值变为0.53而改变(Person.Address.GeoLocation
改变 =3)Person.Address.GeoLocation
因GeoLocation
数据对象值变为0.54而改变(Person.Address.GeoLocation
改变 =4)- 第五个
Person.Address.GeoLocation
更改被忽略,因为它与前一个GeoLocation
对象的数据相同(Person.Address.GeoLocation
更改 =4)
那么让我们看看演示代码的截图,看看我们的预期是否属实。我们预计会发生以下更改
Person
改变 =6Address
改变 = 3GeoLocation
改变 = 4
以下是演示应用程序在调试模式下的截图,证明了这一点(单元测试会更好,抱歉)
所有更改完成后,我们看到只有6次Person
更改
并且只有3次Address
更改
并且有4次GeoLocation
更改
这非常酷。我们只收到了我们预期的更改通知。ChainPropertyObserver
完全能够处理链中用新对象替换现有对象的情况,它完全满意并知道如何处理这种情况,并且仍然向我们提供了我们注册关注的正确INPC回调。
我说这实际上非常有用。
WPF 演示
我还制作了一个更复杂的演示,将对象图绑定为{Binding Person.Address.GeoLocation}
,这虽然我不太喜欢,但它很好地演示了演示。WPF演示看起来像这样,它允许您手动创建更改或使用提供的几个Button
来查看单击Button
时预计哪些属性会更改。您应该查阅代码。
此演示包含以下INPC更改的ListBox
es,因此您可以查看手动更改或使用提供的Button
时会发生什么。
正如我所说,您需要查阅代码以更好地了解单击Button
时发生的更改。它已进行了非常具体的设置。
暂时就这些
我现在只想说这么多。就像我说的,我无法确定这是否真的有用或无用,但尽管如此,我认为这是一段有趣的代码,它向你展示了如何使用Lambda表达式树构建对象链,并且它还演示了一些有趣的弱事件/委托内容,因此仅凭这些原因,我很高兴发表了这篇文章……如果你喜欢它并且能看到它的用途,请告诉我,如果你非常乐意,投票/现金/啤酒/旧酸浩斯唱片和女仆总是受欢迎的……也会接受一只羊驼。
历史
- 2011年3月9日:首次发布。
- 2011年3月10日:更改了实现以处理链中的空对象引用。文章对此进行了讨论。