是否可以通过编程方式将基于 DependencyProperty 的绑定强制为 re-evaluate?

Is it possible to force a binding based on a DependencyProperty to re-evaluate programmatically?

NOTE: Before reading the subject and instantly marking this as a duplicate, please read the entire question to understand what we are after. The other questions which describe getting the BindingExpression, then calling UpdateTarget() method does not work in our use-case. Thanks!

TL:DR 版本

使用 INotifyPropertyChanged 我可以进行绑定 re-evaluate,即使关联的 属性 没有改变,只需引发 属性 的 PropertyChanged 事件]的名字。如果 属性 是 DependencyProperty 并且我无权访问目标,只能访问源,我该如何做?

概述

我们有一个名为 MembershipList 的自定义 ItemsControl,它公开了一个名为 Members 且类型为 ObservableCollection<object> 的 属性。这是一个独立于 ItemsItemsSource 属性的 属性,后者的行为与任何其他 ItemsControl 相同。它是这样定义的...

public static readonly DependencyProperty MembersProperty = DependencyProperty.Register(
    "Members",
    typeof(ObservableCollection<object>),
    typeof(MembershipList),
    new PropertyMetadata(null));

public ObservableCollection<object> Members
{
    get { return (ObservableCollection<object>)GetValue(MembersProperty); }
    set { SetValue(MembersProperty, value); }
}

我们正在尝试做的是为 Items/ItemsSource 中也出现在 Members 中的所有成员设置不同的样式。换句话说,我们试图突出两个列表的交集。

请注意 Members 可能包含根本不在 Items/ItemsSource 中的项目。这就是为什么我们不能简单地使用 multi-select ListBox 的原因,其中 SelectedItems 必须是 Items/ItemsSource 的子集。在我们的用法中,情况并非如此。

另请注意,我们不拥有 Items/ItemsSourceMembers collection,因此我们不能简单地添加 IsMember 属性 到项目并绑定到它。另外,这无论如何都是一个糟糕的设计,因为它会将项目限制为属于一个成员。考虑其中十个控件的情况,它们都绑定到相同的 ItemsSource,但具有十个不同的成员资格 collections.

也就是说,考虑以下绑定(MembershipListItemMembershipList 控件的容器)...

<Style TargetType="{x:Type local:MembershipListItem}">
    <Setter Property="IsMember">
        <Setter.Value>

            <MultiBinding Converter="{StaticResource MembershipTest}">
                <Binding /> <!-- Passes the DataContext to the converter -->
                <Binding Path="Members" RelativeSource="{RealtiveSource AncestorType={x:Type local:MembershipList}}" />
            </MultiBinding>

        </Setter.Value>
    </Setter>
</Style>

这很简单。当 Members 属性 更改时,该值通过 MembershipTest 转换器传递,结果存储在目标 [=167= 上的 IsMember 属性 中].

但是,如果在 Members collection 中添加或删除项,绑定当然 不会 更新,因为 collection 实例本身没有改变,只有它的内容。

在我们的例子中,我们确实希望它 re-evaluate 进行此类更改。

我们考虑向 Count 添加额外的绑定...

<Style TargetType="{x:Type local:MembershipListItem}">
    <Setter Property="IsMember">
        <Setter.Value>

            <MultiBinding Converter="{StaticResource MembershipTest}">
                <Binding /> <!-- Passes the DataContext to the converter -->
                <Binding Path="Members" RelativeSource="{RealtiveSource AncestorType={x:Type local:MembershipList}}" />
                <Binding Path="Members.Count" FallbackValue="0" />
            </MultiBinding>

        </Setter.Value>
    </Setter>
</Style>

...这很接近,因为现在跟踪添加和删除,但是如果您用一个项目替换另一个项目,这将不起作用,因为计数不会改变。

我还尝试创建一个 MarkupExtension 在返回实际绑定之前在内部订阅 Members collection 的 CollectionChanged 事件,认为我可以使用前面提到的事件处理程序中的 BindingExpression.UpdateTarget() 方法调用,但问题是我没有目标 object 从中获取 BindingExpression 以从内部调用 UpdateTarget() ProvideValue() 覆盖。换句话说,我知道我必须告诉某人,但我不知道该告诉谁。

但即使我这样做了,使用这种方法你会很快 运行 遇到问题,你将手动订阅容器作为 CollectionChanged 事件的侦听器目标,这会在容器开始时引起问题得到虚拟化,这就是为什么最好只使用一个绑定,当一个容器被回收时,它会自动并正确地获得 re-applied。但是你又回到了这个问题的开始,即无法告诉绑定更新以响应 CollectionChanged 通知。

解决方案 A - 对 CollectionChanged 个事件使用第二个 DependencyProperty

一个可行的解决方案是创建一个任意的 属性 来表示 CollectionChanged,将其添加到 MultiBinding,然后在您想要刷新绑定时更改它.

为此,我首先创建了一个名为 MembersCollectionChanged 的布尔值 DependencyProperty。然后在 Members_PropertyChanged 处理程序中,我订阅(或取消订阅)CollectionChanged 事件,并在该事件的处理程序中,我切换 MembersCollectionChanged 属性 刷新 MultiBinding.

