0

Suppose I have a basic control with a listbox and a text box, where the listbox is bound to a collection of objects and has a basic data template

<DockPanel LastChildFill="True">
    <TextBlock DockPanel.Dock="Top">Book name</TextBlock>
    <TextBox x:Name="bookNameTextBox" DockPanel.Dock="Top" />
    <TextBlock DockPanel.Dock="Top">Authors</TextBlock>
    <ListBox ItemsSource="{Binding Authors}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Name}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</DockPanel>

public class Author : INotifyPropertyChanged
{
    public string Name { get; set; }
    public ObservableCollection<Book> Books { get; }
}

public class Book : INotifyPropertyChanged
{
    public string Name { get; }
}

What I want to do is have the colours of the items in the listbox change depending on if that author has any books that match the supplied name, for example

Colour = author.Books.Any(b => b.Name.StartsWith(bookNameTextBox.Text)) ? Red : Black;

I initially thought that I could do this using a MultiBinding and a converter, however I couldn't work out how to get the binding to update when items were added to / removed from the books collection, or whent he name of a book changed.

How can I do this in such a way that the colour will update correctly in response to all of the various changes that could affect my logic? e.g.

  • The name of any of the books changing
  • Books being added and removed from the collection
  • The text in the bookNameTextBox text box changing

My MultiBinding looked like this

<TextBlock.Style>
    <Style TargetType="TextBlock">
        <Style.Triggers>
            <DataTrigger Value="True">
                <DataTrigger.Binding>
                    <MultiBinding Converter="{StaticResource MyConverter}">
                        <Binding Path="Books" />
                        <Binding Path="Text" ElementName="bookNameTextBox" />
                    </MultiBinding>
                </DataTrigger.Binding>
                <Setter Property="Foreground" Value="Red" />
            </DataTrigger>
        </Style.Triggers>
    </Style>
</TextBlock.Style>

And my converter (which implemented IMultiValueConverter) looked like this

public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
    var text = (string)values.First(v => v is string);
    var books = (IEnumerable<Book>)values.First(v => v is IEnumerable<Book>);
    return books.Any(b => b.Name.StartsWith(text));
}

This worked however if I then modified any of the books, or added any books the text colour of the list item would not update until the binding was somehow refreshed.

Justin
  • 84,773
  • 49
  • 224
  • 367

3 Answers3

0

I think what you're searching for is Binding Converter, which for every binded item will recieve a call, where you can put your decisional logic and return appropriate result (color in your case).

Tigran
  • 61,654
  • 8
  • 86
  • 123
  • I tried this, however it didn't update the binding when items were added to or removed from the collection or when individual items in the collection were modified. – Justin Sep 13 '12 at 12:17
  • @Justin: it depends on *how* is your binding defined. You can also enforce binidn evalation, if need, so cpnverter will be traced again. – Tigran Sep 13 '12 at 12:21
0

I tried to run your code.

Changing the bookNameTextbox.Text is calling the converter and the result is correct.

There are parts missing from your code.

You don't call the PropertyChanged event. You have to because the view won't get notifications about the changes. So after setting your properties call this event.

Its enough using simple binding. I mean the ObservableCollection keeps update the view when the items in it send the prop change notification correctly.

In this case while using MultiBinding there still something we need - which is pretty weird I think.

According to this q-a when a binding is inside a MultiBinding the ObservableCollection - in this case - needs to raise changed event.

So after modifying the Books collection I called the PropertyChanged event for the Books property and the result was as expected.

Community
  • 1
  • 1
Miklós Balogh
  • 2,164
  • 2
  • 19
  • 26
  • I want to avoid having to explicitly tell the UI to refresh when making changes to the Books collection - after all this is what change notification is for :) – Justin Sep 13 '12 at 14:43
  • @Justin Exactly and you need to notify. Name did not change so it does not notify. – paparazzo Sep 13 '12 at 15:11
0

I've come up with a solution that I'm reasonably happy with loosely based on this StackOverflow question and this linked code sample.

I created an additional class AuthorInfo that inherits from FrameworkElement and put an instance of this class alongside my TextBlock, like so

<DataTemplate>
    <StackPanel>
        <Local:AuthorInfo Collection="{Binding Books}" Text="{Binding Text, ElementName=_bookNameTextBox}" x:Name="_authorInfo" />
        <TextBlock Text="{Binding Name}">
            <TextBlock.Style>
                <Style TargetType="TextBlock">
                    <Style.Triggers>
                        <DataTrigger Value="True" Binding="{Binding BookMatches, ElementName=_authorInfo}">
                            <Setter Property="Foreground" Value="Red" />
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </TextBlock.Style>
        </TextBlock>
    </StackPanel>
