【问题标题】:Is it possible to force a binding based on a DependencyProperty to re-evaluate programmatically?是否可以强制基于 DependencyProperty 的绑定以编程方式重新评估?
【发布时间】:2015-12-25 02:27:23
【问题描述】:

注意:在阅读主题并立即将其标记为重复之前,请阅读整个问题以了解我们的目标。描述获取BindingExpression,然后调用UpdateTarget() 方法的其他问题在我们的用例中不起作用。谢谢!

TL:DR 版本

使用INotifyPropertyChanged,我可以重新评估绑定,即使关联的属性没有更改,只需使用该属性的名称引发PropertyChanged 事件。如果属性是 DependencyProperty 并且我无法访问目标,只能访问源,我该怎么做?

概述

我们有一个名为MembershipList 的自定义ItemsControl,它公开了一个名为MembersObservableCollection<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 中的项目。这就是为什么我们不能简单地使用多选ListBox,其中SelectedItems 必须是Items/ItemsSource 的子集。在我们的使用中,情况并非如此。

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

也就是说,考虑以下绑定(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 转换器传递,结果存储在目标对象的IsMember 属性中。

但是,如果在 Members 集合中添加或删除项目,则绑定当然不会更新,因为集合实例本身并没有改变,只有它的内容发生了变化。

在我们的案例中,我们确实希望它重新评估此类更改。

我们考虑添加一个额外的绑定到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 集合的CollectionChanged 事件,我认为我可以在事件处理程序中使用上述BindingExpression.UpdateTarget() 方法调用,但是问题是我没有目标对象来获取BindingExpression 以从ProvideValue() 覆盖中调用UpdateTarget()。换句话说,我知道我必须告诉某人,但我不知道该告诉谁。

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

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

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

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

这是代码...

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;
}

注意:为了避免内存泄漏,这里的代码确实应该为CollectionChanged 事件使用WeakEventManager。但是由于篇幅已经很长,我把它省略了。

这是使用它的绑定...

<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 类型属性,而不是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));
}

同样,这应该改为使用WeakEventManager

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

但是,问题仍然存在,首先让DependencyObject 也支持INotifyPropertyChanged 接口是否是个好主意。我不知道。我没有发现任何说它是不允许的,我的理解是 DependencyProperty 实际上提出了自己的更改通知,而不是 DependencyObject 它被应用/附加到,所以它们不应该冲突。

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

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

【问题讨论】:

  • 能否请您发布使用MembershipList 类的客户端(C# 和XAML)代码,特别是设置适当的数据上下文——Membership 的值(ViewModel-instances?)和ItemsSource 属性?
  • IsMember 属性添加到您的项目类中,可以绑定到ItemsControlItemTemplate,而不是创建自定义控件,不是更容易吗?如果您确实需要自定义控件,则 CollectionChanged 处理程序需要更新受影响项目容器的 IsMember 属性,但您没有显示任何代码。
  • 不,因为我们不控制用于 ItemsSource 或 Membership 集合的项目,就像 ListBox 对您设置的内容没有任何限制一样。您可以在其中抛出字符串实例甚至整数。简而言之,我们必须能够处理任何数据。另外,恕我直言,您提出的设计无论如何都是一个糟糕的设计,因为您现在让该项目知道成员资格。如果它属于多个成员呢?我理论上可以在屏幕上将十个这样的控件全部绑定到同一个 ItemsSource,但有十个不同的 Membership 集合。有意义吗?
  • @MarqueIV,您能否提供Minimal, Complete, and Verifiable example 以查看全貌?
  • Sergey,我在最后添加了 INPC 解决方案,指出了该设计的潜在警告。但我仍然不知道 DependencyObject 上的 INPC 是否是一件好事,因为我似乎找不到任何答案。但是,我没有看到任何危险信号。

标签: c# wpf xaml dependency-properties inotifypropertychanged


【解决方案1】:

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

第一个解决方案也相当不错,但如果没有很好地评论或记录,一些开发人员可能难以理解它的目的,甚至在 a) 出现问题或 b) 他们需要复制控制其他地方。

由于两者都有效,而且代码的“可读性”确实是您的问题,因此除非其他因素(性能等)成为问题,否则请选择最可读的。

因此,请选择解决方案 B,确保对其进行了适当的注释/记录,并且您的团队了解您解决此问题的方向。

【讨论】:

  • 我的想法完全正确。谢谢!
猜你喜欢
  • 2019-04-22
  • 2017-07-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-14
  • 1970-01-01
相关资源
最近更新 更多