是否可以通过编程方式将基于 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>
的 属性。这是一个独立于 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
中的项目。这就是为什么我们不能简单地使用 multi-select ListBox
的原因,其中 SelectedItems
必须是 Items
/ItemsSource
的子集。在我们的用法中,情况并非如此。
另请注意,我们不拥有 Items
/ItemsSource
或 Members
collection,因此我们不能简单地添加 IsMember
属性 到项目并绑定到它。另外,这无论如何都是一个糟糕的设计,因为它会将项目限制为属于一个成员。考虑其中十个控件的情况,它们都绑定到相同的 ItemsSource
,但具有十个不同的成员资格 collections.
也就是说,考虑以下绑定(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
转换器传递,结果存储在目标 [=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 并且您的团队了解您解决此问题的方向。
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 callingUpdateTarget()
method does not work in our use-case. Thanks!
TL:DR 版本
使用 INotifyPropertyChanged
我可以进行绑定 re-evaluate,即使关联的 属性 没有改变,只需引发 属性 的 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
中的项目。这就是为什么我们不能简单地使用 multi-select ListBox
的原因,其中 SelectedItems
必须是 Items
/ItemsSource
的子集。在我们的用法中,情况并非如此。
另请注意,我们不拥有 Items
/ItemsSource
或 Members
collection,因此我们不能简单地添加 IsMember
属性 到项目并绑定到它。另外,这无论如何都是一个糟糕的设计,因为它会将项目限制为属于一个成员。考虑其中十个控件的情况,它们都绑定到相同的 ItemsSource
,但具有十个不同的成员资格 collections.
也就是说,考虑以下绑定(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
转换器传递,结果存储在目标 [=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 theCollectionChanged
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 并且您的团队了解您解决此问题的方向。