</DataTemplate>

This class has dependency properties for both the collection to monitor and the text value to look for and exposes a BookMatches property that indicates whether or not the book matches the supplied string. This is the property that my trigger binds to.

In order to make sure that the property value is updated when the list or items in the list are modified this class keeps track of subscribing to and unsubscribing from the various property changed events - it looks a bit like this

public class AuthorInfo : FrameworkElement
{
    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register("Text", typeof(string), typeof(AuthorInfo), new PropertyMetadata(default(string), PropertyChangedCallback));

    public static readonly DependencyProperty CollectionProperty =
        DependencyProperty.Register("Collection", typeof (IEnumerable), typeof (AuthorInfo), new PropertyMetadata(default(IEnumerable), PropertyChangedCallback));

    private static readonly DependencyPropertyKey ValuePropertyKey =
        DependencyProperty.RegisterReadOnly("Value", typeof (bool), typeof (AuthorInfo), new PropertyMetadata(default(bool)));

    public static readonly DependencyProperty ValueProperty = ValuePropertyKey.DependencyProperty;

    public bool BookMatches
    {
        get
        {
            return (bool) GetValue(ValueProperty);
        }
        set
        {
            SetValue(ValuePropertyKey, value);
        }
    }

    public IEnumerable Collection
    {
        get
        {
            return (IEnumerable)GetValue(CollectionProperty);
        }
        set
        {
            SetValue(CollectionProperty, value);
        }
    }

    public string Text
    {
        get
        {
            return (string)GetValue(TextProperty);
        }
        set
        {
            SetValue(TextProperty, value);
        }
    }

    protected void UpdateValue()
    {
        var books = Collection == null ? Enumerable.Empty<Book>() : Collection.Cast<Book>();
        BookMatches = !string.IsNullOrEmpty(Text) && books.Any(b => b.Name.StartsWith(Text));
    }

    private void CollectionSubscribe(INotifyCollectionChanged collection)
    {
        if (collection != null)
        {
            collection.CollectionChanged += CollectionOnCollectionChanged;
            foreach (var item in (IEnumerable)collection)
            {
                CollectionItemSubscribe(item as INotifyPropertyChanged);
            }
        }
    }

    private void CollectionUnsubscribe(INotifyCollectionChanged collection)
    {
        if (collection != null)
        {
            collection.CollectionChanged -= CollectionOnCollectionChanged;
            foreach (var item in (IEnumerable)collection)
            {
                CollectionItemUnsubscribe(item as INotifyPropertyChanged);
            }
        }
    }

    private void CollectionItemSubscribe(INotifyPropertyChanged item)
    {
        if (item != null)
        {
            item.PropertyChanged += ItemOnPropertyChanged;
        }
    }

    private void CollectionItemUnsubscribe(INotifyPropertyChanged item)
    {
        if (item != null)
        {
            item.PropertyChanged -= ItemOnPropertyChanged;
        }
    }

    private void CollectionOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
    {
        if (args.OldItems != null)
        {
            foreach (var item in args.OldItems)
            {
                CollectionItemUnsubscribe(item as INotifyPropertyChanged);
            }
        }
        if (args.NewItems != null)
        {
            foreach (var item in args.NewItems)
            {
                CollectionItemSubscribe(item as INotifyPropertyChanged);
            }
        }
        UpdateValue();
    }

    private void ItemOnPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        UpdateValue();
    }

    private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
    {
        var aggregator = (AuthorInfo)dependencyObject;
        if (args.Property == CollectionProperty)
        {
            aggregator.CollectionUnsubscribe(args.OldValue as INotifyCollectionChanged);
            aggregator.CollectionSubscribe(args.NewValue as INotifyCollectionChanged);
        }
        aggregator.UpdateValue();
    }
}

Its not that this subscription / unsubscription business is difficult, its just that its a bit fiddly - this way the fiddly change notification stuff is separated from the presentation logic. It should also be easy enough to refactor this out to have all of the change notification in a base class, so that this logic can be re-used for other types of aggregation.

Community
  • 1
  • 1
Justin
  • 84,773
  • 49
  • 224
  • 367