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

深入了解无WPF属性绑定 - 单向属性绑定(WPF概念脱离WPF - 第2部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (38投票s)

2015年7月5日

CPOL

21分钟阅读

viewsIcon

53342

downloadIcon

292

在纯C#中实现强大的属性绑定

引言

正如我之前所写,WPF(Windows Presentation Foundation)引入了许多新的编程范式,使得创建具有高度重用和关注点分离的架构成为可能。

这些新范式包括

  • 附加属性
  • 绑定
  • 递归树结构(逻辑树和视觉树)
  • 模板(数据模板和控件模板可以重用于创建和修改此类树结构)
  • 路由事件(在树结构中向上和向下传播的事件)

 

许多在WPF中广泛使用的模式都是基于这些范式构建的,例如MVVM和行为模式。

MVVM通过分离视图和视图模型来实现。视图模型代表应用程序的非可视化骨架。视图是模仿视图模型(骨架)结构和动作的可视化肉身,但比视图模型复杂得多。视图和视图模型之间的通信主要通过将视图属性绑定到视图模型属性来实现,这样当视图模型中发生变化时,视图也会随之改变,反之亦然。

有趣的是,WPF引入的范式和模式远大于WPF本身,并且可以在纯非可视化应用程序中使用。事实上,我坚信,WPF范式是编程理论上的质的飞跃,其重要性可与几十年过程式编程之后的OOP突破相媲美

我认为微软的架构师们,提出这些范式时,自己也没有意识到它们可以很容易地适应WPF世界之外的使用。

虽然WPF概念是通用的,但WPF实现却不是——它与视觉元素和WPF的单线程公寓线程模型紧密相关。

在本系列文章中,我将介绍一种通用的C#(非WPF)方式来实现上述范式。只要可能,我也尝试改进WPF的实现,使其更通用、更快。

在我之前的文章《纯C#实现WPF概念 - 第1部分 AProps和绑定介绍》中,我描述了在纯C#中、WPF之外实现附加属性(AProps)。我还展示了几个纯C#绑定功能的使用示例。

在本文中,我计划详细描述纯C#属性绑定的实现。

在下一篇文章中,我计划详细介绍纯C#绑定,包括集合绑定,并介绍用于在XAML中使用这些非WPF绑定的Bind标记扩展。

本文的计划如下:

  • 我将简要描述WPF绑定如何工作。
  • 我将提供纯C#绑定的使用示例。
  • 我将讨论简单属性获取器和设置器的实现。简单属性获取器和设置器是复合路径绑定的构建块,但也可以单独使用以创建简单绑定。
  • 将提供简单绑定用法的示例。
  • 将提供复合绑定实现的详细描述。

 

阅读本文需要一些WPF背景知识,但不是必需的,因为大多数示例都是完全非视觉化的,并且有详细解释。

WPF绑定

WPF提供以下绑定类型

  • 属性绑定
  • 多重绑定(包括PriorityBinding)。
  • 集合绑定(仅隐式)

 

让我们详细了解这些绑定

WPF属性绑定

属性绑定将源对象上指定的属性绑定到目标对象上的属性。

属性绑定的目的是确保源属性和目标属性相互模拟——它们不必相等,但它们的依赖关系可以通过绑定转换器来确定。

上图显示了属性绑定如何工作。

如您所见,图中有源对象和目标对象。绑定由两端带有箭头的虚线表示。目标对象上的属性绑定到源对象上由路径指定的属性——源属性不必直接存在于源对象上——它可以通过复杂路径指定。“断开的‘源属性路径’”线表示一个路径,其中每个线段指定一个路径链接——父对象上的一个属性。

WPF绑定有几种模式

  • OneTime - 只设置目标属性值一次。
  • OneWay - 设置目标属性以与源属性对应(如果目标改变,不改变源属性)
  • TwoWay - 当其中任何一个属性改变时,同步源和目标属性。
  • OneWayToSource - 改变源属性值以与目标对应,反之则不然。

 

请注意,当源和目标之间设置绑定时,还有一种隐式的初始行为。无论绑定模式如何,WPF绑定在绑定初始化期间始终将目标属性值设置为与源属性值同步,反之则不然。

WPF绑定设置在目标对象上,因此(除非明确移除或覆盖),其生命周期与目标对象匹配。

有3.5种指定绑定源对象的方式(顺便说一下,这是我在面试WPF职位时喜欢问的问题)

  1. 如果未明确指定源对象,绑定将假定源对象由目标对象的 DataContext 属性提供。(我称之为指定源对象的一半方式:-))
  2. 使用 Source 属性可以直接指定源(在XAML中可以使用 StaticResource 标记扩展设置 Source 属性)
  3. ElementName 属性只能在 XAML 中使用,通过它,可以将另一个 XAML 元素指定为其 Name 的绑定源对象。
  4. 可以指定 RelativeSource 属性以在视觉树或逻辑树中查找源对象。

 

WPF绑定中还有 FallbackValueTargetNullValue 的概念。如果指定,当源属性的路径不存在时,FallbackValue 将设置到目标上;如果源属性为 null,则 TargetNullValue 将设置到目标上。

所有WPF绑定的一个重要限制是,目标属性必须是依赖属性或附加属性。

WPF多重绑定

Multibinding 将多个源属性值绑定到一个目标属性值。

通常,多重绑定是从源到目标的单向绑定,但也可以是双向或从目标到源的绑定,前提是所有源值都可以从单个目标值重建。

如上图所示,多重绑定由多个单独的绑定组成,这些绑定指向(可能)不同源对象上的不同源属性。所有这些绑定结果将通过多重绑定转换器转换为单个值。

多重绑定检测到其某个源属性发生更改时,会触发目标属性更改。

WPF集合绑定

集合绑定在WPF中仅是隐式定义的。当将 ItemsControl 对象的 ItemsSource 属性绑定到 ObservableCollection<T>(或任何实现 INotifyCollectionChanged 接口的集合)时,就创建了一个集合绑定。

由此产生的可视项目集合将模仿原始的非可视集合:如果源集合中创建并插入、移动或删除了一个项目,则相应的可视项目将分别在可视集合中创建、插入、移动或删除。

新可视化项目的创建算法由 ItemTemplateItemContainerStyle 属性定义。原始的非可视化项目始终成为新可视化项目的 DataContext

纯C#绑定

引言

