Is it possible to force a rewrite of a DependencyProperty-based binding for programmatic revaluation?

NOTE. Before you read the topic and immediately mark it as a duplicate, read the entire question to understand what we are doing. Other issues that describe getting the BindingExpression and then calling the UpdateTarget() method UpdateTarget() not work in our use case. Thanks!

TL: DR version

Using INotifyPropertyChanged , I can override the binding, even if the related property has not changed simply by raising the PropertyChanged event with this property name. How can I do the same if the property is DependencyProperty instead and I don’t have access to the target, only the source?

Overview

We have a custom ItemsControl called MembershipList that provides a property called Members type ObservableCollection<object> . This is a separate property from the Items or ItemsSource properties, which otherwise behave the same with any other ItemsControl . It is defined as 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 stylize all the members from Items / ItemsSource , which also appear in Members , unlike those that don't. In other words, we are trying to highlight the intersection of two lists.

Note that Members may contain items that are not in Items / ItemsSource at all. That is why we cannot just use multi-select ListBox , where SelectedItems should be a subset of Items / ItemsSource . In our use, this is not so.

Also note that we do not own the Items / ItemsSource or Members collections, so we cannot just add the IsMember property to the elements and bind to them. Plus, this would be a poor design, since it would limit the elements to membership in one membership. Consider the case of ten of these controls, all associated with the same ItemsSource , but with ten different collections of collections.

However, consider the following binding ( MembershipListItem - 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> 

This is pretty straight forward. When the Members property changes, this value is passed through the MembershipTest , and the result is stored in the IsMember property on the target.

However, if items are added or removed from the Members collection, the course binding is not updated because the collection instance itself has not changed, but only its contents.

In our case, we want him to overestimate 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 add-ons are now added and removed, but this does not work if you replace one item with another, because the score does not change.

I also tried to create a MarkupExtension , which itself subscribed to the CollectionChanged event of the Members collection, before returning the actual binding, assuming that I could use the above BindingExpression.UpdateTarget() method call in the event handler, but the problem is that I don't have the target an object from which you can call BindingExpression to call UpdateTarget() from an override of ProvideValue() . In other words, I know what I have to say to someone, but I do not know what to say.

But even if I did, using this approach, you will quickly encounter problems when you manually sign containers as listener targets for the CollectionChanged event, which can cause problems when containers start to virtualize, so just use a binding that automatically and returns correctly when the container is recycled. But then you will return to the beginning of this problem, not being able to report the update binding in response to CollectionChanged notifications.

Solution A - Using a Second DependencyProperty for CollectionChanged Events

One possible solution that works is to create an arbitrary property for the CollectionChanged view by adding it to the MultiBinding and then changing it when you want to update the binding.

To do this, here I first created a boolean DependencyProperty called MembersCollectionChanged . Then, in the Members_PropertyChanged handler Members_PropertyChanged I signed up (or unsubscribed) for the CollectionChanged event, and in the handler for this event, I switched the MembersCollectionChanged property, which updates MultiBinding .

Here is 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 memory leaks, here the code really should use the WeakEventManager for the CollectionChanged event. However, I left it because of brevity in an already long post.

And here is the binding for using 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> 

It really worked, but someone who reads this code doesn't exactly know its intent. In addition, for each such type of use, you need to create a new custom property for the control ( MembersCollectionChanged here) cluttering up your API. However, technically it satisfies the requirements. He just feels dirty doing it this way.

Solution B - Use INotifyPropertyChanged

Another solution using INotifyPropertyChanged is shown below. This means that the MembershipList also supports INotifyPropertyChanged . I changed Members as a standard property of the CLR type instead of DependencyProperty . Then I subscribe to his CollectionChanged event in the installer (and unsubscribe from the old one, if any). Then it's just a matter of creating a PropertyChanged event for Members with the CollectionChanged event.

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 needs to be changed to use the WeakEventManager .

This seems to work well with the first binding at the top of the page and it is very clear what this intent is.

However, the question remains, if it is nice to have DependencyObject , also to support the INotifyPropertyChanged interface in the first place. I'm not sure. I did not find anything that says this is not allowed, and my understanding is that DependencyProperty actually calls its own notification of the change, not the DependencyObject that it applies / attached so that they do not conflict.

By the way, why can't you just implement the INotifyCollectionChanged interface and raise the PropertyChanged event for DependencyProperty . If the binding is set to DependencyProperty , it does not listen to notifications about the PropertyChanged object at all. Nothing has happened. He falls on deaf ears. To use INotifyPropertyChanged , you must implement the property as a standard CLR property. This is what I did in the above code, which again works.

I would just like to know how you can do the equivalent of creating a PropertyChanged event for DependencyProperty without actually changing the value, if possible. I'm starting to think that this is not so.

+6
source share
1 answer

The second option, when your collection also implements INotifyPropertyChanged , is a good solution to this problem. It's easy to see that the code is not “hidden” anywhere, and it uses elements that are familiar to all XAML developers and your team.

The first solution is also very good, but if it is not commented on or well documented, some developers may try to understand its purpose or even know that it is there when a) everything goes wrong, or b) they need to copy the behavior that control is different location.

Since both work, and it is really the “readability” of the code that is your problem, go with the most readable if other factors (performance, etc.) do not become a problem.

So, go to Solution B, make sure it is properly commented / documented, and that your team is aware of the direction you went to solve this problem.

+1
source

All Articles