65.9K
CodeProject 正在变化。 阅读更多。
Home

链式属性观察器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (42投票s)

2011年3月9日

CPOL

26分钟阅读

viewsIcon

181282

downloadIcon

877

一组实用类,用于观察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的数据发生变化,或者PersonAddress发生变化,或者PersonAddress的地理位置数据发生变化,我们都应该做些什么,这就是我真正的意思。

想象一下,当我们使用Person对象作为更大过程的一部分时,当Person的任何数据发生变化时,我们都想发送一张新Invoice。现在,使用传统方法,我必须在一个持有Person对象实例的顶层类中连接三个监听器(一个用于Person,一个用于PersonAddress,一个用于Address的地理位置),这本身并没有那么糟糕,但是如果一个新Address对象在某个我们无法预测的时间点被分配给该Person对象呢?

我们实际上会有一个INPC事件处理程序(用于Person对象的旧Address对象)指向一个我们本应取消挂钩的过时对象,但我们什么时候会这样做呢?我们怎么会知道呢?我们必须监听Person对象上的变化,其中被更改的属性是“Address”,并在那里取消挂钩旧的地址INPC监听器,这意味着我们随后需要为PersonAddress重新挂钩一个新的INPC事件处理程序。

好的,弱事件可以在这里提供帮助,所以我们不一定需要担心取消挂钩,但我们仍然需要重新挂钩到新的PersonAddress对象......如你所见,需要做相当多的内务管理。我对此的想法是,我为什么要进行这些内务管理?当然,我可以编写一些智能属性观察器来为我完成所有这些工作。

本文演示了一组类,可以更容易地监控对象链中任何地方的更改。实际上,您可以选择要监控链的哪些部分的更改。因此,如果这听起来对您有用,请继续阅读,否则不用担心,希望在下一篇文章中见到您。

类似的想法

虽然据我所知,目前还没有与本文中介绍的代码功能相同的现有解决方案,但本文确实从另一个出色的项目获得了灵感,那就是我的WPF同道Philip Sumi的优秀lambda 绑定文章,他提出了允许将两个lambda表达式传递给一个小类,然后这些表达式可以进行单向或双向绑定,并且还支持ValueConverters。

尽管我们在处理lambda表达式方面存在某些相似之处(如下所示),但我们试图实现的功能却大相径庭;我的代码是关于链式INPC回调的,而Philip的代码则是关于将一个lambda表达式绑定到另一个表达式。

目录

本文将涵盖以下内容

代码如何解决问题

处理大部分工作的主要类是“ChainPropertyObserver”。它如何工作,用文字说明如下

  1. 我们创建一个初始节点链,这些节点观察lambda表达式提供的链中的对象。这些在内部表示为List<ChainNode>List<ChainNode>是通过获取lambda表达式中访问过的属性的属性名称的字符串值获得的,然后使用一点反射来获取实际对象,我们将这些对象存储在实际的ChainNode实例中。我们还设置了一个整体监听器,当链中任何节点发生变化时,它都会被回调。
  2. 我们还接受 n 个特定回调处理程序,以监视链中特定的属性更改。这些在内部表示为 List<PropertyNode>List<PropertyNode> 是通过获取 lambda 表达式中访问过的属性的属性名称的字符串值获得的,然后使用少量反射来获取实际对象,我们将这些对象存储在实际的 ChainNode 实例中。
  3. 添加特定的回调处理程序时,我们查找与回调具有相同对象类型和属性名称的ChainNode,并将为特定属性更改注册的特定回调委托分配给匹配的ChainNode。基本上,我们只获取一组触发回调委托的节点,即List<ChainNode>,因此我们必须找到正确的节点并为其分配正确的回调委托。
  4. 当检测到节点值发生变化时,我们获取其在链中的位置,从整个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

  1. ChainNode,带有一个对象引用,指向持有Person实例的任何对象的Person对象属性
  2. ChainNode,带有一个对象引用,指向前一个ChainNodePerson对象的Address对象属性
  3. ChainNode,带有一个对象引用,指向前一个ChainNodeAddress对象的GeoLocation对象属性

INPC 回调节点