我在纯C#中实现了WPF属性和集合绑定的最重要的功能。在我的实现中,集合绑定变为显式的,并且可以在任何两个集合之间(也在C#代码中)执行。

我还创建了 Bind 标记扩展,用于在 XAML 中使用这些绑定。

我尚未实现纯C#多重绑定,但未来会实现。

在本文中,我将只讨论纯属性绑定的实现。

在某些方面,我的绑定实现比WPF实现更通用。

  • 我的实现不要求目标属性是附加属性或AProp(有关AProp的定义,请参阅纯C#实现WPF概念 - 第1部分 AProps和绑定介绍)。目标属性可以是简单的C#属性。
  • 目标属性不必属于目标对象——它可以通过相对于目标对象的路径给出。
  • 绑定不要求与目标对象绑定——它可以独立存在。

 

然而,我的实现中缺少WPF绑定的一个重要特性——指定树结构中源对象的能力——这由 RelativeSource 属性提供。我计划将来添加此功能。

纯属性绑定示例

让我们从上一篇文章中的示例开始,现在我们将更多地讨论绑定的实现方式。

此示例的代码位于 PropertyBindingTests 解决方案下。

这是 Program.Main() 方法的源代码

public static void Main()
{
    // create the Contact object (binding's source object)
    Contact joeContact = new Contact
    {
        FirstName = "Joe",
        LastName = "Doe",

        HomeAddress = new Address { City = "Boston" },
    };

    // Create the PrintModel (binding's target object)
    PrintModel printModel = new PrintModel();
    Console.WriteLine("Before binding the printModel's Home City is null");
    printModel.Print();

    //create the binding
    OneWayPropertyBinding<object, object> homeCityBinding = new OneWayPropertyBinding<object, object>();

    // set the binding's links from the source object to the source property are "HomeAddress" and "City"
    CompositePathGetter<object> homeCitySourcePathGetter =
        new CompositePathGetter<object>
        (
            new BindingPathLink<object>[]
            {
                new BindingPathLink<object>("HomeAddress"),
                new BindingPathLink<object>("City"),
            },
            null
        );

    homeCitySourcePathGetter.TheObj = joeContact;
    homeCityBinding.SourcePropertyGetter = homeCitySourcePathGetter;

    // set the binding's links from the target object to the target property are "HomeCityPrintObj" and "PropValueToPrint"
    CompositePathSetter<object> homeCityTargetPathSetter = new CompositePathSetter<object>
    (
        new BindingPathLink<object>[]
        {
            new BindingPathLink<object>("HomeCityPrintObj"),
            new BindingPathLink<object>("PropValueToPrint")
        }
    );

    homeCityTargetPathSetter.TheObj = printModel;
    homeCityBinding.TargetPropertySetter = homeCityTargetPathSetter;

    // do the actual binding between the source and the target
    // by calling Bind() method on the binding.
    homeCityBinding.Bind();


    Console.WriteLine("\nAfter binding the printModel's Home City is Boston");
    printModel.Print();

    joeContact.HomeAddress.City = "Brookline";

    Console.WriteLine("\nHome City change is detected - now Home City is Brookline");
    printModel.Print();

    joeContact.HomeAddress = new Address { City = "Allston" };

    Console.WriteLine("\nHome Address change is detected - now Home City is Allston");
    printModel.Print();


    printModel.HomeCityPrintObj = new PrintProp("Home City");
    Console.WriteLine("\nWe change the whole target link, but the binding keeps the target up to date:");
    printModel.Print();
}  

这是运行此示例时得到的结果

Before binding the printModel's Home City is null
Home City: null

After binding the printModel's Home City is Boston
Home City: Boston

Home City change is detected - now Home City is Brookline
Home City: Brookline

Home Address change is detected - now Home City is Allston
Home City: Allston

We change the whole target link, but the binding keeps the target up to date:
Home City: Allston  

示例绑定的源是 Contact 类型的 joeContact 对象。它有一个 Address 类型的 HomeAddress 属性。Address 又有一个 string 类型的 City 属性——这就是我们绑定的源属性。

绑定的目标对象是 PrintModel 类型的 printModel。它有一个 PrintProp 类型的 HomeCityPrintObj 属性。PrintProp 有一个 object 类型的 PropValueToPrint 属性,这就是我们绑定的目标属性。

因此,我们的绑定将 joeContact 对象上的 HomeAddress.City 属性绑定到 printModel 对象上的 HomeCityPrintObj.PropValueToPrint 属性。

请注意,与WPF绑定不同,此绑定可以将普通(既非附加也非AProp)属性作为其目标——我们上面展示的所有属性都是普通属性。

另请注意,虽然WPF绑定要求目标属性必须在目标对象上定义,但我们允许从目标对象到目标属性有一个复杂的路径——在我们的例子中,该路径由两个链接组成。

我们绑定中使用的一些属性会触发 INotifyPropertyChanged.PropertyChanged 事件。然而,这不是必需的——只有当我们希望在相应属性更改时能够更改目标属性时才需要。在我们的示例中,所有源属性和目标路径中第一个链接对应的目标属性 HomeCityPrintObj 都会在更改时触发该事件。这意味着无论链中的任何部分发生更改,都会检测到任何更改。

特别是,在此示例中,我们展示了当我们更改 joeContact.HomeAddress.City 为另一个字符串("Brookline")或将整个 joeContact.HomeAddress 更改为另一个 Address 时,目标属性会发生变化。

  joeContact.HomeAddress = new Address { City = "Allston" };

即使我们改变了目标的 HomeCityPrintObj

  printModel.HomeCityPrintObj = new PrintProp("Home City");

新的 HomeCityPrintObj 通过绑定为其 PropValueToPrint 属性获取正确的值。

如果源路径或目标路径中的一个(或某些)链接没有触发 PropertyChanged 事件,绑定仍然能够检测到所有其余链接的更改。

绑定实现

核心绑定类是 OneWayPropertyBinding<SourcePropertyType, TargetPropertyType>

如果源和目标属性类型未知,可以使用类型中立的子类

public class OneWayPropertyBinding : OneWayPropertyBinding<object, object>  

我们将主要使用类型中立的实现,因为对于复杂路径,可能难以预测所有链接的类型,而且我们为绑定提供了XAML标记扩展,而众所周知XAML无法很好地处理泛型(不幸的是)。

由于我们的绑定不必在目标对象上定义,我们可以通过提供两个 OneWayBinding 对象来模拟双向绑定,一个从源到目标,另一个从目标到源。通过在属性设置器中检查相等性来防止无限循环。

让我们来看看绑定类的定义

public class OneWayPropertyBinding<SourcePropertyType, TargetPropertyType> : 
        IPropertyBinding<SourcePropertyType, TargetPropertyType>, 
        IBinding, 
        IRegistrableObject  

它实现了三个接口:IPropertyBinding<SourcePropertyType, TargetPropertyType>IBindingIRegisrableObject

我们稍后将讨论 IRegistableObject 的必要性,而其他两个接口现在将进行解释。

IBinding 是一个通用绑定接口——集合和属性绑定类都实现了它。

public interface IBinding : IRegistrableObject
{
    // specifies initial synchronization between the binding source and target
    void InitialSync();

    // creates the binding
    void Bind(bool doInitialSync = true);

    // removes the binding
    void UnBind(); 
} 

它提供了上面注释中描述的三个方法。

InitialSync() 方法允许在绑定创建时指定初始同步——通常在初始同步期间,目标属性从源属性获取其值,但也可以反之。

方法 Bind(...) 的参数 doInitialBinding 允许在不需要初始同步时将其关闭。这在双向绑定(如上所述)由两个单向绑定组成的情况下很重要——我们不希望反向绑定也执行初始同步。

现在让我们来看看 IPropertyBinding 接口

public interface IPropertyBinding<SourcePropertyType, TargetPropertyType> 
{
    // used for source property value retrieval
    IObjWithPropGetter<SourcePropertyType> SourcePropertyGetter { get; set; }

    // used for target property value setting
    IObjWithPropSetter<TargetPropertyType> TargetPropertySetter { get; set; }
} 

该接口有两个属性——用于检索源值的 SourcePropertyGetter 和用于设置目标属性值的 TargetPropertySetter,分别定义为 IObjWithPropGetterIObjWithPropSetter 接口。

让我们深入研究这些接口

public interface IObjWithPropGetter<PropertyType> : 
    IObjectSetter, 
    IPropGetter<PropertyType>
{
    // returns true is the object is set, false otherwise
    bool HasObj
    {
        get;
    }
}  

IObjWithPropGetter 派生自两个非常简单的接口 IObjectSetterIPropGetter。让我们看看它们

public interface IPropGetter<PropertyType>
{
    // fires when the property changes
    // its argument is new property value
    event Action<PropertyType> PropertyChangedEvent;

    // forces PropertyChangedEvent to fire
    // (it is needed e.g. when when two properties
    // are bound - the source property should 
    // trigger the target property change even
    // if the source property does not change)
    void TriggerPropertyChanged();
}  

它包含 PropertyChangedEvent,当属性更改时应该触发,以及 TriggerPropertyChanged() 方法,无论何时调用它,都可以强制触发事件(即使属性当时没有更改)。

这是 IObjectSetter 的代码

public interface IObjectSetter
{
    // sets TheObj property
    object TheObj
    {
        set;
    }
}   

通常,当 TheObj 更改或其属性更改时,IObjWithPropGetter 会触发 PropertyChangedEvent,并将其对象的属性值传递给它。这样,我们就能同时处理初始同步和属性更改。

现在让我们看看 IObjWithPropSetter 接口

public interface IObjWithPropSetter : IObjectSetter, IPropSetter
{

}  

它派生自 IObjectSetter(上面已解释)和 IPropSetter 接口。

public interface IPropSetter
{
    // sets the target property
    void Set(PropertyType property);
}  

Set 方法设置目标属性,但仅当目标对象存在且不为 null 时。

现在,让我们看看这些接口的一些实现。

简单属性获取器和设置器的实现

在本小节中,我将描述简单属性获取器和设置器的实现——那些对应于单个链接路径的实现。它们可以单独使用,但也可以用作复合获取器和设置器(用于复杂源和目标路径)的构建块。复合属性获取器和设置器将在下面描述。

有两个抽象类 GenericSimplePropWithDefaultGetter<PropertyType>GenericSimplePropSetter<PropertyType>,许多上述接口的实现都派生自它们。这些类提供了所有获取器和设置器通用的实现功能。

public abstract class GenericSimplePropWithDefaultGetter<PropertyType> : 
    IObjWithPropGetter<PropertyType>
{
    // default value to be passed to PropertyChangedEvent
    // when the
    PropertyType _defaultValue;

    // called to clear the 'old' object
    // when TheObj property changes
    protected abstract void OnObjUnset();

    // Called to set the events etc on the 'new'
    // object when TheObj property changes
    protected abstract void OnObjSet();

    object _obj;
    public object TheObj
    {
        protected get
        {
            return _obj;
        }

        set
        {
            if (_obj == value)
                return;

            if (HasObj) // clear the old object
            {
                // clear the event handlers from
                // the 'old' object before overriding it
                OnObjUnset();
            }

            _obj = value;

            if (HasObj)
            {
                // set the event handlers
                // on the 'new' object after it 
                // has been set
                OnObjSet();
            }

            TriggerPropertyChanged();
        }
    }

    public bool HasObj
    {
        get
        {
            return _obj != null;
        }
    }

    public GenericSimplePropWithDefaultGetter
    (
        PropertyType defaultValue = default(PropertyType)
    )
    {
        _defaultValue = defaultValue;
    }

    ~GenericSimplePropWithDefaultGetter()
    {
        //this.TheObj = null;
    }

    // Event from IPropGetter interface
    // that signifies that the property was changed on
    // an object or that the object was changed itself
    public event Action<PropertyType> PropertyChangedEvent;

    // gets Property Value from an object
    public abstract PropertyType GetPropValue();

    // Triggers the PropertyChangedEvent firing 
    // passing to it the corresponding property value if 
    // HasObj is true or _defaultValue otherwise
    public void TriggerPropertyChanged()
    {
        if (PropertyChangedEvent != null)
        {
            PropertyType propValue;

            if (HasObj)
            {
                // if object exists - get the property value from the object
                propValue = GetPropValue();
            }
            else
            {
                // if the object does not exist set the propValue to default
                propValue = _defaultValue;
            }

            // fire the PropertyChangedEvent event
            // passing propValue to it
            PropertyChangedEvent(propValue);
        }
    }
}  

HasObj 属性检查 TheObj 是否为 null。

TriggerPropertyChanged() 是该类最重要的函数——它触发 PropertyChangedEvent,并向其传递正确的属性值。如果 HasObjfalse,则传递默认值(可以在类构造函数中设置,或默认设置为 default(PropertyType))。如果 HasObjtrue,则使用 GetPropValue() 抽象方法从对象中获取属性值。

类中还有两个抽象方法:OnObjUnset()OnObjSet()。它们都在 TheObj 属性设置器中被调用。OnObjUnset() 在“旧”对象被覆盖之前清除其事件处理程序,而 OnObjSet() 在“新”对象上设置事件处理程序。

object _obj;
public object TheObj
{
    ...
    set
    {
        if (_obj == value)
            return;

        if (HasObj) // clear the old object
        {
            // clear the event handlers from
            // the 'old' object before overriding it
            OnObjUnset();
        }

        _obj = value;

        if (HasObj)
        {
            // set the event handlers
            // on the 'new' object after it 
            // has been set
            OnObjSet();
        }

        TriggerPropertyChanged();
    }
}  

现在让我们看看 GenericSimplePropSetter<PropertyType>

public abstract class GenericSimplePropSetter<PropertyType> :
    IObjWithPropSetter<PropertyType>
{
    // keeps last value for set by the binding
    // so that if the object is temporarily set to null
    // and then reset to another object, 
    // the binding's property value could be set on it.
    ValueHolder<PropertyType> _lastValueHolder = new ValueHolder<PropertyType>();

    // clear the event handlers from
    // the 'old' object before overriding it
    protected virtual void OnObjUnset() {}

    // set the event handlers
    // on the 'new' object after it 
    // has been set
    protected abstract void OnObjSet();

    // Set the 
    protected abstract void SetPropValue(PropertyType propValue);

    object _obj;
    public object TheObj
    {
        protected get
        {
            return _obj;
        }
        set
        {
            if (Object.ReferenceEquals(_obj, value))
                return;

            // clear the event handlers from
            // the 'old' object before overriding it
            OnObjUnset();

            _obj = value;

            // set the event handlers
            // on the 'new' object after it 
            // has been set
            OnObjSet();

            // if the binding value has been set before,
            // set it on the newly assigned object
            if (_lastValueHolder.HasBeenSet)
                Set(_lastValueHolder.TheValue);
        }
    }

    // constructor that can set TheObj property
    public GenericSimplePropSetter(object obj = null) 
    {
        TheObj = obj;
    }

    // implementation of IPropSetter<PropertyType>.Set
    // calls SetPropValue to set the Binding
    // value on TheObj
    public void Set(PropertyType propValue)
    {
        // nothing is set if there is no object
        if (_obj != null)
        {
            // set the property value on TheObj object
            SetPropValue(propValue);
        }

        // set TheValue on _lastValueHolder
        // to keep it in case TheObj changes
        _lastValueHolder.TheValue = propValue;
    }
}  

此类的主要方法是 Set(PropertyType propValue)。如果 TheObj 不为空,它会调用抽象方法 SetPropertyValue(propValue) 来设置对象的属性值。无论如何(即使 TheObj 为空),它都会将绑定目标值设置到 _lastValueHolder 对象上,当 TheObj 被设置时,它会将其设置到 TheObj 上。

_lastValueHolder 用于在 TheObj 对象更改或为 null 时保存属性值。

就像上面描述的Getter类一样,当 TheObj 属性更改时,抽象方法 OnObjUnset()OnObjSet() 被调用,以分别取消设置“旧”对象并设置“新”对象。

public object TheObj
{
    ...
    set
    {
        if (Object.ReferenceEquals(_obj, value))
            return;

        if (_obj != null)
        {
            // clear the event handlers from
            // the 'old' object before overriding it
            OnObjUnset();
        }

        _obj = value;

        if (_obj != null)
        { 
            // set the event handlers
            // on the 'new' object after it 
            // has been set
            OnObjSet();
        }

        // if the binding value has been set before,
        // set it on the newly assigned object
        if (_lastValueHolder.HasBeenSet)
            Set(_lastValueHolder.TheValue);
    }
}  

现在让我们看看 GenericSimplePropWithDefaultGetter<PropertyType>GenericSimplePropSetter<PropertyType> 的各种子类。

我们来看看 PlainPropGetterAndSetter.cs 文件。它包含许多用于普通(即非AProp)获取器和设置器的获取器和设置器类。

NotifiedPropWithDefaultGetter<PropertyType> 是所有“普通”属性获取器和设置器的抽象基类。它被称为 NotifiedPropWithDefaultGetter,因为它使实现 INotifyPropertyChanged 接口的对象在触发 INotifyPropertyChanged.PropertyChanged 事件时调用其 TriggerPropertyChanged() 方法。

public abstract class NotifiedPropWithDefaultGetter<PropertyType> 
    : GenericSimplePropWithDefaultGetter<PropertyType>
{
    string _propName;
    Func<object, object> _propertyGetter = null;

    // returns INotifyPropertyChanged object if 
    // TheObj implements the interface, otherwise return null,
    INotifyPropertyChanged NotifyingObj
    {
        get
        {
            return TheObj as INotifyPropertyChanged;
        }
    }

    // if the object implements INotifyPropertChanged
    // it removes the PropertyChanged event handler obj_PropertyChanged
    // from it.
    protected override void OnObjUnset()
    {
        _propertyGetter = null;

        if (NotifyingObj != null)
        {
            NotifyingObj.PropertyChanged -= obj_PropertyChanged;
        }
    }

    // abtstract function that should return a property getter
    protected abstract Func<object, object> GetPropGetter(object obj, string propName);

    // if the object implements INotifyPropertChanged
    // it adds a PropertyChanged event handler obj_PropertyChanged
    // to it.
    protected override void OnObjSet()
    {
        _propertyGetter = GetPropGetter(TheObj, _propName);

        if (NotifyingObj != null)
            NotifyingObj.PropertyChanged += obj_PropertyChanged;
    }

    // checks if the name of the property that triggered
    // PropertyChanged event on the NotifyingObj, is the 
    // same as that of this property getter. 
    // If yes, it fires TriggerPropertyChanged method.
    void obj_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName != _propName)
            return;

        TriggerPropertyChanged();
    }

    // sets the property name and the default 
    // value to return
    public NotifiedPropWithDefaultGetter
    (
        string propName,
        PropertyType defaultValue = default(PropertyType)
    ) : base(defaultValue)
    {
        _propName = propName;
    }


    // returns the property valus
    public override PropertyType GetPropValue()
    {
        return (PropertyType)_propertyGetter(TheObj);
    }
}  

