c# - 是否可以强制基于DependencyProperty的绑定(bind)以编程方式重新评估?

标签 c# wpf xaml dependency-properties inotifypropertychanged

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,即使关联的属性没有发生变化,我也可以简单地通过引发带有该属性名称的PropertyChanged事件来重新评估绑定(bind)。如果该属性是DependencyProperty而我无权访问目标,而只能访问源,我该怎么做?

概述

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

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

也就是说,请考虑以下绑定(bind)(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集合中或从中删除,则绑定(bind)当然不会更新,因为集合实例本身并未更改,仅其内容已更改。

就我们而言,我们确实希望它重新评估这种变化。

我们考虑这样向Count添加额外的绑定(bind)。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
<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,在返回实际绑定(bind)之前内部订阅了CollectionChanged集合的Members事件,以为我可以在事件处理程序中使用前面提到的BindingExpression.UpdateTarget()方法调用,但是问题是我没有目标从中获取BindingExpression的对象,以便从UpdateTarget()覆盖内调用ProvideValue()。换句话说,我知道我必须告诉某人,但我不知道该告诉谁。

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

解决方案A-对DependencyProperty事件使用第二个CollectionChanged
一个可行的可行解决方案是创建一个代表CollectionChanged的任意属性,将其添加到MultiBinding中,然后在每次您想要刷新绑定(bind)时进行更改。

为此,我首先在此处创建了一个名为DependencyProperty的 bool 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;
}

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.



这是使用它的绑定(bind)...
<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事件(并取消订阅旧的)。然后,只需在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));
}

Again, this should be changed to use the WeakEventManager.



这似乎可以与页面顶部的第一个绑定(bind)一起很好地工作,并且很清楚它的意图是什么。

但是,问题仍然存在,首先让DependencyObject也支持INotifyPropertyChanged接口(interface)是一个好主意。我不确定。我还没有发现任何表示不允许的内容,我的理解是DependencyProperty实际上会引发自己的更改通知,而不是应用/附加的DependencyObject,因此它们不应冲突。

巧合的是,这就是为什么您不能简单地实现INotifyCollectionChanged接口(interface)并引发PropertyChangedDependencyProperty事件的原因。如果在DependencyProperty上设置了绑定(bind),则它根本不会监听对象的PropertyChanged通知。没发生什么事。它落在耳朵上。要使用INotifyPropertyChanged,您必须将该属性实现为标准CLR属性。这就是我在上面的代码中所做的,再次可以正常工作。

我只想找出如何做就相当于可以为PropertyChanged引发DependencyProperty事件而实际上不更改值(如果可能的话)。我开始认为不是。

最佳答案

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

第一个解决方案也很不错,但是如果未对其进行很好的注释或记录,则某些开发人员可能会难以理解它的目的,或者甚至在以下情况时就知道它的存在:a)出现问题或b)他们需要将该控件的行为复制到其他地方。

由于这两者都起作用,而实际上是代码的“可读性”成为您的问题,因此,除非其他因素(性能等)成为问题,否则应以最具可读性的方式进行。

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

关于c# - 是否可以强制基于DependencyProperty的绑定(bind)以编程方式重新评估?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32812948/

相关文章:

wpf - 从 xaml 中的样式 setter 属性获取值

c# - 如何将不同的查询结果绑定(bind)到组合框并将其作为选定单元格值传递

xaml - Visual Studio 2017 Xamarin Form 预览失败并出现 System.Exception - 无法呈现 Android XAML 预览

c# - Azure 耐用功能。编排会产生许多不需要的事件

c# - 如果有镜面激光忽略其他物体

c# - 使用边框的 WPF Microsoft Bing 控件 SetView

wpf - 基于祖先类型的存在设置样式

c# - Unity 5.x : Where's the RegisterType<TFrom, TTo>?

C# Smtp 以附件形式发送后删除文件;因为正在使用而被拒绝?

c# - UI 线程 vs 后台线程 - UI 控制可访问性边界