当我们使用ChainPropertyObserver.RegisterChainLinkHandler(..)方法注册特定回调时,我们几乎遵循与之前相同的过程,只不过这次,当解析传入ChainPropertyObserver.RegisterChainLinkHandler(..)方法的lambda表达式时,我们最终会创建一个List<PropertyNode>

例如,如果我们传入一个Person.Address.GeoLocation的lambda,并且在两次调用ChainPropertyObserver.RegisterChainLinkHandler(..)方法时提供了以下内容,我们最终将在内部创建以下List<PropertyNode>

对于RegisterChainHandler(() => Person.Address)

  1. PropertyNode,带有一个对象引用,指向持有Person实例的任何对象的Person对象属性
  2. PropertyNode,带有一个对象引用,指向前一个PropertyNodePerson对象的Address对象属性

对于RegisterChainHandler(() => Person.Address.GeoLocation)

  1. PropertyNode,带有一个对象引用,指向持有Person实例的任何对象的Person对象属性
  2. PropertyNode,带有一个对象引用,指向前一个PropertyNodePerson对象的Address对象属性
  3. PropertyNode,带有一个对象引用,指向前一个PropertyNodeAddress对象的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);
}

这里有几点需要注意,即我们有一个名为MyPersonPerson类型属性,它是这个测试类,并且我们向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时,我们存储了两样东西。我们维护一个访问过的成员名称列表,这些实际上只是我们在表达式树中看到的属性的字符串名称。我们维护两个列表,因为我们使用这些相同的访问方法来创建ChainNodePropertyNode,所以我们只是将东西存储在两个内部列表中,memberSeen用于ChainNodes,propMemberSeen用于PropertyNodes。

访问常量表达式

可以看出,当我们访问ConstantExpression时,我们存储了两件事。我们维护两个对象,它们实际上只是持有我们在表达式树中看到的源对象的原始对象。我们维护两个对象,因为我们使用这些相同的访问方法来创建ChainNodePropertyNode,所以我们只是将东西存储在两个内部对象中,wrRoot用于ChainNodes,wrPropRoot用于PropertyNodes。

在访问完整个表达式树之后,我们最终会在ChainPropertyObserver中存储类似这样的内容

这假设我们在一个演示应用程序中;这就是它指向一个被WeakReference包装的WPF应用程序对象的原因。

最后发生的事情是,使用一点反射创建了一个内部ChainNode列表(它继承自NodeBase),我们利用了wrRoot WeakReferencemembersSeen List<string>。这两个信息足以让我们完全反射出实际的对象链,我们将其存储在List<ChainNode>中,该列表内部也使用WeakReferences来包装节点所代表的对象。

以下是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(..)方法也有能力创建PropertyNodes;我们稍后会讲到。

为了完整起见,这是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(..)方法时提供的原始表达式的解析方式大致相同。

还需要发生两件事

  1. 我们需要在内部Dictionary中存储一个条目,该条目表示在ChainedPropertyObserver.RegisterChainLinkHandler(..)方法中提供的handlerExpression中看到的最后一个对象。我们选择最后一个节点,因为这显然是用户在提供handlerExpression时的意图。
  2. 我们还会遍历原始的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();
}

ChainNodes被重新创建并重新分配特定回调后,我们简单地调用用户在调用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 } };

现在让我们看几个用例

  1. 如果我们有一个注册在GeoLocation上的特定监听器,并且Address被设置为null,我们将不会GeoLocation特定回调上收到回调,直到链中有一个GeoLocation对象被分配给一个非null的Address对象
  2. 如果我们有一个注册在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 对象引用