该类将 obj_PropertyChanged() 事件处理程序附加到 TheObj 对象的 INotifyPropertyChanged.PropertyChanged 事件(如果 TheObj 实现了此类接口)。然后,当 TheObj 触发该事件时,它会将属性的名称与其 _propName 字段进行比较,如果它们相同,则调用 TriggerPropertyChanged() 方法。

抽象函数 GetPropValue 被重写以调用 TheObj 上的 _propertyGetter,并且 _propertyGetter 委托由 GetPropGetter(...) 抽象方法返回。

// abtstract function that should return a property getter
protected abstract Func<object, object> GetPropGetter(object obj, string propName);  

代码中提供了此类的两个具体实现:PlainPropWithDefaultGetter<PropertyType>MapPropWithDefaultGetter<PropertyType>。前者从对象获取普通属性,后者假定对象为 IDictionary,属性名是键,并根据键返回对应的值。MapPropWithDefaultGetter<PropertyType> 实现可用于例如实现动态属性的绑定。

这些获取器也有相应的设置器类:PlainPropertySetter<PropertyType>MapPropertySetter<PropertyType>。它们都派生自 PlayPropertySetterBase<PropertyType> 类。

public abstract class PlainPropSetterBase<PropertyType> : GenericSimplePropSetter<PropertyType>
{
    Action<object, object> _propertySetter = null;

