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!
Using INotifyPropertyChanged
I can make a binding re-evaluate even when the associated property hasn't changed simply by raising a PropertyChanged
event with that property's name. How can I do the same if instead the property is a DependencyProperty
and I don't have access to the target, only the source?
We have a custom ItemsControl
called MembershipList
which exposes a property called Members
of type ObservableCollection<object>
. This is a separate property from the Items
or ItemsSource
properties which otherwise behave identical to any other ItemsControl
. It's defined like such...
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); }
}
What we are trying to do is style all members from Items
/ItemsSource
which also appear in Members
differently from those which don't. Put another way, we're trying to highlight the intersection of the two lists.
Note that Members
may contain items which are not in Items
/ItemsSource
at all. That fact is why we can't simply use a multi-select ListBox
where SelectedItems
has to be a subset of Items
/ItemsSource
. In our usage, that is not the case.
Also note we do not own either the Items
/ItemsSource
, or the Members
collections, therefore we can't simply add an IsMember
property to the items and bind to that. Plus, that would be a poor design anyway since it would restrict the items to belonging to one single membership. Consider the case of ten of these controls, all bound to the same ItemsSource
, but with ten different membership collections.
That said, consider the following binding (MembershipListItem
is a container for the MembershipList
control)...
<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>
It's pretty straight forward. When the Members
property changes, that value is passed through the MembershipTest
converter and the result is stored in the IsMember
property on the target object.
However, if items are added to or removed from the Members
collection, the binding of course does not update because the collection instance itself hasn't changed, only its contents.
In our case, we do want it to re-evaluate for such changes.
We considered adding an additional binding to Count
as such...
<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>
...which was close since additions and removals are now tracked, but this doesn't work if you replace one item for another since the count doesn't change.
I also attempted to create a MarkupExtension
that internally subscribed to the CollectionChanged
event of the Members
collection before returning the actual binding, thinking I could use the aforementioned BindingExpression.UpdateTarget()
method call in the event handler, but the problem there is I don't have the target object from which to get the BindingExpression
to call UpdateTarget()
on from within the ProvideValue()
override. In other words, I know I have to tell someone, but I don't know who to tell.
But even if I did, using that approach you quickly run into issues where you would be manually subscribing containers as listener targets to the CollectionChanged
event which would cause issues when the containers start to get virtualized, which is why it's best to just use a binding which automatically and correctly gets re-applied when a container is recycled. But then you're right back to the start of this problem of not being able to tell the binding to update in response to the CollectionChanged
notifications.
DependencyProperty
for CollectionChanged
eventsOne possible solution which does work is to create an arbitrary property to represent the CollectionChanged
, adding it to the MultiBinding
, then changing it whenever you want to refresh the binding.
To that effect, here I first created a boolean DependencyProperty
called MembersCollectionChanged
. Then in the Members_PropertyChanged
handler, I subscribe (or unsubscribe) to the CollectionChanged
event, and in the handler for that event, I toggle the MembersCollectionChanged
property which refreshes the MultiBinding
.
Here's the code...
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.
And here's the binding to use it...
<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>
This did work but to someone reading the code isn't exactly clear of its intent. Plus it requires creating a new, arbitrary property on the control (MembersCollectionChanged
here) for each similar type of usage, cluttering up your API. That said, technically it does satisfy the requirements. It just feels dirty doing it that way.
INotifyPropertyChanged
Another solution using INotifyPropertyChanged
is show below. This makes MembershipList
also support INotifyPropertyChanged
. I changed Members
to be a standard CLR-type property instead of a DependencyProperty
. I then subscribe to its CollectionChanged
event in the setter (and unsubscribe the old one if present). Then it's just a matter of raising a PropertyChanged
event for Members
when the CollectionChanged
event fires.
Here is the code...
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
.
This seems to work fine with the first binding at the top of the page and is very clear what it's intention is.
However, the question remains if it's a good idea to have a DependencyObject
also support the INotifyPropertyChanged
interface in the first place. I'm not sure. I haven't found anything that says it's not allowed and my understanding is a DependencyProperty
actually raises its own change notification, not the DependencyObject
it's applied/attached to so they shouldn't conflict.
Coincidentally that's also why you can't simply implement the INotifyCollectionChanged
interface and raise a PropertyChanged
event for a DependencyProperty
. If a binding is set on a DependencyProperty
, it isn't listening to the object's PropertyChanged
notifications at all. Nothing happens. It falls on deaf ears. To use INotifyPropertyChanged
, you have to implement the property as a standard CLR property. That's what I did in the code above, which again, does work.
I'd just like to find out how you can do the equivalent of raising a PropertyChanged
event for a DependencyProperty
without actually changing the value, if that's even possible. I'm starting to think it isn't.
The CoerceValueCallback for a dependency property is invoked any time that the property system or any other caller calls CoerceValue on a DependencyObject instance, specifying that property's identifier as the dp . Changes to the property value may have come from any possible participant in the property system.
Windows Presentation Foundation (WPF) provides a set of services that can be used to extend the functionality of a type's property. Collectively, these services are referred to as the WPF property system. A property that's backed by the WPF property system is known as a dependency property.
The second option, where your collection also implements INotifyPropertyChanged
, is a good solution for this problem. It's easy to understand, the code isn't really 'hidden' anywhere and it uses elements familiar to all XAML devs and your team.
The first solution is also pretty good, but if it's not commented or documented well, some devs may struggle to understand it's purpose or even know that it's there when a) things go wrong or b) they need to copy the behaviour of that control elsewhere.
Since both work and it's really the 'readability' of the code that is your problem, go with the most readable unless other factors (performance, etc) become a problem.
So go for Solution B, make sure that it is appropriately commented/documented and that your team are aware of the direction you've gone to solve this problem.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With