【发布时间】:2015-12-25 02:27:23
【问题描述】:
注意:在阅读主题并立即将其标记为重复之前,请阅读整个问题以了解我们的目标。描述获取
BindingExpression,然后调用UpdateTarget()方法的其他问题在我们的用例中不起作用。谢谢!
TL:DR 版本
使用INotifyPropertyChanged,我可以重新评估绑定,即使关联的属性没有更改,只需使用该属性的名称引发PropertyChanged 事件。如果属性是 DependencyProperty 并且我无法访问目标,只能访问源,我该怎么做?
概述
我们有一个名为MembershipList 的自定义ItemsControl,它公开了一个名为Members 的ObservableCollection<object> 类型的属性。这是与Items 或ItemsSource 属性分开的属性,这些属性的行为与任何其他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,但具有十个不同的成员集合。
也就是说,考虑以下绑定(MembershipListItem 是 MembershipList 控件的容器)...
<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 事件,并在该事件的处理程序中,我切换刷新MultiBinding 的MembersCollectionChanged 属性。
这是代码...
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属性添加到您的项目类中,可以绑定到ItemsControl的ItemTemplate,而不是创建自定义控件,不是更容易吗?如果您确实需要自定义控件,则CollectionChanged处理程序需要更新受影响项目容器的IsMember属性,但您没有显示任何代码。 -
不,因为我们不控制用于 ItemsSource 或 Membership 集合的项目,就像 ListBox 对您设置的内容没有任何限制一样。您可以在其中抛出字符串实例甚至整数。简而言之,我们必须能够处理任何数据。另外,恕我直言,您提出的设计无论如何都是一个糟糕的设计,因为您现在让该项目知道成员资格。如果它属于多个成员呢?我理论上可以在屏幕上将十个这样的控件全部绑定到同一个 ItemsSource,但有十个不同的 Membership 集合。有意义吗?
-
@MarqueIV,您能否提供Minimal, Complete, and Verifiable example 以查看全貌?
-
Sergey,我在最后添加了 INPC 解决方案,指出了该设计的潜在警告。但我仍然不知道 DependencyObject 上的 INPC 是否是一件好事,因为我似乎找不到任何答案。但是,我没有看到任何危险信号。
标签: c# wpf xaml dependency-properties inotifypropertychanged