这是代码...

public static readonly DependencyProperty MembersCollectionChangedProperty = DependencyProperty.Register(
    "MembersCollectionChanged",
    typeof(bool),
    typeof(MembershipList),
    new PropertyMetadata(false));

public bool MembersCollectionChanged
{
    get { return (bool)GetValue(MembersCollectionChangedProperty); }
    set { SetValue(MembersCollectionChangedProperty, value); }
}

public static readonly DependencyProperty MembersProperty = DependencyProperty.Register(
    "Members",
    typeof(ObservableCollection<object>),
    typeof(MembershipList),
    new PropertyMetadata(null, Members_PropertyChanged)); // Added the change handler

public int Members
{
    get { return (int)GetValue(MembersProperty); }
    set { SetValue(MembersProperty, value); }
}

private static void Members_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var oldMembers = e.OldValue as ObservableCollection<object>;
    var newMembers = e.NewValue as ObservableCollection<object>;

    if(oldMembers != null)
        oldMembers.CollectionChanged -= Members_CollectionChanged;

    if(newMembers != null)
        oldMembers.CollectionChanged += Members_CollectionChanged;
}

private static void Members_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
    // 'Toggle' the property to refresh the binding
    MembersCollectionChanged = !MembersCollectionChanged;
}

Note: To avoid a memory leak, the code here really should use a WeakEventManager for the CollectionChanged event. However I left it out because of brevity in an already long post.

这是使用它的绑定...

<Style TargetType="{x:Type local:MembershipListItem}">
    <Setter Property="IsMember">
        <Setter.Value>

            <MultiBinding Converter="{StaticResource MembershipTest}">
                <Binding /> <!-- Passes in the DataContext -->
                <Binding Path="Members" RelativeSource="{RealtiveSource AncestorType={x:Type local:MembershipList}}" />
                <Binding Path="MembersCollectionChanged" RelativeSource="{RealtiveSource AncestorType={x:Type local:MembershipList}}" />
            </MultiBinding>

        </Setter.Value>
    </Setter>
</Style>

这确实有效,但阅读代码的人并不完全清楚其意图。此外,它需要为每种类似的用法在控件(此处为 MembersCollectionChanged)上创建一个新的任意 属性,从而使您的 API 变得混乱。也就是说,从技术上讲它确实满足要求。这样做感觉很脏。

解决方案 B - 使用 INotifyPropertyChanged

另一个使用 INotifyPropertyChanged 的解决方案如下所示。这使得 MembershipList 也支持 INotifyPropertyChanged。我将 Members 更改为标准 CLR-type 属性 而不是 DependencyProperty。然后,我在 setter 中订阅了它的 CollectionChanged 事件(如果存在,则取消订阅旧事件)。然后这只是在 CollectionChanged 事件触发时为 Members 引发 PropertyChanged 事件的问题。

这是代码...

private ObservableCollection<object> _members;
public ObservableCollection<object> Members
{
    get { return _members; }
    set
    {
        if(_members == value)
            return;

        // Unsubscribe the old one if not null
        if(_members != null)
            _members.CollectionChanged -= Members_CollectionChanged;

        // Store the new value
        _members = value;

        // Wire up the new one if not null
        if(_members != null)
            _members.CollectionChanged += Members_CollectionChanged;

        RaisePropertyChanged(nameof(Members));
    }
}

private void Members_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    RaisePropertyChanged(nameof(Members));
}

Again, this should be changed to use the WeakEventManager.

这似乎适用于页面顶部的第一个绑定,并且非常清楚它的意图。

但是,问题仍然是让 DependencyObject 首先也支持 INotifyPropertyChanged 接口是否是个好主意。我不确定。我还没有发现任何说这是不允许的,我的理解是DependencyProperty 实际上提出了自己的更改通知,而不是 DependencyObject 它是 applied/attached 所以他们不应该发生冲突。

巧合的是,这也是为什么您不能简单地实现 INotifyCollectionChanged 接口并为 DependencyProperty 引发 PropertyChanged 事件的原因。如果在 DependencyProperty 上设置了绑定,它根本不会收听 object 的 PropertyChanged 通知。什么都没发生。它充耳不闻。要使用 INotifyPropertyChanged,您必须将 属性 作为标准 CLR 属性 来实现。这就是我在上面的代码中所做的,同样有效。

我只是想了解如何在不实际更改值的情况下为 DependencyProperty 引发 PropertyChanged 事件,如果可能的话。我开始认为它不是。

第二个选项,您的集合也实现了 INotifyPropertyChanged,是解决这个问题的好方法。它很容易理解,代码并不真正 'hidden' 任何地方,它使用所有 XAML 开发人员和您的团队熟悉的元素。

第一个解决方案也很不错,但如果它没有得到很好的注释或记录,一些开发人员可能很难理解它的用途,甚至在 a) 出现问题或 b) 他们需要复制该控件在其他地方的行为。

由于两者都有效,而且您的问题实际上是代码的 'readability',请选择最易读的代码,除非其他因素(性能等)成为问题。

因此请选择解决方案 B,确保它是适当的 commented/documented 并且您的团队了解您解决此问题的方向。