    string _propName;
    public string ThePropName
    {
        get
        {
            return _propName;
        }
    }

    // sets the property setter based on the object
    protected override void OnObjSet()
    {
        if (TheObj == null)
            return;

        _propertySetter = GetPropSetter(TheObj, _propName);
    }

    // returns the property setter delegate
    protected abstract Action<object, object> GetPropSetter(object obj, string propName);

    public PlainPropSetterBase(string propName) : base()
    {
        Init(propName);
    }

    public PlainPropSetterBase(object obj, string propName)
        : base(obj)
    {
        Init(propName);
    }

    // invokes the property setter delegate to 
    // set the property value on TheObj
    protected override void SetPropValue(PropertyType propValue)
    {
        if (TheObj == null)
            return;

        _propertySetter(TheObj, propValue);
    }

    void Init(string propName)
    {
        _propName = propName;
    }

}  

PlayPropertySetterBase<PropertyType> 类定义了一个抽象方法

protected abstract Action<object, object> GetPropSetter(object obj, string propName);  

它返回一个用于设置对象属性的委托。

这是 PlainPropWithDefaultGetter<PropertyType> 的代码

public class PlainPropWithDefaultGetter<PropertyType> : 
    NotifiedPropWithDefaultGetter<PropertyType>
{
    Func<object, object> _currentPropGetter = null;

    public PlainPropWithDefaultGetter
    (
        string propName,
        PropertyType defaultValue = default(PropertyType)
    )
        : base(propName, defaultValue)
    {

    }

    Type _oldObjType = null;
    // returns the property getter delegate
    protected override Func<object, object> GetPropGetter(object obj, string propName)
    {
        if (obj == null)
            return null;

        Type newObjType = obj.GetType();

        // only change the property getter if the
        // newObjType is different from the _oldObjType
        if (newObjType != _oldObjType)
        {
            // this is an expesive operation involving 
            // compiling a LINQ  expression 
            _currentPropGetter = CompiledExpressionUtils.GetUntypedCSPropertyGetter(obj, propName);
            _oldObjType = newObjType;
        }

        return _currentPropGetter;
    }
}  

