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

集合行为

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (7投票s)

2017 年 11 月 26 日

CPOL

8分钟阅读

viewsIcon

8096

downloadIcon

88

我描述了集合行为的可重用实现,这些行为使项目能够以特定方式表现,只要这些项目属于某个集合。

引言

根据我的知识,行为模式最早是由 MS Expression Blend 团队为 WPF 引入的。尽管如此,行为与视觉效果无关。事实上,我在我之前的几篇文章中举了几个非视觉行为的例子。例如,基于 View-View Model 的 WPF 和 XAML 实现模式 讨论了纯粹的非视觉“单选”和“最后两项”选择行为。

总的来说,行为是一个对象,它可以以非侵入性的方式修改类的行为,也就是说,它不需要对类进行任何编码更改。

WPF 非常丰富,事件和行为在那里可能非常有用,但我遇到了很多情况,行为对于非 WPF 对象(包括 View Models)也很有用。

行为的工作方式是:当它附加到一个对象时,它会修改该对象,包括为该对象的一些事件创建处理程序。这些处理程序是创建对象行为更改的原因。当行为从对象分离时,其事件处理程序应从对象的事件中移除。

基于以上陈述,最简单的行为是在附加到对象时提供一个事件处理程序,并在分离时将其移除。

行为可以是无状态的或有状态的

  1. 无状态行为不知道它们附加到的特定对象——实际上,您只有一个静态行为对象,用于修改所有需要它的对象的行为。
  2. 有状态的行为会保留对它们正在修改的对象的引用。相应地,每个被修改的对象都必须有自己的行为。

集合行为允许将行为附加到集合中的每个项目。诀窍是保持行为附加到属于集合的每个项目,并在项目从集合中移除时,或当集合被替换时,将其从所有项目中移除。

文章布局如下

  1. 我以回顾单个项目行为是什么开始本文,并提供一个非常简单的示例。
  2. 然后,我将展示如何将此类行为附加到项目集合,以便仅当项目是集合的一部分时,它才会被附加到每个项目,如果项目从集合中移除,则会被分离。
  3. 然后,我将展示如何通过使用 DoForEachItemCollectionBehavior 类以更少的代码实现相同的目标。
  4. 最后,我将展示如何通过使用行为的可处置令牌进一步改进代码。特别是,它缩短了集合行为的语法,允许链接行为,也允许轻松地在包含集合的类外部附加行为。

代码位置

您可以从文章或以下 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";
}  

我们创建一个包含可通知属性 TheStringMyNotifiablePropsTestClass 类的对象。我们附加 PrintNotifiablePropertyBehavior,其目的是在属性更改时将可通知属性的名称和值打印到控制台。我们将 TheString 属性更改为“Hello Worldstring,并且由于行为已附加,它会打印……

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 中提供了一些额外的结构。

我们向其中添加了 SetItemsUnsetItems 方法,它们接受项目集合并将处理程序添加到它们的 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;
    }
} 

然后,ObservableCollectionCollectionChanged 事件的处理程序将调用 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  

结论

在本文中,我描述了可重用的功能,它吸收了为集合项目创建行为的复杂性,只要它们是集合的一部分。当项目被添加到集合时,行为会被附加到它们;当项目从集合中移除时,行为会被从它们上分离。请享受代码!

© . All rights reserved.