7

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 => (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 => (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 => (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 shown 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 => _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.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • Could you please post the client (C# and XAML) code which uses the `MembershipList` class and, in particular, sets the appropriate data context — values (ViewModel–instances?) of the `Membership` and `ItemsSource` properties? – Sergey Vyacheslavovich Brunov Sep 27 '15 at 21:46
  • Wouldn't it be easier to add an `IsMember` property to your item class, that can be bound to in an `ItemsControl`'s `ItemTemplate`, instead of creating a custom control? If you do need the custom control, then the `CollectionChanged` handler needs to update the `IsMember` properties of the affected item containers, but you haven't shown any of that code. – Pieter Witvoet Sep 27 '15 at 21:52
  • No, because we do not control the items used for the ItemsSource or the Membership collections, the same way a ListBox imposes no restrictions on what you set on it. You could throw string instances or even integers in it. In short, we have to be able to work with *any* data. Plus, IMHO what you proposed is a bad design anyway since you now made the item aware of the membership. What if it belonged to multiple memberships? I theoretically could have ten of these controls on the screen all bound to the same ItemsSource, but with ten different Membership collections. Make sense? – Mark A. Donohoe Sep 27 '15 at 21:56
  • @MarqueIV, could you please provide the [Minimal, Complete, and Verifiable example](http://stackoverflow.com/help/mcve) to see the whole picture? – Sergey Vyacheslavovich Brunov Sep 27 '15 at 21:57
  • Sergey, I added the INPC solution at the end, calling out the potential caveats to that design. But I still don't know if INPC on a DependencyObject is a good thing as I can't seem to find any answers to that. I don't however see any red flags. – Mark A. Donohoe Sep 27 '15 at 22:25
  • This is why you don't use `ObservableCollection` as a property type in a UI element. Instead, see how `ItemsControl` deals with this (in it's own source code) using the `ItemsChanged` event and all that – Federico Berasategui Sep 28 '15 at 03:42
  • @MarqueIV yes, I've realized that, a little too late. Deleted the previous comment, I apologize. – Federico Berasategui Sep 28 '15 at 03:43
  • The ObservableCollection isn't really what's used. Internally it's using code exactly the same way as ListBox. That's why I called out this as pseudo-code. Again, the question is about a) raising property change notifications for a collectionChanged event, and b) if you can do something similar to what you do in INPC. – Mark A. Donohoe Sep 28 '15 at 03:43
  • The actual property uses a MembersSource and a Members collection, the same as an ItemsSource and an Items collection. The only difference between what I'm doing and what a multi-select listbox is doing is my Members don't have to actually be in the List, as per my description in the question. – Mark A. Donohoe Sep 28 '15 at 03:45
  • It's designed that way to let the consumer of the control simply set a binding to the MembersSource property, or to add members directly, again, mirroring what a ListBox, TreeView or other ItemsControl does. – Mark A. Donohoe Sep 28 '15 at 03:46
  • @MarqueIV makes sense. But why don't just `var cc = control.Members as INotifyCollectionChanged;` then `if (cc != null) cc.CollectionChanged += ...` in the change handler of your DP? – Federico Berasategui Sep 28 '15 at 03:47
  • I mean, if people are binding your control to an `INotifyCollectionChanged`, why not just handle changes in that? you can un-subscribe from the event upon change of the DP itself again, preventing leaks – Federico Berasategui Sep 28 '15 at 03:49
  • ...which again if you read what I posted is exactly what I'm doing. The problem is that works for INPC. If you wire up CollectionChanged in the change handler, how do you then notify/update the binding in the container's style? Again, I listed all this out in details above. – Mark A. Donohoe Sep 28 '15 at 03:49
  • @MarqueIV I apologize. It would help if you posted actual code, I'm too lazy to read all that to be honest. – Federico Berasategui Sep 28 '15 at 03:50
  • That's why I put the TL:DR section at the beginning. But my advice is you should read it because it addresses everything that you've come at me with here in the comments. It shows why you can and can't do some things. That's why I wrote it. And I did post actual code at the end which shows a working solution, albeit one that uses INPC. I'm trying to find one that uses DPs. Hence this post. – Mark A. Donohoe Sep 28 '15 at 03:51
  • Ahh, now I understand the problem: why is the `IsMember` property a binding *target*? Can't you just LINQ your item containers and set manually, upon collection changed OR DP changed? – Federico Berasategui Sep 28 '15 at 03:52
  • If the list is virtualized, how would you otherwise wire the containers up as targets of the CollectionChanged event? You can't, especially because the containers are recycled. Plus, in reality, this is more akin to a tree structure as well, but that's besides the point. The point is the IsMember needs to be re-evaluated if Members changes, either the item or its contents. One change handler in the base control can publish a PropertyChanged which *all* containers subscribe to via a RelativeSource binding. Again, the INPC version works fine. If that is valid on a DP, that's the answer. – Mark A. Donohoe Sep 28 '15 at 03:55
  • Do you know of the DO/DP equivalent of INPC's PropertyChanged event that I can call for a specific DP? If so, that's the solution. I've been trying to dig in the internals of a DP and its PropertyChanged but haven't found it yet. It has to be pretty straightforward one would think. – Mark A. Donohoe Sep 28 '15 at 03:57
  • @MarqueIV looks like `DependencyPropertyDescriptor` does that internally, and does not seem to expose a way to do it from the outside, other than `SetValue()` – Federico Berasategui Sep 28 '15 at 04:03
  • Yeah, and calling SetValue with the same value of course doesn't raise the change notification (but ironically does still call Coerce, not that that helps me here.) I guess I'll just go with the INPC version I posted since it does work, and as long as I use weak events, there shouldn't be an issue. – Mark A. Donohoe Sep 28 '15 at 04:05
  • @MarqueIV I'm still thinking there should be a better way to do this, though I'm not a control designer so I'll generally handle these things at the VM level. – Federico Berasategui Sep 28 '15 at 04:06
  • @MarqueIV btw, if the list is virtualized, you can trust the Item Container's `DataContextChanged` (and set `IsMember` manually in there). That'd work for both recycling and non-recycling virtualization. – Federico Berasategui Sep 28 '15 at 04:08
  • Again, I went over that because that's exactly what you set with a binding, and that of course works. But how do you notify that binding that the collection's members have changed? Put another way, all I am trying to do is tell those bindings 'Hey, you need to refresh because the collection's contents has changed' but you can't map a binding to an event, only to a property, but there is no property on a INotifyPropertyChanged collection to bind to. Hopefully it's starting to make more sense now. – Mark A. Donohoe Sep 28 '15 at 04:11
  • BTW, that's exactly what you *can* do if you use INPC instead of a DP, which is why I posted that as a possible solution. Again, I encourage you to take a couple minutes and read through what I wrote. – Mark A. Donohoe Sep 28 '15 at 04:12
  • Yeah, but I'm talking about a "pull"-style solution rather than a "push" one. Right now you're trying to push the change notification to the items, what I mean is have the DataContextChanged event in the item container "ask" whether `IsMember` should be true for that particular instance and set it. Maybe this is not an option due to performance considerations.. – Federico Berasategui Sep 28 '15 at 04:15
  • Again, that *is* what it's doing. A container pulls the value when the container gets recycled. That works. What *doesn't* happen is if an external source adds, removes or modifies the Members collection. That has to be a push because what would tell the Containers to re-pull the data? They already got their value from the binding, but the collection changed afterwards. lol... again, please just read the darn question!!! lol lol – Mark A. Donohoe Sep 28 '15 at 04:17
  • Alright. First, apologies for not reading (and paying the proper attention to) your question first. I'm not trying to be annoying (I'm annoying by nature and unintentionally). Second: In the past I've done things that I'm not really proud of, such as putting a `bool ShouldBindingsReevaluate` somewhere and toggling that to let the `MultiBinding`s know they need to reevaluate... Since you can't really call `PropertyChanged` for a DP... this is a (rather hacky and ugly) solution that could work. – Federico Berasategui Sep 28 '15 at 04:23
  • That's actually the solution we came up with before I posted this. And in a lot of ways, that's a very valid solution to forcing a binding to refresh on some arbitrary trigger. But the down-side is that it requires that extra property, and as I'm sure you know, you essentially have to toggle it (or otherwise change it) which means that you could be setting 'false' to a property called 'ShouldBindingsReevaluate' which can be confusing to debug. Either that or you have to make the MultiValueConverter aware of that property and return DoNothing if that value is false, then just bail out. – Mark A. Donohoe Sep 28 '15 at 04:26
  • I like that with INPC you just say 'Raise this property' and you're done. No mucking around with multi-bindings, converters or anything else. It's a shame DPs don't have a way to say 'twiddle the change notification even though technically you yourself hasn't changed.' – Mark A. Donohoe Sep 28 '15 at 04:27
  • lol... and no, I don't think you're annoying. Just lazy! [smirk] In all seriousness, I know I'm pretty damn good at what I do so when I come here, chances are it's for more advanced things. It's hard wording the actual question, which started as just what was in TL:DR, but people want details, then when you give them, they start to redesign your code instead of answering the original question. Happens all the time here. But again, I'm good. I've already gone down a lot of paths. I even have the INPC version working. I was just looking for a DO/DP alternative. – Mark A. Donohoe Sep 28 '15 at 04:29
  • @MarqueIV Off-topic: Am I allowed to be lazy on sunday, at 1:30 AM?... – Federico Berasategui Sep 28 '15 at 04:36
  • Touché! :) (I'm typing these because 'Touché didn't have enough characters to post the comment. lol) – Mark A. Donohoe Sep 28 '15 at 04:37

1 Answers1

1

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.

pete the pagan-gerbil
  • 3,136
  • 2
  • 28
  • 49