该类主要做的事情是实现抽象函数 GetPropSetter(...)。该实现检查 TheObj 类型是否已更改,如果已更改,则调用 CompiledExpressionUtils.GetUntypedCSPropertyGetter(obj, propName) 函数,该函数通过创建LINQ表达式并编译它来返回属性获取器委托。对于那些感兴趣它如何完成的人,这里是链接 基于表达式的属性获取器和设置器。我使用表达式而不是反射的原因是基于表达式的获取器和设置器速度快得多,正如您从上面参考中看到的那样。

MapPropwithDefaultGetter<PropertyType> 甚至更简单。

public class MapPropWithDefaultGetter<PropertyType> : NotifiedPropWithDefaultGetter<PropertyType>
{
    Func<object, object> _mapPropGetter = null;
    public MapPropWithDefaultGetter
    (
        string propName,
        PropertyType defaultValue = default(PropertyType)
    )
        : base(propName, defaultValue)
    {
        _mapPropGetter = (theObj) =>
        {
            IDictionary mapPropContainer = theObj as IDictionary;

            return mapPropContainer[propName];
        };

    }
    protected override Func<object, object> GetPropGetter(object obj, string propName)
    {
        return _mapPropGetter;
    }
}  

它的 GetPropGetter(...) 方法返回一个从 IDictionary 返回值的委托。

现在让我们来看看设置器。这是 PlainPropertySetter<PropertyType> 的代码。

public class PlainPropertySetter<PropertyType> : PlainPropSetterBase<PropertyType>
{
    Action<object, object> _currentSetter = null;
    Type _oldObjType = null;
    protected override Action<object, object> GetPropSetter(object obj, string propName)
    {
        if (obj == null)
            return null;

        Type newObjType = obj.GetType();

        if (newObjType != _oldObjType)
        {
            // for speed's sake change the setter only if the new object type is different
            // this is an expensive operation 
            // that involves compiling a LINQ Expression
            _currentSetter = CompiledExpressionUtils.GetUntypedCSPropertySetter(obj, propName);
            _oldObjType = newObjType;
        }

        return _currentSetter;
    }

    public PlainPropertySetter(string propName)
        : base(propName)
    {
    }

    public PlainPropertySetter(object obj, string propName) : base(obj, propName)
    {
    }
}  

我们再次使用 CompiledExpressionUtils 方法 GetUntypedCSPropertySetter 通过编译 LINQ 表达式返回一个属性设置器委托。

这是 MapPropertySetter<PropertyType> 的代码

public class MapPropertySetter<PropertyType> : PlainPropSetterBase<PropertyType>
{
    Action<object, object> _mapPropertySetter = null;

    protected override Action<object, object> GetPropSetter(object obj, string propName)
    {
        return _mapPropertySetter;
    }

    void Init()
    {
        _mapPropertySetter = (theObj, propValue) =>
        {
            IDictionary map = theObj as IDictionary;

            map[ThePropName] = propValue;
        };
    }

