集合行为






4.97/5 (7投票s)
我描述了集合行为的可重用实现,这些行为使项目能够以特定方式表现,只要这些项目属于某个集合。
引言
根据我的知识,行为模式最早是由 MS Expression Blend 团队为 WPF 引入的。尽管如此,行为与视觉效果无关。事实上,我在我之前的几篇文章中举了几个非视觉行为的例子。例如,基于 View-View Model 的 WPF 和 XAML 实现模式 讨论了纯粹的非视觉“单选”和“最后两项”选择行为。
总的来说,行为是一个对象,它可以以非侵入性的方式修改类的行为,也就是说,它不需要对类进行任何编码更改。
WPF 非常丰富,事件和行为在那里可能非常有用,但我遇到了很多情况,行为对于非 WPF 对象(包括 View Models)也很有用。
行为的工作方式是:当它附加到一个对象时,它会修改该对象,包括为该对象的一些事件创建处理程序。这些处理程序是创建对象行为更改的原因。当行为从对象分离时,其事件处理程序应从对象的事件中移除。
基于以上陈述,最简单的行为是在附加到对象时提供一个事件处理程序,并在分离时将其移除。
行为可以是无状态的或有状态的
- 无状态行为不知道它们附加到的特定对象——实际上,您只有一个静态行为对象,用于修改所有需要它的对象的行为。
- 有状态的行为会保留对它们正在修改的对象的引用。相应地,每个被修改的对象都必须有自己的行为。
集合行为允许将行为附加到集合中的每个项目。诀窍是保持行为附加到属于集合的每个项目,并在项目从集合中移除时,或当集合被替换时,将其从所有项目中移除。
文章布局如下
- 我以回顾单个项目行为是什么开始本文,并提供一个非常简单的示例。
- 然后,我将展示如何将此类行为附加到项目集合,以便仅当项目是集合的一部分时,它才会被附加到每个项目,如果项目从集合中移除,则会被分离。
- 然后,我将展示如何通过使用
DoForEachItemCollectionBehavior
类以更少的代码实现相同的目标。 - 最后,我将展示如何通过使用行为的可处置令牌进一步改进代码。特别是,它缩短了集合行为的语法,允许链接行为,也允许轻松地在包含集合的类外部附加行为。
代码位置
您可以从文章或以下 Github 链接下载代码:集合行为文章代码。
所有测试解决方案都位于 TESTS 文件夹下。
单个项目行为示例
解决方案 TESTS/NP.Tests.SingleItemBehaviorTest/NP.Tests.SingleItemBehaviorTest.sln 包含一个 PrintNotifiablePropertyBehavior
的示例,该示例将在单个项目上运行。
Program.Main()
方法包含用法代码
static void Main(string[] args)
{
// create the test class
MyNotifiablePropsTestClass myTestClass = new MyNotifiablePropsTestClass();
// create the behavior
PrintNotifiablePropertyBehavior printNotifiablePropertyBehavior =
new PrintNotifiablePropertyBehavior();
// attach the behavior to the class
printNotifiablePropertyBehavior.Attach(myTestClass);
// should print (since Behavior is attached)
myTestClass.TheString = "Hello World";
// detach the behavior from the class
printNotifiablePropertyBehavior.Detach(myTestClass);
// should not print (since Behavior is detached);
myTestClass.TheString = "Bye World";
}
我们创建一个包含可通知属性 TheString
的 MyNotifiablePropsTestClass
类的对象。我们附加 PrintNotifiablePropertyBehavior
,其目的是在属性更改时将可通知属性的名称和值打印到控制台。我们将 TheString
属性更改为“Hello World
” string
,并且由于行为已附加,它会打印……
TheString: Hello World
……到控制台。
然后,我们从对象上分离行为
// detach the behavior from the class
printNotifiablePropertyBehavior.Detach(myTestClass);
并将 TheString
属性更改为“Bye World
”
// should not print (since Behavior is detached);
myTestClass.TheString = "Bye World";
此更改不会被打印,因为属性已被分离。
MyNotifiablePropsTestClass
的实现也非常简单——它实现了 INotifiablePropertyChanged
接口,并且只有一个可通知属性 TheString
#region TheString Property
private string _str;
public string TheString
{
get
{
return this._str;
}
set
{
if (this._str == value)
{
return;
}
this._str = value;
this.OnPropertyChanged(nameof(TheString));
}
}
#endregion TheString Property
PringNotifiablePropertyBehavior
简单地将一个处理程序附加到 INotifiablePropertyChanged.PropertyChanged
事件,该事件(使用反射)提取属性值并将属性名称和值打印到控制台
public class PrintNotifiablePropertyBehavior :
IStatelessBehavior<INotifyPropertyChanged>
{
// the handler
private static void NotifyiableObject_PropertyChanged
(
object sender,
PropertyChangedEventArgs e
)
{
// pring property name and value to the console
// using reflection
sender.PrintPropValue(e.PropertyName);
}
public void Attach(INotifyPropertyChanged notifyiableObject)
{
// add the handler
notifyiableObject.PropertyChanged +=
NotifyiableObject_PropertyChanged;
}
public void Detach(INotifyPropertyChanged notifyiableObject)
{
// remove the handler
notifyiableObject.PropertyChanged -=
NotifyiableObject_PropertyChanged;
}
}
集合行为
现在假设我们想为某个 ObservableCollection
中的每个项目实现类似的行为。属于集合的项目应附加此行为,从集合中移除的项目(或当整个集合在某个包含类中被属性覆盖时)应从项目中分离行为。所有这些都应自动发生,而无需在包含集合的类之外进行任何额外编码。
TESTS/NP.Tests.ItemsCollectionTest/NP.Tests.ItemsCollectionTest.sln 项目中提供了一个此类示例。
MyNotifiableCollectionTestClass
包含属性 TheCollection
,它是一个 ObservableCollection
,包含 MyNotifiablePropsTestClass
的项目(在上一个部分中描述)。
这是测试代码(来自项目的 Program.Main()
方法)
// create the observable collection containing class
MyNotifiableCollectionTestClass collectionTestClass =
new MyNotifiableCollectionTestClass();
// create item 1.
MyNotifiablePropsTestClass item1 = new MyNotifiablePropsTestClass();
// set TheCollection property of the collection
// containing class to be an ObservableCollection
// that contains item1
collectionTestClass.TheCollection =
new ObservableCollection<MyNotifiablePropsTestClass>
(
new MyNotifiablePropsTestClass[] { item1 }
);
// change item1.TheString
// since item1 is part of the collection,
// it should print to console:
item1.TheString = "Item1: Hello World";
// create item2
MyNotifiablePropsTestClass item2 = new MyNotifiablePropsTestClass();
// add item2 to the collection
collectionTestClass.TheCollection.Add(item2);
// change item2.TheString
// since item2 is part of the collection,
// it should print to console
item2.TheString = "Item2: Hello World";
// remove item2 from collection
collectionTestClass.TheCollection.RemoveAt(1);
// since item2 is no longer
// part of the collection
// should NOT print to console
item2.TheString = "Item2: Bye Wordl";
// disconnect the whole collection (i.e. Item1)
collectionTestClass.TheCollection = null;
// since the collection property is null,
// nothing should be printed to console
// when Item1.TheString is changed.
item1.TheString = "Item1: Bye World";
运行代码时,应在控制台打印以下内容
TheString: Item1: Hello World
TheString: Item2: Hello World
“Bye World
”消息不应被打印,因为当它们更改时,项目不属于 MyNotifiableCollectionTestClass.TheCollection
属性。
为了实现这一点,我们在 MyNotifiableCollectionTestClass
中提供了一些额外的结构。
我们向其中添加了 SetItems
和 UnsetItems
方法,它们接受项目集合并将处理程序添加到它们的 PropertyChanged
事件,或从中移除处理程序。
// removes the PropertyChanged handler from all
// old items
void UnsetItems(IEnumerable items)
{
if (items == null)
return;
foreach (MyNotifiablePropsTestClass item in items)
{
item.PropertyChanged -= Item_PropertyChanged;
}
}
// attached the PropertyChanged handler to all
// new items
void SetItems(IEnumerable items)
{
if (items == null)
return;
foreach(MyNotifiablePropsTestClass item in items)
{
item.PropertyChanged += Item_PropertyChanged;
}
}
然后,ObservableCollection
的 CollectionChanged
事件的处理程序将调用 UnsetItems
来处理旧项目,并调用 SetItems
来处理新项目。
private void _collection_CollectionChanged
(
object sender,
NotifyCollectionChangedEventArgs e
)
{
// remove handlers from the old items
UnsetItems(e.OldItems);
// add handlers to all new items
SetItems(e.NewItems);
}
最后,在 TheCollection
属性的 setter 中,我们设置 CollectionChanged
处理程序,并且我们还调用 UnsetItems
来处理旧集合,并调用 SetItems
来处理新集合(通过值传递)。
private ObservableCollection<MyNotifiablePropsTestClass> _collection;
public ObservableCollection<MyNotifiablePropsTestClass> TheCollection
{
get
{
return this._collection;
}
set
{
if (this._collection == value)
{
return;
}
if (_collection != null)
{
// remove the handler
// from the old collection
_collection.CollectionChanged -=
_collection_CollectionChanged;
}
// remove handlers from items
// in the old collection
UnsetItems(this._collection);
this._collection = value;
// add handlers to the items
// in the new collection
SetItems(this._collection);
if (_collection != null)
{
// watch for the new collection change
// to set the added and unset
// the removed items
_collection.CollectionChanged +=
_collection_CollectionChanged;
}
}
}
您可以看到,为了使具有集合的类按照我们想要的方式运行,我们不得不添加大量的代码。
在下一节中,我将展示如何通过使用可重用的 DoForEachItemCollectionBehavior<T>
类来大大缩减此额外代码量。
使用 DoForEachItemCollectionBehavior<T> 类实现集合行为
解决方案 TESTS/NP.Tests.ItemsCollectionBehaviorTest/NP.Tests.ItemsCollectionBehaviorTest.sln 展示了代码的巨大简化。预期的行为和 Program.Main(...)
方法与上一节完全相同;更改在于 MyNotifiableCollectionTestClass
类内部的结构。您可以看到该类的大小几乎减半——从 103 行减少到 54 行。
在这里,唯一的额外代码是定义行为以及在 setter 中附加和分离集合的行为。
// define the behavior.
DoForEachItemCollectionBehavior<INotifyPropertyChanged>
_doForEachItemCollectionBehavior =
new DoForEachItemCollectionBehavior<INotifyPropertyChanged>
(
item => item.PropertyChanged += Item_PropertyChanged,
item => item.PropertyChanged -= Item_PropertyChanged
);
这是 TheCollection
属性的 getter 和 setter 代码
private ObservableCollection<MyNotifiablePropsTestClass> _collection;
public ObservableCollection<MyNotifiablePropsTestClass> TheCollection
{
get
{
return this._collection;
}
set
{
if (this._collection == value)
{
return;
}
// detach from old collection
_doForEachItemCollectionBehavior.Detach(_collection);
this._collection = value;
// attach to new collection
_doForEachItemCollectionBehavior.Attach(_collection);
}
}
通过使用可处置令牌进一步改进代码
有一种方法可以进一步共享代码,方法是使用可处置行为令牌和 NP.Paradigms.Behaviors.DoForEachBehaviorUtils
static
类的扩展方法。
看看 TESTS/NP.Tests.ItemsCollectionDisposableBehaviorTest/NP.Tests.ItemsCollectionDisposableBehaviorTest.sln 解决方案。
同样,Program.Main(...)
方法与前两个示例完全相同。
然而,MyNotifiableCollectionTestClass
比上一个示例更小——只有 52 行。
我们只需要添加以下类字段
// 包含可处置令牌。
// 调用其 Dispose()
方法将
// 从集合中分离所有行为 IDisposable _disposableBehaviors;
并将以下代码添加到 setter 的末尾
_disposableBehaviors?.Dispose(); // detaches old behaviors
_disposableBehaviors = _collection.AddBehavior
(
item => item.PropertyChanged += Item_PropertyChanged,
item => item.PropertyChanged -= Item_PropertyChanged
);
在这里,我们通过调用 DoForEachBehaviorUtils.AddBehavior
扩展方法来创建行为,甚至是一个行为集合,当集合更改时。该方法返回一个 IDisposable
,它将包含所有创建的行为,并在调用 IDisposable.Dispose()
方法时分离它们。
那些熟悉 Rx.NET 的人会认出这种取消订阅的方式是从那里借用的。
现在,您可能想知道为什么我说多个行为可能在令牌处置时被分离,即使只有一个行为被创建。原因是这个方法(DoForEachBehaviorUtils.AddBehavior
)也允许我们链接多个行为,如下一节所示。
在定义集合的类外部链接多个集合行为
此示例的代码位于 TESTS/NP.Tests.ItemsCollectionDisposableChainedBehaviorTest/NP.Tests.ItemsCollectionDisposableChainedBehaviorTest.sln 解决方案下。
该示例展示了如何使用 AddBehavior
扩展方法链接多个行为,以及如何使用可处置令牌范例将行为附加到定义它的类外部的集合。
测试对象有一些更改。首先,MyNotifiableCollectionTestClass
现在定义了一个事件 TheCollectionValueChangedEvent
,当 TheCollection
属性的值发生更改时(在属性的 setter 中)会触发该事件。
public event Action<ObservableCollection<MyNotifiablePropsTestClass>>
TheCollectionValueChangedEvent = null;
#region TheCollection Property
private ObservableCollection<MyNotifiablePropsTestClass> _collection;
public ObservableCollection<MyNotifiablePropsTestClass> TheCollection
{
get
{
return this._collection;
}
set
{
if (this._collection == value)
{
return;
}
this._collection = value;
// fire collection changed event:
TheCollectionValueChangedEvent?.Invoke(_collection);
}
}
#endregion
可处置令牌和设置行为已移至 Program.Main(...)
。
Static
Program
类现在有一个 static
属性 NumberItemsInCollection
。第二个行为链的目的 — 维护此属性以匹配 collectionTestClass.TheCollection
集合中的项目数量。此属性的 setter 还将包含属性值的 string
发送到控制台。
static int _numberItemsInCollection = 0;
public static int NumberItemsInCollection
{
get
{
return _numberItemsInCollection;
}
set
{
if (_numberItemsInCollection == value)
return;
_numberItemsInCollection = value;
// write the changed property to console
Console.WriteLine($"Number items in collection: {_numberItemsInCollection}");
}
}
在 Program.Main(...)
方法体最顶部,创建 collectionTestClass
后,我们向其 TheCollectionValueChangedEvent
添加一个处理程序。
// create the observable collection containing class
MyNotifiableCollectionTestClass collectionTestClass =
new MyNotifiableCollectionTestClass();
// create the handler for event fired when TheCollection
// property is assigned a new value
collectionTestClass.TheCollectionValueChangedEvent +=
CollectionTestClass_TheCollectionValueChangedEvent;
Program.Main(...)
主体其余部分与前 3 个示例完全相同。
这是 CollectionTestClass_TheCollectionValueChangedEvent
处理程序。
static IDisposable _disposableBehaviors = null;
private static void CollectionTestClass_TheCollectionValueChangedEvent
(
ObservableCollection<MyNotifiablePropsTestClass> collection
)
{
_disposableBehaviors?.Dispose();
// chain the two behaviors - one to record the
// changed property value, the other to maintain
// NumberItemsInCollection of the same size as
// the collection.
_disposableBehaviors = collection
.AddBehavior
(
item => item.PropertyChanged += Item_PropertyChanged,
item => item.PropertyChanged -= Item_PropertyChanged
)
.AddBehavior
(
item => NumberItemsInCollection++,
item => NumberItemsInCollection--
);
}
请注意,只有一个可处置令牌(_disposableBehaviors
),即使我们可以链接任意数量的行为。
另外,请注意,我们也可以在集合定义的类外部使用 Attach
/Detach
方法,但我们需要两个事件 — 一个指示旧集合已被移除,一个指示新集合已创建,或者一个带有两个参数的事件 — 分别对应新旧集合,这比我们使用可处置令牌要复杂。
运行此示例时,您应该会得到
Number items in collection: 1
TheString: Item1: Hello World
Number items in collection: 2
TheString: Item2: Hello World
Number items in collection: 1
Number items in collection: 0
结论
在本文中,我描述了可重用的功能,它吸收了为集合项目创建行为的复杂性,只要它们是集合的一部分。当项目被添加到集合时,行为会被附加到它们;当项目从集合中移除时,行为会被从它们上分离。请享受代码!