纯 C# 实现 WPF 概念 - 第一部分:AProps 和绑定入门






4.97/5 (70投票s)
在 WPF 之外,使用纯 C# 实现附加属性和绑定
引言
WPF(Windows Presentation Foundation)引入了许多新的编程范例,据我所知,这些范例并未被任何其他框架或语言所利用。这些范例使得 WPF 编程非常强大、简洁,并且非常适合关注点分离。一旦您理解了如何使用这些范例,就可以用寥寥数行代码实现大量功能,同时保持关注点分离,从而使您的软件易于扩展、修改和调试。
WPF 范例的有趣之处在于,其中许多范例与 WPF、可视化编程甚至 C# 语言都无关,并且可以为非可视化编程和其他语言(例如 Java 和 JavaScript)实现和应用,为构建完全不同的应用程序带来相同的优势。
Microsoft 团队实现 WPF 范例的方式将它们与 WPF 紧密耦合。因此,要在 WPF 之外使用它们,即使是在 C# 中,也需要另一项实现。事实上,我的感觉是,WPF 的架构师和开发人员本身并没有完全理解他们所取得的了不起的概念性突破,因此并没有推动这些范例获得更广泛的接受。
在这一系列文章中,我将介绍我自己在纯(非 WPF)C# 中实现的大部分 WPF 范例。其中许多范例的实现方式略有不同——通常比 WPF 版本更通用。我还删除了一些我在 WPF 中不经常使用的功能。最初,这些范例在我的一系列博客文章中有简要描述——您可以在 CodeProject 上找到它们。
未来我计划为 Java 和 JavaScript(或者可能是 TypeScript)创建类似的包。
以下是将在这些文章系列中描述的无 WPF 范例实现的列表:
- 附加属性
- 绑定
- DataTemplates
- 通用树(函数式树)- 而非可视化树和逻辑树。
- 路由事件
- 行为
在本期系列文章中,我将讨论在 WPF 之外重新实现附加属性和绑定。我还将讨论比 WPF 更高级但由 WPF 推广的概念:非侵入式对象修改(NIOM)和数据及功能模仿(DAFM)。
目前我还没有启用多线程或选择线程来实现功能——因此所有示例都是单线程的,并且更改处理发生在调用更改的同一线程中。我计划在未来某个时候解决多线程问题。
阅读本文需要一些 WPF 知识,但非必需,因为我提供了非 WPF 的示例来说明这里描述的每一项功能。
代码结构
测试解决方案文件可在“Tests”目录下的文件夹中找到。
此外,“Tests”目录还包含一个 NP.Tests.GenericTestObjects
库,用于创建(非常简单的)测试对象,如果它们在多个测试中使用的话。
“NP.Paradigms”文件夹包含实现本文所述 WPF 范例的通用无 WPF 代码。
非 WPF 实现的附加属性(AProps)用法和非侵入性概念
WPF 中的附加属性
对阅读本文的 WPF 开发人员的重要说明: 在下面讨论附加属性时,我还包括了依赖属性——我认为它们是附加属性的一种版本,要求在使用它们的类中进行定义。
WPF 中的附加属性部分是为了让具有数百个属性(但大多数属性都具有默认值)的对象占用更少的空间,而不是一种直接的实现——即所谓的稀疏实现。
同时,它们解决了许多其他非常重要的问题:
- 它们允许将任何属性附加到任何对象,而无需提前在该对象上定义该属性。
- 可以注册属性更改处理程序,以便在附加属性在对象上发生更改时调用它。这允许在附加属性更改后执行一些非常有趣的逻辑,并且是附加和分离行为到对象的基石。
- 附加属性可以用作 WPF 绑定目标。在我们的绑定实现中,我们将放宽此条件——因此,不仅我们的附加属性(或我称之为 AProps),而且普通属性也可以用作绑定目标。
- WPF 附加属性可以向下传播到可视化树。我尚未在 WPF 之外实现这一部分。
附加属性、行为和非侵入式对象修改(NIOM)概念
存在一个广泛使用但尚未正式化的软件开发概念,称为非侵入式对象修改(NIOM);这是我尝试对其进行形式化。
纯 C# 允许“添加”方法到类,而无需修改类——所谓的扩展方法可以在一个静态类中定义,该类独立于调用它的对象所在的类。在这种情况下,非侵入性当然只是名义上的——它们不是修改类的“真实”方法;例如,它们无法访问类的私有或受保护成员,但这仍然很重要。
WPF 的附加属性定义在一个静态类中,并且可以附加到任何派生自 DependencyObject
类的对象。这允许将一些数据与对象关联,而无需修改对象的类。这对于关注点分离非常重要。例如,假设您有一个通用对象 Widget
。Widget
代表一个带有某些信息的窗口。一些 Widget
有标题,有些没有。一种实现带有标题的 Widget
的方法是创建一个继承自 Widget
类的 WidgetWithHeader
类。然而,由于 C# 缺乏多重继承,这将阻止 WidgetWithHeader
使用任何其他超类。相反,我们可以将 Widget
的标题创建一个完全独立的控件,并使用附加属性将其附加到需要标题的 Widget
上。在这种情况下,我们甚至可以使用相同的附加属性将标题附加到可能仍需要标题的非 Widget
对象上。
当然,附加属性的相同方法也可以应用于非可视化对象。
行为是通过非侵入式方式修改对象的最强大方法。在 WPF 中,行为通常使用附加属性附加到对象上。行为本身为对象的事件提供事件处理程序。使用行为,您可以在对象类之外为对象创建非常复杂的功能。
同样,没有任何内容可以阻止行为在 WPF 之外使用,我在 基于 View-View Model 的 WPF 和 XAML 实现模式(代码复用的 WPF 和 XAML 模式,简单示例,第 2 部分) 中给出了非可视化行为的示例。
AProps 用法示例
我将 WPF 之外的附加属性实现称为 AProps,以区别于 WPF 实现。
我将先展示如何使用 AProps,然后再讨论它们是如何实现的——因为我坚信,首先,用户需要理解一个概念的必要性,然后才重要地讨论它的实现方式。
本节中提供的示例位于“Tests/APropTests”和“Tests/APropsIndividualHandlersTests”文件夹下。两者都引用了定义在 NP.Tests.GenericTestObjects
项目中的一个非常简单的 Person
类。这是 Person
类的代码:
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public override string ToString() { return FirstName + " " + LastName; } }
在 NP.Tests.APropsTests
项目中,我们展示了如何:
- 为特定的对象和属性类型创建 AProp,指定默认属性值和通用的属性更改前后处理程序(通用是指它们将针对每个对象触发,如果其相应的 AProp 发生更改)。
- 在相应类型的任何对象上设置属性,并观察处理程序的触发。
这是测试项目 Program.Main()
方法的代码:
public static void Main() { // create the AProp AProp<Person, bool> isStudentAProp = new AProp<Person, bool> ( false, // by default, the person is not a student (obj, oldVal, newVal) =>// to be called before the property is set { Console.WriteLine ( "\tIsStudent AProp is about to change on person " + obj + " from " + oldVal + " to " + newVal ); return true; // returning false would cancel the update. }, (obj, oldVal, newVal) =>// to be called after the property is set { Console.WriteLine("\tIsStudent AProp changed on the person " + obj); } ); Person nick = new Person { FirstName = "Nick", LastName = "Polyak" }; // before property change, isNickStudent is set to default value 'false' bool isNickStudent = isStudentAProp.GetProperty(nick); Console.WriteLine("Default IsStudent AProp on object nick is " + isNickStudent); // if the value is the same as before (false) - no callbacks are fired isStudentAProp.SetProperty(nick, false); // if the value change to 'true' - the callbacks are fired. isStudentAProp.SetProperty(nick, true); isNickStudent = isStudentAProp.GetProperty(nick); // will show that isNickStudent is now set to 'true' Console.WriteLine("Default IsStudent AProp on object nick is " + isNickStudent); }
行 AProp<Person, bool> isStudentAProp = new AProp<Person, bool>(...)
创建了 AProp。
与 WPF 附加属性一样,AProp 是一个单独的对象。与 WPF 附加属性不同——只要 AProp 对象在您希望使用它的地方可见,用于获取或设置对象上的值,它就不必定义为静态的,并且不必位于静态类中。在我们的例子中,我们只在 Program.Main()
方法中使用它,因此我们可以将其定义在同一个函数内。
另外,请注意 AProp 比 WPF 的附加属性更类型安全——它的两个类型参数分别定义了我们想要附加的对象类型和附加属性的类型——在我们的例子中,我们希望能够将类型为 bool
的 IsStudent
属性附加到类型为 Person
的对象上,所以我们定义了泛型类型 <Person, bool>
。
AProps 可以附加到任何引用类型对象——不允许使用值类型。这又比 WPF 更通用,WPF 只允许使用派生自 DependencyObject
类的类用于此目的。
现在,让我们看看整个构造函数:
// create the AProp AProp<Person, bool> isStudentAProp = new AProp<Person, bool> ( false, // by default, the person is not a student (obj, oldVal, newVal) =>// to be called before the property is set { Console.WriteLine ( "\tIsStudent AProp is about to change on person " + obj + " from " + oldVal + " to " + newVal ); return true; // returning false would cancel the update. }, // is called before the property is set. (obj, oldVal, newVal) =>// to be called after the property is set { Console.WriteLine("\tIsStudent AProp changed on the person " + obj); } );
构造函数的第一个参数是默认值(如果在给定对象上从未设置过值,则返回的属性值)。
构造函数的第二个参数是 BeforePropertyChangedDelegate<ObjectType, PropertyType>
类型的委托。
public delegate bool BeforePropertyChangedDelegate<ObjectType, PropertyType> ( ObjectType obj, PropertyType oldPropertyValue, PropertyType newPropertyValue );
它允许操作正在设置 AProp 的对象、旧的属性值和新的属性值。它还通过返回布尔值 false
给了您最后一次取消属性更改的机会。
我们将其实现为一个 Lambda 表达式:
(obj, oldVal, newVal) =>// to be called before the property is set { Console.WriteLine ( "\tIsStudent AProp is about to change on person " + obj + " from " + oldVal + " to " + newVal ); return true; // returning false would cancel the update. }
正如您所见,我们的处理程序正在打印一条消息,说明属性即将更改。
下一个参数是 OnPropertyChangedDelegate<ObjectType, PropertyType>
类型的后更改处理程序。
public delegate void OnPropertyChangedDelegate<ObjectType, PropertyType> ( ObjectType obj, PropertyType oldPropertyValue, PropertyType newPropertyValue );
在我们的示例中,它也是一个 Lambda 表达式:
(obj, oldVal, newVal) =>// to be called after the property is set { Console.WriteLine("\tIsStudent AProp changed on the person " + obj); }
它打印一条消息,说明属性已更改。
我们将 pre 和 post 更改处理程序中打印的消息通过制表符 "\t"
进行移位,以免它们与 Main
方法本身中打印的消息混淆。
请注意,pre 和 post 更改处理程序都可以作为 null
传递(或根本不传递),在这种情况下,属性更改时不会触发通用处理程序。
其余代码在很大程度上已通过内联注释进行了解释。
// create Person object nick Person nick = new Person { FirstName = "Nick", LastName = "Polyak" }; // before property change, isNickStudent is set to default value 'false' bool isNickStudent = isStudentAProp.GetProperty(nick); Console.WriteLine("Default IsStudent AProp on object nick is " + isNickStudent); // if the value is the same as before (false) - no callbacks are fired isStudentAProp.SetProperty(nick, false); // if the value change to 'true' - the callbacks are fired. isStudentAProp.SetProperty(nick, true); isNickStudent = isStudentAProp.GetProperty(nick); // will show that isNickStudent is now set to 'true' Console.WriteLine("Default IsStudent AProp on object nick is " + isNickStudent);
我们创建了一个 Person
对象 nick
,并使用 AProp 的 setter 和 getter 来在该对象上设置和获取属性。以下是如何从对象 nick
获取 isStudentAProp
值的示例:
bool isNickStudent = isStudentAProp.GetProperty(nick);
以下是如何将值 true
设置到对象 nick
的 isStudentAProp
上的示例:
isStudentAProp.SetProperty(nick, true);
请注意,如果属性被设置为与之前相同的值(无论是默认值还是其他值),则不会发生任何变化,也不会触发任何处理程序,正如我们在代码中所演示的。
Default IsStudent AProp on object nick is False IsStudent AProp is about to change on person Nick Polyak from False to True IsStudent AProp changed on the person Nick Polyak Default IsStudent AProp on object nick is True
在单个对象上使用 AProp 更改处理程序
第二个 AProp 示例位于 NP.Tests.APropsIndividualHandlersTests
解决方案下。它展示了如何向单个对象添加一个 Post 更改 AProp 处理程序——以便在 AProp 在该对象上更改时才会触发它,而不会在任何其他对象上触发。
这是附加属性不具备的重要功能——我能够模仿它的唯一方法是创建一个与我们想要检测更改的附加属性作为源的绑定,并使用目标附加属性的更改处理程序进行更改检测。
位于 Program.Main()
方法中的测试代码比前一个示例更简单:
public static void Main() { // do not set generic pre and post change handlers for the AProp AProp<Person, bool> isStudentAProp = new AProp<Person, bool> ( false // by default, the person is not a student ); // create a person object Person nick = new Person { FirstName = "Nick", LastName = "Polyak" }; // create another Person object Person joe = new Person { FirstName = "Joe", LastName = "Doe" }; // attach the inidividual isStudentAProp change handler to object 'nick' isStudentAProp.AddOnPropertyChangedHandler(nick, IndividualPropChangeHandler); // handler is set for object 'nick' so it will be triggered here isStudentAProp.SetProperty(nick, true); // handler is not set for object 'joe', so it won't be triggered here isStudentAProp.SetProperty(joe, true); } private static void IndividualPropChangeHandler(Person obj, bool oldPropertyValue, bool newPropertyValue) { Console.WriteLine("This is INDIVIDUAL AProp value change handler fired on object " + obj.ToString()); }
我们将 isStudentProp
创建为一个没有任何通用更改处理程序的 AProp。
创建了两个 Person
对象 nick
和 joe
——以便展示当我们向 nick
附加一个单独的更改处理程序时,更改 joe
上的 AProp 不会触发它。
这是我们如何将 isStudentAProp
单独更改处理程序附加到 nick
对象上的方法:
// attach the inidividual isStudentAProp change handler to object 'nick' isStudentAProp.AddOnPropertyChangedHandler(nick, IndividualPropChangeHandler);
运行此测试时,您会看到处理程序仅为 nick
对象触发。
This is INDIVIDUAL AProp value change handler fired on object Nick Polyak
清除 AProps
为了将对象上的 AProp 重置为默认值,可以使用 APropClearAProperty(Object obj)
方法。例如,在上面的示例中,可以通过调用 isStudentAProp.ClearAProperty(nick)
来清除对象 nick
上的 isStudentAProp
。所有附加到 nick
对象上的单独更改处理程序也将被移除。
AProps 与附加属性的比较
上面的示例展示了 AProps 的完整功能。它们比附加属性的复杂性要小得多,但它们涵盖了我曾经从附加属性中需要的一切,除了能够将属性值向下继承到可视化树的能力。与附加属性不同,AProps 可以定义在任何引用类型对象上,而不仅仅是派生自 DependencyObject
的对象。此外,AProps 更类型安全,并允许为单个对象注册更改处理程序。
AProp 实现
有两种方法可以将(属性)值与对象关联。一种方法是将此值添加到对象的类中——在这种情况下,该类的每个对象都将包含对该值的引用。
另一种不那么常见的方法是创建一个可由对象访问的值集合。这可以通过使用 Dictionary<ObjectType, ValueType>
来完成,其中对象作为键。然后(只要您拥有对字典的引用),您就可以为每个对象获取属性值。Microsoft 就是这样实现附加属性的,我也是这样实现 AProps 的。
AProp 实现代码位于 NP.Paradigms 项目中的 AProp.cs 文件中。
对象到值的字典由类成员定义:
ConditionalWeakTable<ObjectType, APropertyValueWrapper<ObjectType, PropertyType>> _objectToPropValueMap;
我使用了 ConditionalWeakTable
而不是普通的 Dictionary
类,因为它对键和值创建弱引用,因此对象销毁不会受到它作为键的阻碍。此外,销毁的对象将从 ConditionalWeakTable
集合中自动删除。另外,ConditionalWeakTable
是多线程安全的,因此您无需担心在多个线程上访问和设置 AProps。
这个“字典”的键是 ObjectType
的普通对象,而值是普通的属性值,但它们是 APropertyValueWrapper<ObjectType, PropertyType>
类的对象。
APropertyValueWrapper<ObjectType, PropertyType>
是一个私有类,定义在同一个文件中。它包含 AProp 值作为:
internal PropertyType APropertyValue { get; set; }
它还包含对拥有该值的对象的弱引用:
internal WeakReference<ObjectType> ObjReference { get; private set; }
以及一个用于附加单个 AProp 更改事件处理程序的事件:
// allows to set event handlers to be fired one the AProperty is changed on the object internal event OnPropertyChangedDelegate<ObjectType, PropertyType> OnPropertyChangedEvent = null;
可以通过以下代码触发该事件:
internal void FireOnPropertyChangedEvent(PropertyType oldPropertyValue) { if (OnPropertyChangedEvent != null) { ObjectType obj; if (ObjReference.TryGetTarget(out obj)) { OnPropertyChangedEvent(obj, oldPropertyValue, APropertyValue); } } }
以上也说明了为什么我们需要对“包含”AProp 值的对象的引用——我们需要它来将其作为第一个参数传递给事件处理程序。
类 AProp<ObjectType, PropertyType>
提供了我们上面显示的用于在对象上设置和获取 AProp 值的方法。此外,它提供了 PropertyType _defaultValue
类成员,该成员定义了在传递给 AProp.GetProperty(ObjectType obj)
方法的对象上未设置 AProp 时(在 ConditionalWeakTable
表中没有相应条目)要返回的值。
还有一个非强类型版本的 AProp 类:
public class AProp : AProp<object, object>
它更通用,但类型安全度较低。
注册 AProps
类 AProp<ObjectType, PropertyType>
实现了一个非常简单的接口:
public interface IAPropValueGetter { object GetObjectAPropValue(object obj); }
此接口允许获取传递对象的非类型化 AProp 值。
还有一个静态类:
public static class AllAProps { static public List<IAPropValueGetter> TheAProps { get { ... } } ... }
它包含一个集合 TheAProps
,其中包含您应用程序中定义过的所有 AProps。每个 AProp
对象在其构造函数中被添加到 AllAProps.TheAProps
集合中。还有一个内置机制用于删除对过时(已垃圾回收)AProp
的引用。
此时此集合尚未使用,但这是您应用程序中所有 AProps 的唯一方式,并且可能被工具使用,例如 Snoop 改进后可以处理 AProps。(当然,目前 Snoop 没有这种功能)。
非 WPF 实现的绑定和数据及功能模仿(DAFM)概念
引言
本文篇幅已大,所以我将在此处仅提供一个绑定的预览,主要内容将在本系列的第二部分中介绍。
WPF 并不是第一个开始使用绑定的框架,但 WPF(据我所知)是第一个以绑定为中心的框架。著名的 MVVM 模式只是绑定所能实现的功能之一。
绑定是我称为数据及功能模仿(DAFM)的概念的核心。
MVVM 是 DAFM 的一个例子——您创建一个纯粹的非可视化骨架,称为 ViewModel 或 VM。这个骨架可以负责创建一些数据和数据集合、与应用程序的其他部分以及后端进行通信等等。
View(应用程序的可视部分)是围绕骨架生长并模仿其数据和功能的“肉”。View 的更改(例如,在可编辑区域的用户输入)也可以导致 ViewModel 的更改(绑定可以双向工作)。
重要提示: 上图可能会让人认为 ViewModel 了解 View。这是不正确的!ViewModel 控制 View 的唯一方式是通过绑定。然而,View 了解 ViewModel,并且可以通过绑定和其他一些方式修改它。
两种数据类型和两种数据绑定
在高级别上,有两种数据结构:
- 可以由映射到值的名称表示的数据。前面小节中考虑的 C#
Person
类对象就是一个这样的数据的例子——它的FirstName
属性可能映射到值“joe”,它的LastName
属性可能映射到值“doe”; - 可以由形状相似的对象集合表示的数据。任何集合,例如整数集合,都将是此类数据的示例。
通过结合这两种类型的数据,我们可以创建一个数据层次结构——例如,类的属性可以包含一些复杂类型对象的集合,这些对象包含多个属性等等。
JSON 非常适合表示数据结构层次结构——包括名称-值对和集合。
更知名的 WPF 绑定是将两个对象上的两个属性进行绑定,即它绑定了数据层次结构中的名称-值类型。
另一种类型的绑定——是两个集合之间的绑定——它使目标集合模仿源集合的结构——当源集合中的项目被插入或删除时,相应的项目也会被插入或删除到目标集合中。这种类型的绑定在 WPF 中仅隐式使用——当 ItemsControl
的 ItemsSource
绑定到 ObservableCollection<T>
时。
下面我将介绍实现这两种数据绑定的类。
数据绑定示例
如上所述,我将在这里展示一些非 WPF 绑定的功能,而更详细的讨论和实现细节将在下一篇文章中提供。
属性绑定示例
此测试位于 PropertyBindingTests.sln 解决方案下。
为了演示带复合路径的绑定,我创建了一个类层次结构。
源对象是 Contact
类。它包含类型为 Address
的 HomeAddress
和 WorkAddress
属性。Address
包含 City
和 Street
字符串属性。
public class Address : INotifyPropertyChanged { ... public string City { ... } public string Street { ... } }
和
public class Contact : Person, INotifyPropertyChanged { ... public Address HomeAddress { ... } public Address WorkAddress { ... } }
Contact
和 Address
类都实现了 INotifiablePropertyChanged
接口,并且所有属性在属性更改时都会触发 PropertyChanged
事件。
目标对象是 PrintModel
类。它包含类型为 PrintProp
的 HomeCityPrintObj
属性。它还包含一个 Print()
方法,该方法调用 HomeCityPrintObj.Print()
。PrintProp
类包含 _propertyName
字符串字段、PropValueToPrint
字符串属性和 Print()
方法,该方法打印属性名称和属性值。
public class PrintProp { public PrintProp(string propName) { _propName = propName; } readonly string _propName; public object PropValueToPrint { get; set; } public void Print() { string strToPrint = "null"; if (PropValueToPrint != null) strToPrint = PropValueToPrint.ToString(); Console.WriteLine(_propName + ": " + strToPrint); } }
和
public class PrintModel : INotifyPropertyChanged { ... PrintProp _homeCityPrintObj = null; public PrintProp HomeCityPrintObj { private get { return _homeCityPrintObj; } set { if (_homeCityPrintObj == value) return; _homeCityPrintObj = value; OnPropertyChanged("HomeCityPrintObj"); } } public PrintModel() { HomeCityPrintObj = new PrintProp("Home City"); } public void Print() { HomeCityPrintObj.Print(); } }
在 Program.Main()
方法中,我们创建了一个 Contact
和一个 PrintModel
,并将 Contact
的 HomeAddress/City
属性绑定到目标的 HomeCityPrintObj/PropValueToPrint
属性。
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
请注意,我们的属性绑定可以绑定到目标上的复合路径(在我们的例子中是 HomeCityPrintObj/PropValueToPrint
),而 WPF 属性绑定只能绑定到目标上的直接属性。从这个意义上说,我们的绑定比 WPF 的更通用。
同时请注意,只要源路径上的相应属性在更改时触发 INotifyPropertyChanged.PropertyChanged
事件,目标属性也会被更改:首先我们更改城市名称,然后我们更改整个 HomeAddress
,在这两种情况下,更改都会被 PrintModel
捕获。
另一个有趣的结果是,即使我们更改了目标路径上的链接,例如:
printModel.HomeCityPrintObj = new PrintProp("Home City");
并且此链接更改也触发了 PropertyChanged
事件,活动绑定将强制新的 printModel.HomeCityPrintObj
具有正确的 PropValueToPrint
。
在后续文章中,我们将展示我们的 Binding
在源路径和目标路径中都可以包含 AProp 和 WPF 附加属性链接。
集合绑定示例
集合绑定示例位于 CollectionBindingTests.sln 解决方案下。
我们使用 ObservableCollection<Person>
作为绑定源,使用 ObservableCollection<PersonVM>
作为绑定目标集合。
Person
是一个非常简单的类,我们之前已经使用过。它包含 FirstName
和 LastName
字符串属性。PersonVM
继承自 Person
,并添加了 IsEnabled
和 IsVisible
属性。
public class PersonVM : Person { public bool IsVisible { get; set; } public bool IsEnabled { get; set; } public PersonVM() { IsEnabled = true; IsVisible = true; } public PersonVM(Person p) : this() { this.FirstName = p.FirstName; this.LastName = p.LastName; } }
在某种意义上,Person
对象集合可以被认为是 Model,而 PersonVM
对象集合可以被认为是 ViewModel。
这是 Program.Main()
方法的代码:
public static void Main() { // source collection ObservableCollection<Person> personCollection = new ObservableCollection<Person>(); personCollection.Add(new Person { FirstName = "Nick", LastName = "Polyak" }); personCollection.Add(new Person { FirstName = "Joe", LastName = "Doe" }); // target collection Collection<PersonVM> personVMCollection = new Collection<PersonVM>(); Console.WriteLine("Before binding personVMCollection is Empty:"); personVMCollection.PrintCollection(); OneWayCollectionBinding<Person, PersonVM> collectionBinding = new OneWayCollectionBinding<Person, PersonVM> { SourceCollection = personCollection, TargetCollection = personVMCollection, SourceToTargetItemDelegate = (person) => new PersonVM(person) //source to target converter }; collectionBinding.Bind(); Console.WriteLine("After binding personVMCollection is populated with the same items as the input collection:"); personVMCollection.PrintCollection(); Console.WriteLine("When 'John Smith' person is added to the source collection, he is also added to the target collection:"); personCollection.Add(new Person { FirstName = "John", LastName = "Smith" }); personVMCollection.PrintCollection(); Console.WriteLine("When 'Nick Polyak' person is removed from the source collection, he is also removed from the target collection:"); personCollection.RemoveAt(0); personVMCollection.PrintCollection(); }
调用 Binding 的 Bind()
方法后,目标集合将模仿源集合,无论对源集合做出何种更改。
Before binding personVMCollection is Empty: EMPTY After binding personVMCollection is populated with the same items as the input collection: Nick Polyak, Joe Doe When 'John Smith' person is added to the source collection, he is also added to the target collection: Nick Polyak, Joe Doe, John Smith When 'Nick Polyak' person is removed from the source collection, he is also removed from the target collection: Joe Doe, John Smith
为了实现模仿,源集合应实现 INotifyCollectionChanged
接口,而目标集合应实现 ICollection<T>
接口,即允许添加和删除项目。
摘要
在本文中,我展示了如何在 WPF 之外实现和使用附加属性和绑定,并提供了一些用法示例。这里讨论的 AProps 和 Bindings 在许多方面比 WPF 的更通用、限制更少,并且可以用于任何对象,而不仅仅是 DependencyObjects
。
在下一篇文章中,我计划更多地关注绑定,提供绑定的实现细节,引入事件绑定的概念,并提供更多的使用示例。