    public MapPropertySetter(string propName)
        : base(propName)
    {
        Init();
    }

    public MapPropertySetter(object obj, string propName)
        : base(obj, propName)
    {
        Init();
    }
}  

属性设置器在 IDictionary 上设置键值对,其中键是属性名称,值是我们想要设置的值。

现在看看 APropGetterAndSetter.cs 文件。关于 AProp 是什么的解释,请参阅我之前的文章 纯 C# 实现 WPF 概念 - 第 1 部分 AProps 和绑定介绍

APropGetterAndSetter.cs 文件中定义了三个类。其中一个 APropGetter<PropertyType> 已被弃用,这里不再讨论。

APropWithDefaultGetter<PropertyType> 派生自 GenericSimplePropWithDefaultGetter<PropertyType>,就像上面讨论的普通属性类一样。

public class APropWithDefaultGetter<PropertyType> : GenericSimplePropWithDefaultGetter<PropertyType>
{
    AProp<object, PropertyType> _aProp;

    protected override void OnObjUnset()
    {
        // unset AProp individual object property change handler
        _aProp.RemoveOnPropertyChangedHandler(TheObj, OnPropChanged);
    }

    protected override void OnObjSet()
    {
       // set AProp individual object property change handler
       _aProp.AddOnPropertyChangedHandler(TheObj, OnPropChanged);
    }

    public APropWithDefaultGetter
    (
        AProp<object, PropertyType> aProp, 
        PropertyType defaultValue = default(PropertyType) 
    )
        : base(defaultValue)
    {
        _aProp = aProp;
    }

    void OnPropChanged(object obj, PropertyType oldValue, PropertyType newValue)
    {
        TriggerPropertyChanged();
    }

    // returns the AProp value of TheObj
    public override PropertyType GetPropValue()
    {
        return _aProp.GetProperty(TheObj);
    }
}  

实现的 OnObjSet()OnObjUnset() 方法在对象上设置和取消设置单独的 AProp 更改处理程序。GetPropValue() 的实现返回对象上的 AProp 值。

public override PropertyType GetPropValue()
{
    return _aProp.GetProperty(TheObj);
}  

APropSetter<PropertyType> 派生自 GenericSimplePropSetter<PropertyType>

public class APropSetter<PropertyType> :
    GenericSimplePropSetter<PropertyType>
{
    protected override void OnObjSet()
    {
    }

    protected override void SetPropValue(PropertyType propValue)
    {
        // set the AProp on the object
        _aProp.SetProperty(TheObj, propValue);
    }

    AProp<object, PropertyType> _aProp;

    void Init(AProp<object, PropertyType> aProp)
    {
        _aProp = aProp;
    }

    public APropSetter(AProp<object, PropertyType> aProp) : base()
    {
        Init(aProp);
    }

    public APropSetter(object obj, AProp<object, PropertyType> aProp) : base(obj)
    {
        Init(aProp);
    }
}  

使用简单设置器和获取器的示例

我们上面看到的获取器和设置器被称为“简单”,因为它们只获取或设置对象上的属性(或AProp)。下面我们将描述复合获取器和设置器,它们可以用于复杂路径。它们使用简单获取器和设置器作为链接,并允许检测属性更改或通过复杂路径设置属性——例如,可以检测源对象的AProp的普通属性上的属性更改。

演示使用简单获取器和设置器的绑定的项目名为 BindingsWithSimpleGettersAndSettersTests。这是包含测试的 Program.Main() 方法。

static void Main()
{
    #region Plain Property to Plain Property Binding
    Console.WriteLine("Plain Prop to Plain Prop Binding Test");
    Address address = new Address { City = "Boston" };

    PrintProp printProp = new PrintProp("City");

    OneWayPropertyBinding<string, object> cityBinding = new OneWayPropertyBinding<string, object>();

    cityBinding.SourcePropertyGetter = new PlainPropWithDefaultGetter<string>("City") { TheObj = address };
    cityBinding.TargetPropertySetter = new PlainPropertySetter<object>("PropValueToPrint") { TheObj = printProp };

    Console.WriteLine("Before binding is set the City property should be null on printProp object");
    printProp.Print();

    // bind the source to the target
    cityBinding.Bind();
    Console.WriteLine("After binding is set the City property should be 'Boston' on printProp object");
    printProp.Print();

    address.City = "Brookline";
    Console.WriteLine("After source's property was changed to 'Brookline', the target property also changes");
    printProp.Print();
    #endregion Plain Property to Plain Property Binding

    #region AProp to Plain Property Binding
    Console.WriteLine();
    Console.WriteLine();
    Console.WriteLine("AProp to Plain Prop Binding Test");

    AProp personFirstNameAProp = new AProp("Unknown"); // default is "Unknown"

    object personData = new object();

    Person person = new Person();

    OneWayPropertyBinding firstNameBinding = new OneWayPropertyBinding();
    firstNameBinding.SourcePropertyGetter =
        new APropWithDefaultGetter<object>(personFirstNameAProp, "No Name Given") { TheObj = personData };

    firstNameBinding.TargetPropertySetter = 
        new PlainPropertySetter<object>("FirstName") { TheObj = person };

    Console.WriteLine("Before the binding, the First name should be null:");

    if (person.FirstName == null)
    {
        Console.WriteLine("null");
    }
    else
    {
        Console.WriteLine(person.FirstName);
    }

    firstNameBinding.Bind();
    Console.WriteLine("After the binding, the First name should set to the AProp's default value - 'Unknown':");
    Console.WriteLine(person.FirstName);

    personFirstNameAProp.SetProperty(personData, "John");

    Console.WriteLine("After setting the AProp on the source object to 'John' the target property should be set also:");
    personFirstNameAProp.SetProperty(personData, "John");
    Console.WriteLine(person.FirstName);

    #endregion AProp to Plain Property Binding
}  

这是运行此示例后获得的打印输出

Plain Prop to Plain Prop Binding Test
Before binding is set the City property should be null on printProp object
City: null
After binding is set the City property should be 'Boston' on printProp object
City: Boston
After source's property was changed to 'Brookline', the target property also changes
City: Brookline


AProp to Plain Prop Binding Test
Before the binding, the First name should be null:
null
After the binding, the First name should set to the AProp's default value - 'Unknown':
Unknown
After setting the AProp on the source object to 'John' the target property should be set also:
John  

