Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 Version

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?

Overview

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.

Solution A - Using Second DependencyProperty for CollectionChanged events

One 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 the CollectionChanged 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.

Solution B - Use 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.

like image 872
Mark A. Donohoe Avatar asked Sep 27 '15 21:09

Mark A. Donohoe


People also ask

What is CoerceValueCallback?

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.

What is a dependency property in WPF?

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.


1 Answers

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.

like image 91
pete the pagan-gerbil Avatar answered Sep 26 '22 02:09

pete the pagan-gerbil