是否可以强制基于DependencyProperty的绑定以编程方式重新评估?

Mar*_*eIV 6 c# wpf xaml dependency-properties inotifypropertychanged

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

TL:DR版本

使用INotifyPropertyChanged我可以做一个绑定重新评估时,相关的属性没有被抚养简单地改变甚至PropertyChanged与该属性的名称事件.如果相反属性是a DependencyProperty并且我无权访问目标,只有源代码,我该怎么做?

概观

我们有一个自定义ItemsControl调用MembershipList,它公开了一个名为Memberstype 的属性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); }
}
Run Code Online (Sandbox Code Playgroud)

我们要做的是设置来自Items/的所有成员的样式与不ItemsSource出现的成员Members不同.换句话说,我们试图突出两个列表的交集.

请注意,Members可能包含不在Items/ ItemsSource中的项目.这其实就是为什么我们不能简单地用一个多选ListBox,其中SelectedItems必须的一个子集Items/ ItemsSource.在我们的使用中,情况并非如此.

另请注意,我们不拥有Items/ ItemsSourceMembers集合,因此我们不能简单地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>
Run Code Online (Sandbox Code Playgroud)

这很直接.当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>
Run Code Online (Sandbox Code Playgroud)

...现在跟踪添加和删除是关闭的,但是如果您将一个项目替换为另一个项目,则这不起作用,因为计数不会更改.

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

但即使我这样做,使用这种方法你会很快遇到一些问题,你会手动订阅容器作为CollectionChanged事件的监听器目标,当容器开始虚拟化时会导致问题,这就是为什么最好只使用绑定在回收容器时自动正确地重新应用.但是接下来你就回到了这个问题的开始,即无法告诉绑定更新以响应CollectionChanged通知.

解决方案A - 使用Second DependencyProperty作为CollectionChanged事件

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

为此,我在这里首先创建了一个DependencyProperty名为的布尔值MembersCollectionChanged.然后在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;
}
Run Code Online (Sandbox Code Playgroud)

注意:为了避免内存泄漏,这里的代码确实应该使用a WeakEventManager来表示CollectionChanged事件.然而,由于在已经很长的帖子中简洁,我把它排除了.

这是绑定使用它...

<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>
Run Code Online (Sandbox Code Playgroud)

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

解决方案B - 使用 INotifyPropertyChanged

另一种解决方案INotifyPropertyChanged如下所示.这MembershipList也得到了支持INotifyPropertyChanged.我改为Members标准的CLR类型属性而不是DependencyProperty.然后我CollectionChanged在setter中订阅它的事件(如果存在,则取消订阅旧事件).然后,它只是一个养育之事PropertyChanged事件Members,当CollectionChanged事件触发.

这是代码......

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));
}
Run Code Online (Sandbox Code Playgroud)

再次,这应该改为使用WeakEventManager.

这似乎与页面顶部的第一个绑定工作正常,并且非常清楚它的意图是什么.

但是,问题仍然存在,如果首先DependencyObject支持INotifyPropertyChanged界面是一个好主意.我不确定.我没有找到任何说它不被允许的东西,我的理解DependencyProperty实际上是提出了自己的变更通知,而不是DependencyObject它的应用/附加,因此它们不应该发生冲突.

巧合的是,这也是为什么你不能简单地实现INotifyCollectionChanged界面并为一个PropertyChanged事件引发事件的原因DependencyProperty.如果在a上设置了绑定DependencyProperty,则根本不会监听对象的PropertyChanged通知.什么都没发生.它充耳不闻.要使用INotifyPropertyChanged,您必须将该属性实现为标准CLR属性.这就是我在上面的代码中所做的,它再次起作用.

我想知道你如何能够做到相当于在不实际改变价值PropertyChangedDependencyProperty情况下提高事件,如果可能的话.我开始认为它不是.

pet*_*bil 1

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

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

由于两者都可以工作,并且实际上是代码的“可读性”才是您的问题,因此请选择最具可读性的代码,除非其他因素(性能等)成为问题。

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