我们演示了两个绑定——一个将 Address 类型对象上的普通属性 City 连接到 PrintProp 类型对象上的另一个普通属性 PropValueToPrint

另一个绑定将源 AProp personFirstNameAProp 连接到普通 object personData = new object()。连接到 Person 类型对象上的普通属性 FirstName

当然,我们也可以将 AProp 作为目标,将普通属性作为源,或者我们可以使用两个 AProp,一个作为目标,另一个作为源。

我们还可以使用基于字典(Map)的属性作为源和目标,同时使用我们上面描述的相应获取器和设置器。这对于动态对象非常方便。

复合属性绑定

这里我们将描述如何定义具有复合属性获取器和设置器的绑定。事实上,本文中的第一个示例就是这种绑定的一个例子。源属性是 Contact 类型源对象上的 HomeAddres 属性上的 City 属性。目标属性是 PrintModel 类型目标对象上的 HomeCityPrintObj 属性上的 PropValueToPrint 属性。

在这里,我提供另一个复合绑定的示例——源路径和目标路径中都有一个普通属性和一个AProp。在介绍示例之后,我将描述复合属性获取器和设置器的实现。

此复合绑定示例位于 CompositeBindingTests 项目下。这是它的 Program.Main() 方法

public static void Main()
{
    AProp<object, object> workAddress = new AProp<object, object>(null);

    object sourceObject = new object();

    Address sourceAddress = new Address { City = "Boston" };

    workAddress.SetProperty(sourceObject, sourceAddress);

    AProp<object, object> secondWorkCity = new AProp<object, object>("Unknown");

    Contact targetObject = new Contact
    {
        FirstName = "Joe",
        LastName = "Smith",
        WorkAddress = new Address()
    };

    OneWayPropertyBinding addressBinding = new OneWayPropertyBinding();

    addressBinding.SourcePropertyGetter = new CompositePathGetter<object>
    (
        new BindingPathLink<object>[]
        {
            new BindingPathLink<object> { TheAProp = workAddress}, // AProp link
            new BindingPathLink<object> { PropertyName = "City" }  // Plain property link 
        },
        "Unknown City" // default
    )
    { TheObj = sourceObject };

    addressBinding.TargetPropertySetter = new CompositePathSetter<object>
    (
        new BindingPathLink<object>[]
        {
             new BindingPathLink<object> { PropertyName = "WorkAddress" }, // Plain property link 
             new BindingPathLink<object> { TheAProp = secondWorkCity}  // AProp link
        }
    )
    { TheObj = targetObject };

    Console.WriteLine("Before the binding the target value is AProp's default - 'Unknown'");
    Console.WriteLine(secondWorkCity.GetProperty(targetObject.WorkAddress));

    addressBinding.Bind();

    Console.WriteLine("After the binding the target value is 'Boston'");
    Console.WriteLine(secondWorkCity.GetProperty(targetObject.WorkAddress));


    sourceAddress.City = "Brookline";
    Console.WriteLine("After the source value changed to 'Brookline' the target changes also");
    Console.WriteLine(secondWorkCity.GetProperty(targetObject.WorkAddress));
}

这是此示例的输出

Before the binding the target value is AProp's default - 'Unknown'
Unknown
After the binding the target value is 'Boston'
Boston
After the source value changed to 'Brookline' the target changes also
Brookline  

如您所见——我们将 sourceObject 上的 workAddress AProp 的 City 属性绑定到 targetObjectContact 类型)的 WorkAddress 普通属性上的 secondWorkCity AProp。

换句话说,我们将源对象上AProp定义的普通属性绑定到目标对象上普通属性定义的AProp,而我们的复合绑定仍然可以处理它。

BindingPathLink 对象用于指定属性的类型——对于普通属性,我们只需将属性名指定为字符串,而对于 AProp,我们将 BindingPathLinkTheAProp 属性设置为 AProp 对象,例如

addressBinding.SourcePropertyGetter = new CompositePathGetter<object>
(
    new BindingPathLink<object>[]
    {
        new BindingPathLink<object> { TheAProp = workAddress}, // AProp link
        new BindingPathLink<object> { PropertyName = "City" }  // Plain property link 
    },
    "Unknown City" // default
)  

请注意,不应同时设置 BindingPathLink 上的 TheAPropPropertyName 属性。

复合获取器和设置器实现

复合获取器和设置器类定义在 NP.Paradigms 项目下的 CompositePathPropertyGetterAndSetter.cs 文件中。

这是 CompositePathGetter<PropertyType> 的代码

public class CompositePathGetter<PropertyType> : IObjWithPropGetter<PropertyType>
{
    object _defaultValue;

    public event Action<PropertyType> PropertyChangedEvent;

    // Call TriggerPropertyChanged() method
    // on the last property getter. This will
    // trigger PropertyChangedEvent firing on the 
    // CompositePathGetter object itself, passing
    // a correct property value to it.
    public void TriggerPropertyChanged()
    {
        if (PropertyChangedEvent == null)
            return;

        LastPropGetter.TriggerPropertyChanged();
    }

    // Last property getter in the chain.
    IObjWithPropGetter<object> LastPropGetter
    {
        get
        {
            return _propGetters[_propGetters.Count - 1];
        }
    }

    IEnumerable<BindingPathLink<object>> _pathLinks;

    List<IObjWithPropGetter<object>> _propGetters = new List<IObjWithPropGetter<object>>();
    public List<IObjWithPropGetter<object>> PropGetters
    {
        get
        {
            return _propGetters;
        }
    }

    public CompositePathGetter(IEnumerable<BindingPathLink<object>> pathLinks, object defaultValue)
    {

        _pathLinks = pathLinks;

        _defaultValue = defaultValue;

        // create the first property getter in the chain to be 
        // of type SimplePropGetter that returns TheObj itself as the property value.
        IObjWithPropGetter<object> previousPropGetter = new SimplePropGetter<object>();
        _propGetters.Add(previousPropGetter);

        // we are creating a chain of Property Getters according to the path links. 
        // when the property on the previous property getter changes, 
        // the new property value becomes TheObj (object) of the next property getter
        foreach (var pathLink in _pathLinks)
        {
            IObjWithPropGetter<object> propGetter = pathLink.GetPropertyGetter();

            _propGetters.Add(propGetter);

            if (previousPropGetter != null)
            {
                previousPropGetter.PropertyChangedEvent += (obj) =>
                    {
                        propGetter.TheObj = obj;
                    };
            }

            previousPropGetter = propGetter;
        }

        // connect PropertyChangeEvent of the last getter
        // to fire the PropertyChangedEvent of the 
        // whole CompositePathGetter object. 
        // If LastPropGetter does not have an object, 
        // we pass the _defaultValue as the argument
        // to the event, otherwise we pass the real value. 
        previousPropGetter.PropertyChangedEvent += (propVal) =>
            {
                if (this.PropertyChangedEvent == null)
                    return;

                if (!LastPropGetter.HasObj)
                {
                    PropertyChangedEvent( (PropertyType) _defaultValue);
                }
                else
                {
                    PropertyChangedEvent((PropertyType)propVal);
                }
            };
    }