当我们第一次访问表达式树常量节点时,我们使用WeakReferences来持有常量(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项(ChainNodePropertyNode都继承自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 WeakReferenceed对象的INPC PropertyChanged事件是如何使用弱事件监听器监听的。这个弱事件监听器是由WPF同道Paul Stovell编写的。

我还在ChainNode实现的IDisposable中取消挂钩了弱事件监听器

ChainNode中还做了另一件事,那就是它还响应NodeBase NodeValue WeakReferenced对象的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如何处理这些情况,能让您安心。

如何使用它

我认为本文介绍的代码非常易于使用,基本上可以归结为这三个步骤

  1. 创建ChainPropertyObserver
  2. 这就像这样完成

    chainPropertyObserver = new ChainPropertyObserver();
  3. 创建一个你希望监控的链,并带有一个整体的更改回调
  4. 这就像这样完成

    chainPropertyObserver.CreateChain(() => MyPerson.Address.GeoLocation, Changed);
  5. (可选)为链的一部分注册一个特定的回调处理程序
  6. 这就像这样完成

    //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在链中看到空对象引用后是如何恢复的;它只是恢复了。

  1. Person 因分配了新的Address对象而改变(Person改变 = 1)
  2. Person因分配了一个新的Address对象而改变(Person改变 = 2)
  3. Person因分配了一个空Address对象而改变(Person改变 = 3)
  4. Person因分配了一个新的Address对象而改变(Person改变 = 4)
  5. Person因分配了一个空Address对象而改变(Person改变 = 5)
  6. Person因分配了一个新的Address对象而改变(Person改变 = 6)
  7. Person.Address因分配了一个新的GeoLocation对象而改变(Person.Address改变 =1)
  8. 第二个Person.Address GeoLocation更改被忽略,因为它与上一个GeoLocation对象的数据相同(Person.Address更改 =1)
  9. Person.Address.GeoLocation因分配了一个空GeoLocation对象而改变(Person改变 = 2)
  10. Person.Address因分配了一个新的GeoLocation对象而改变(纬度0.66,经度0.71)(Person.Address改变 =3)
  11. Person.Address.GeoLocationGeoLocation数据对象值变为0.51而改变(Person.Address.GeoLocation改变 =1)
  12. Person.Address.GeoLocationGeoLocation数据对象值变为0.52而改变(Person.Address.GeoLocation改变 =2)
  13. Person.Address.GeoLocationGeoLocation数据对象值变为0.53而改变(Person.Address.GeoLocation改变 =3)
  14. Person.Address.GeoLocationGeoLocation数据对象值变为0.54而改变(Person.Address.GeoLocation改变 =4)
  15. 第五个Person.Address.GeoLocation更改被忽略,因为它与前一个GeoLocation对象的数据相同(Person.Address.GeoLocation更改 =4)

那么让我们看看演示代码的截图,看看我们的预期是否属实。我们预计会发生以下更改

  • Person 改变 =6
  • Address 改变 = 3
  • GeoLocation 改变 = 4

以下是演示应用程序在调试模式下的截图,证明了这一点(单元测试会更好,抱歉)

所有更改完成后,我们看到只有6次Person更改

并且只有3次Address更改

并且有4次GeoLocation更改

这非常酷。我们只收到了我们预期的更改通知。ChainPropertyObserver完全能够处理链中用新对象替换现有对象的情况,它完全满意并知道如何处理这种情况,并且仍然向我们提供了我们注册关注的正确INPC回调。

我说这实际上非常有用。

WPF 演示

我还制作了一个更复杂的演示,将对象图绑定为{Binding Person.Address.GeoLocation},这虽然我不太喜欢,但它很好地演示了演示。WPF演示看起来像这样,它允许您手动创建更改或使用提供的几个Button来查看单击Button时预计哪些属性会更改。您应该查阅代码。

此演示包含以下INPC更改的ListBoxes,因此您可以查看手动更改或使用提供的Button时会发生什么。

正如我所说,您需要查阅代码以更好地了解单击Button时发生的更改。它已进行了非常具体的设置。

暂时就这些

我现在只想说这么多。就像我说的,我无法确定这是否真的有用或无用,但尽管如此,我认为这是一段有趣的代码,它向你展示了如何使用Lambda表达式树构建对象链,并且它还演示了一些有趣的弱事件/委托内容,因此仅凭这些原因,我很高兴发表了这篇文章……如果你喜欢它并且能看到它的用途,请告诉我,如果你非常乐意,投票/现金/啤酒/旧酸浩斯唱片和女仆总是受欢迎的……也会接受一只羊驼。

历史

  • 2011年3月9日:首次发布。
  • 2011年3月10日:更改了实现以处理链中的空对象引用。文章对此进行了讨论。
© . All rights reserved.