ObservableCollection<T> 中的 BlockReentrancy

25

请问什么是 ObservableCollection<T> 中的 BlockReentrancy 方法?能否请有好心人解释一下其作用?

MSDN 给出以下示例:

//The typical usage is to wrap an OnCollectionChanged call within a using scope, as in the following example:

using (BlockReentrancy())
{
    // OnCollectionChanged call
}

但这并没有为我澄清目的是什么。有人可以解释一下吗?


发音为 BlockReëntrancy - Colonel Panic
除了接受的答案之外,值得一提的是,在多线程使用情况下,当集合被频繁更新干扰到"OnCollectionChanged"处理时,也需要使用using (BlockReentrancy()) {}。这可能仅涉及单个XAML绑定,而且在自定义代码中没有任何其他显式订阅者。 - DK.
4个回答

28
一个ObservableCollection实现了INotifyCollectionChanged接口,因此它有一个CollectionChanged事件。如果有订阅者订阅这个事件,他们可以在集合已经在通知过程中时进一步修改集合。由于CollectionChanged事件跟踪了确切的更改内容,这种交互可能会变得非常混乱。
因此,ObservableCollection允许作为一个特殊情况,一个单独的CollectionChanged事件订阅者在其处理程序中修改集合。但是,如果有两个或更多的CollectionChanged事件订阅者,则禁止从CollectionChanged处理程序修改集合。
一对方法BlockReentrancyCheckReentancy用于实现这个逻辑。 BlockReentrancyOnCollectionChanged方法开始时使用,而CheckReentancy在所有修改集合的方法中使用。

非常有趣的是,MSDN文档没有提到这个小事实,即只有在将多个处理程序附加到CollectionChanged事件时,CheckReentrancy才会防止重入。 - treaschf
1
“如果有两个或更多的订阅者订阅了CollectionChanged事件,则在CollectionChanged处理程序中禁止修改集合。”那么如果只有一个订阅者但有两个工作线程修改集合呢?在这种情况下,是否应该使用另一个锁来锁定Insert和Remove等操作,并锁定OnCollectionChanged处理程序? - Felix

13

这是实现BlockReentrancy()的内容。

protected IDisposable BlockReentrancy()
{
   this._monitor.Enter();
   return this._monitor;
}

还有另一种方法CheckReentrancy()

protected void CheckReentrancy()
{
    if ((this._monitor.Busy && (this.CollectionChanged != null)) && (this.CollectionChanged.GetInvocationList().Length > 1))
    {
        throw new InvalidOperationException(SR.GetString("ObservableCollectionReentrancyNotAllowed"));
    }
}

在修改集合之前,ClearItemsInsertItemMoveItemRemoveItemSetItem等方法会检查CheckReentrancy()

因此,在using块内保证集合不会被改变,但前提是订阅了多个处理程序到CollectionChanged事件。

using BlockReentrancy())
{
    CollectionChanged(this, e);
}

这个例子展示了BlockReentrancy()的作用。

private static void Main()
{
    collection.CollectionChanged += CollectionCollectionChanged1;
    collection.CollectionChanged += CollectionCollectionChanged2;
    collection.Add(1);
}

private static void CollectionCollectionChanged1(object sender, NotifyCollectionChangedEventArgs e)
{
    collection.Add(2); // this line will throw exception
}

private static void CollectionCollectionChanged2(object sender, NotifyCollectionChangedEventArgs e)
{
}

1
你的演示代码将抛出 StackOverflowException 而不是 InvalidOperationException。在这种情况下,没有检查重入。请参见我的答案。 - Rick Sladkey

3

重入是指一个方法直接或间接地执行某些操作,导致该方法被再次调用,可能会递归。在这种情况下,如果您想防止在处理程序内更改集合,则应在OnCollectionChanged委托内使用using块;任何尝试修改集合都会引发异常。如果没有使用它,则任何修改集合的尝试都将导致再次调用OnCollectionChanged。


1
以下是 BlockReentrancy 的 代码。在 ObservableCollection 的实现中,每个集合修改器方法的开头都会调用 CheckReentrancy。
    /// <summary>
    /// Disallow reentrant attempts to change this collection. E.g. an event handler
    /// of the CollectionChanged event is not allowed to make changes to this collection.
    /// </summary>
    /// <remarks>
    /// typical usage is to wrap e.g. a OnCollectionChanged call with a using() scope:
    /// <code>
    ///         using (BlockReentrancy())
    ///         {
    ///             CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, item, index));
    ///         }
    /// </code>
    /// </remarks>
    protected IDisposable BlockReentrancy()
    {
        _blockReentrancyCount++;
        return EnsureMonitorInitialized();
    }

    /// <summary> Check and assert for reentrant attempts to change this collection. </summary>
    /// <exception cref="InvalidOperationException"> raised when changing the collection
    /// while another collection change is still being notified to other listeners </exception>
    protected void CheckReentrancy()
    {
        if (_blockReentrancyCount > 0)
        {
            // we can allow changes if there's only one listener - the problem
            // only arises if reentrant changes make the original event args
            // invalid for later listeners.  This keeps existing code working
            // (e.g. Selector.SelectedItems).
            if (CollectionChanged?.GetInvocationList().Length > 1)
                throw new InvalidOperationException(SR.ObservableCollectionReentrancyNotAllowed);
        }
    }

    private SimpleMonitor EnsureMonitorInitialized()
    {
        return _monitor ?? (_monitor = new SimpleMonitor(this));
    }

(版权所有 (c) .NET Foundation 和贡献者)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接