    object _obj;
    public object TheObj
    {
        set
        {
            if (_obj == value)
                return;

            _obj = value;

            // set the first of the prop getters in the chain.
            this._propGetters[0].TheObj = _obj;
        }
    }

    public bool HasObj
    {
        get
        {
            return _obj != null;
        }
    }
}  

为了创建 CompositePathGetter,我们向其构造函数传递了一个 BindingPathLink<object> 对象的集合。对于每个链接,我们通过使用方法 GetPropertyGetter() 创建相应的简单属性获取器。

IObjWithPropGetter<object> propGetter = pathLink.GetPropertyGetter();  

此函数根据链接的 ThePropertyKind 属性返回正确的属性获取器。

public virtual IObjWithPropGetter<PropertyType> GetPropertyGetter()
{
    switch (ThePropertyKind)
    {
        case PropertyKind.Plain:
            return new PlainPropWithDefaultGetter<PropertyType>(PropertyName, DefaultValue);
        case PropertyKind.Map:
            return new MapPropWithDefaultGetter<PropertyType>(PropertyName, DefaultValue);
        case PropertyKind.AProp:
            return new APropWithDefaultGetter<PropertyType>(TheAProp, DefaultValue);
        default:
            return null;
    }
}  

路径链接上的 ThePropertyKind 可以根据路径链接的构建方式进行设置。例如,设置其 PropertyName 将导致 ThePropertyKind 设置为 PropertyKind.Plain,而设置其 TheAProp 属性将导致 ThePropertyKind 设置为 PropertyKind.AProp

我们按照链式结构排列从路径链接获得的属性获取器对象,这样在链中前一个链接上触发 PropertyChangeEvent 时,会触发设置下一个获取器的 TheObj 属性(这反过来又会触发在该获取器上触发 PropertyChangedEvent 等)。链中的最后一个属性获取器会触发 CompositePathGetter<PropertyType> 对象自身的 PropertyChangedEvent,确保绑定将更改传播到其属性设置器对象。这种链式结构确保了无论链中哪个链接发生更改,CompositePathGetter 对象都会注意到并传播事件。

这是 CompositePathSetter<PropertyType> 的代码。

public class CompositePathSetter<PropertyType> : IObjWithPropSetter<PropertyType>
{
    IObjWithPropSetter<object> _theSetter = null;

    // the last link in the chain and the only setter
    // within it
    public IObjWithPropSetter<object> TheSetter
    {
        get
        {
            return _theSetter;
        }
    }

    // The chain of property getters - all links
    // except for the last one.
    List<IObjWithPropGetter<object>> _propGetters;
    public List<IObjWithPropGetter<object>> PropGetters
    {
        get
        {
            return _propGetters;
        }
    }

    public CompositePathSetter(IEnumerable<BindingPathLink<object>> pathLinks)
    {
        // designate the setter path link as last link
        BindingPathLink<object> theSetterPathLink = pathLinks.Last();

        _theSetter = theSetterPathLink.GetPropertySetter();

        // Just like in case of CompositePathGetter
        // create a chain of getters up to the last link
        // in order to detect and propogate TheObj property changes.
        _propGetters =
            pathLinks
                .TakeWhile((pathLink) => (!Object.ReferenceEquals(pathLink, theSetterPathLink)))
                .Select((pathLink) => pathLink.GetPropertyGetter())
                .ToList();

        // set all the prop getters exactly like in
        // CompositePathGetter constructor.
        // So that we set TheObj property of the 
        // next getter to be the value of the previous one
        IObjWithPropGetter<object> previousPropGetter = null;
        foreach (var propGetter in _propGetters)
        {
            if (previousPropGetter != null)
            {
                previousPropGetter.PropertyChangedEvent += (obj) =>
                {
                    propGetter.TheObj = obj;
                };
            }

            previousPropGetter = propGetter;
        }

        // the last getter's PropertyChangedEvent
        // is setting TheObj property on the setters. 
        if (previousPropGetter != null)
        {
            // set the last property getter to the set the setter
            previousPropGetter.PropertyChangedEvent += (obj) =>
            {
                _theSetter.TheObj = obj;
            };
        }
    }

    // Setting the property value 
    // sets it on the setter (the last link in the chain)
    public void Set(PropertyType propertyValue)
    {
        _theSetter.Set(propertyValue);
    }

    // set TheObj property of the first link in the chain
    public object TheObj
    {
        set 
        {
            if (_propGetters.Count > 0)
            {
                _propGetters[0].TheObj = value;
            }
            else
            {
                _theSetter.TheObj = value;
            }
        }
    }
}  

在这里,我们同样以与 CompositePathGetter 非常相似的方式创建一个对象链,只是这个链中的最后一个对象是一个设置器。这将起到同样的作用——每当路径中的对象发生更改时,CompositePathGetter 都会注意到更改并将所需的绑定值一直传播到目标对象。

摘要

在本文中,我描述了我在纯C#中、WPF之外实现绑定范式。从某种意义上说,我的实现更强大——它不要求目标是依赖属性或附加属性,甚至不要求是AProp,它还允许将目标属性指定为相对于目标对象的复杂路径。

此绑定的另一个重要特性是,与 WPF 绑定不同,它不必绑定到绑定的目标。这意味着例如我们可以将相同的绑定类用于前向和反向绑定。

然而,一个重要的特性尚未实现——绑定到树结构中向上源属性的能力(类似于WPF的 RelativeSource AncestorType 功能)。我计划将来实现它。

在下一篇文章中,我将详细讨论绑定。特别是,我将讨论:

  • 实现双向绑定和绑定模式
  • 集合绑定
  • 允许在XAML中使用此绑定实现的Bind标记扩展

 

© . All rights reserved.