Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ObservableCollection notification on member change - How to ObservableCollectionEx

0.00/5 (No votes)
30 Sep 2013 2  
How to have notification on ObservableCollection, not only when the collection changes but also when any member of any of the collection items is changed. How to take advantage of it in a collection binding.

Introduction

I was urged to write an article about how I make use of this ObservableCollectionEx in a real world situation. Along with it I present my CollectionExtensionsclass to use with the ObservableCollection and ObservableCollectionEx classes.

Background

I was creating a visual query builder where users could create a database query without knowing any SQL. It was to be based on a collection of table columns with a few properties that would allow me to build the SELECT, WHERE, GROUP BY and ORDER BY clauses of the query. For instance, a ListBox would hold the entire list of columns of the db table and in another ListBox the list of columns to be in the SELECT clause. For a column to appear in the second ListBox, thus in the SELECT clause of the query as in any window part where the other clauses where to be build, I thought of using some properties on the Column class and binding the other ListBox’s ItemSource to a filtered sub-collection of the full column collection.

The fact that the whole thing would be based on a single collection would make things easier: a single source of data and the possibility to use a bindable DependencyProperty holding the collection.

The solution would be the use of Value Converters in top of any binging to that base collection of columns info. This worked fine with ObservableCollectionfor the bindings and value converters when items were added or removed from the base collection and with a lot of lines of code to handle events fired by changes in the controls of the ItemTemplateof the ListBox holding the full collection. This event handling would make changes in the collection in such a way that it would fire the CollectionChange event.

This lead to "What if I could have a notification when a member of the collection item is changed?"

I found the answer in the ObservableCollectionEx class.

Simplified example

I created a very simple project exemplifying the use of the ObservableCollectionEx and tried to keep things as simple as possible.

Key points of the project:

  • The ObservableCollectionEx class;
  • The CollectionExtensions class;
  • An Item class with a few members;
  • An Items class to hold a collection of Item;
  • An Items type DependencyProperty;
  • A ListBox to show all items of the base collection;
  • A ListBox to show a filtered collection of the base collection;
  • A Value Converter to filter the base collection.

The code

ObservableCollectionEx

public partial class ObservableCollectionEx<T> : ObservableCollection<T>
    where T : INotifyPropertyChanged
{
    public ObservableCollectionEx() : base() { }

    public ObservableCollectionEx(List<T> list)
        : base((list != null) ? new List<T>(list.Count) : list)
    {
        CopyFrom(list);
    }

    public ObservableCollectionEx(IEnumerable<T> collection)
    {
        if (collection == null)
            throw new ArgumentNullException("collection");
        CopyFrom(collection);
    }

    private void CopyFrom(IEnumerable<T> collection)
    {
        IList<T> items = Items;
        if (collection != null && items != null)
        {
            using (IEnumerator<T> enumerator = collection.GetEnumerator())
            {
                while (enumerator.MoveNext())
                {
                    items.Add(enumerator.Current);
                }
            }
        }
    }  
 
    protected override void InsertItem(int index, T item)
    {
        base.InsertItem(index, item);
        item.PropertyChanged += Item_PropertyChanged;
    }

    protected override void RemoveItem(int index)
    {
        Items[index].PropertyChanged -= Item_PropertyChanged;
        base.RemoveItem(index);
    }

    protected virtual void MoveItem(int oldIndex, int newIndex)
    {
        T removedItem = this[oldIndex];
        base.RemoveItem(oldIndex);
        base.InsertItem(newIndex, removedItem);
    } 

    protected override void ClearItems()
    {
        foreach (T item in Items)
        {
            item.PropertyChanged -= Item_PropertyChanged;
        }
        base.ClearItems();
    }

    protected override void SetItem(int index, T item)
    {
        T oldItem = Items[index];
        T newItem = item;
        oldItem.PropertyChanged -= Item_PropertyChanged;
        newItem.PropertyChanged += Item_PropertyChanged;
        base.SetItem(index, item);
    }

    private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        var handler = ItemPropertyChanged;
        if (handler != null) { handler(sender, e); }
    }

    public event PropertyChangedEventHandler ItemPropertyChanged;
}

CollectionExtensions

public static class CollectionExtensions
{
    public static ObservableCollection<T> ToObservableCollection<T>(
      this IEnumerable<T> enumerableList)
    {
        return enumerableList != null ? new ObservableCollection<T>(enumerableList) : null;
    }

    public static ObservableCollectionEx<T> ToObservableCollectionEx<T>(
      this IEnumerable<T> enumerableList) where T : INotifyPropertyChanged
    {
        return enumerableList != null ? new ObservableCollectionEx<T>(enumerableList) : null;
    }
}

The ToObservableCollection() method is not used in this project but I decided to leave it for it might be useful.

The Item_Class

public class Item_Class : INotifyPropertyChanged
{
    private int id;
    private string desc;
    private bool show_LB2;
    private bool count_TB;

    public int Id
    {
        get
        {
            return this.id;
        }
        set
        {
            if ((this.id != value))
            {
                this.id = value;
                NotifyPropertyChanged("Id");
            }
        }
    }

    public string Desc
    {
        get
        {
            return this.desc;
        }
        set
        {
            if ((this.desc != value))
            {
                this.desc = value;
                NotifyPropertyChanged("Desc");
            }
        }
    }

    public bool Show_LB2
    {
        get
        {
            return this.show_LB2;
        }
        set
        {
            if ((this.show_LB2 != value))
            {
                this.show_LB2 = value;
                NotifyPropertyChanged("Show_LB2");
            }
        }
    }
 
    public bool Count_TB
    {
        get
        {
            return this.count_TB;
        }
        set
        {
            if ((this.count_TB != value))
            {
                this.count_TB = value;
                NotifyPropertyChanged("Count_TB");
            }
        }
    }

    public Item_Class()
    { }

    public Item_Class(int id,
                      string desc,
                      bool show_LB2,
                      bool count_TB)
    {
        Id = id;
        Desc = desc;
        Show_LB2 = show_LB2;
        Count_TB = count_TB;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(string p)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(p));
        }
    }
}

The Items_Class – A collection of Item_Class

We can say that here is where the magic works. The constructor casts the CollectionChanged event on the Items member and bubbles it to the Items_Class.

The same happens with the ItemPropertyChanged on the Items member. Any change to another member of this class fires the NotifyPropertyChanged normally (see the SomeOtheMember member).

Furthermore, when the Changed event on the Items member are caught you can perform some actions on the other members (here I recalculate a filtered Items count).

public class Items_Class : INotifyPropertyChanged
{
    public Items_Class()
    {
        Items = new ObservableCollectionEx<Item_Class>();
        Items.CollectionChanged += (sender, e) =>
        {
            this.SomeOtheMember = Items.Where(c => c.Count_TB == true).Count();
            NotifyPropertyChanged("Items");
        };
        Items.ItemPropertyChanged += (sender, e) =>
        {
            this.SomeOtheMember = Items.Where(c => c.Count_TB == true).Count();
            NotifyPropertyChanged("Items");
        };
    }

    public ObservableCollectionEx<Item_Class> Items
    {
        get;
        private set;
    }

    private int someOtheMember;
    public int SomeOtheMember
    {
        get
        {
            return this.someOtheMember;
        }
        set
        {
            if ((this.someOtheMember != value))
            {
                this.someOtheMember = value;
                NotifyPropertyChanged("SomeOtheMember");
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(string p)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(p));
    }
}

The Value Converter

Here is where the full collection gets filtered to serve as the ItemSource for the second ListBox and the use of the ToObservableCollectionEx() comes handy.

public class ToListBox2Converter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null) return null;
        return ((ObservableCollectionEx<Item_Class>)value).Where(c => c.Show_LB2 == true).ToObservableCollectionEx();
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

The MainPage

<UserControl
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:ObservableCollectionExExample" 
    xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" 
    x:Name="userControl" 
    x:Class="ObservableCollectionExExample.MainPage"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400">
 
    <UserControl.Resources>
        <local:ToListBox2Converter x:Key="ToListBox2Converter"/>
        <DataTemplate x:Key="DataTemplate1">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="20"/>
                    <ColumnDefinition Width="80"/>
                    <ColumnDefinition Width="16"/>
                    <ColumnDefinition Width="16"/>
                </Grid.ColumnDefinitions>
                <sdk:Label x:Name="LabelId" Margin="0,0,0,4" 
                  d:LayoutOverrides="Width, Height" Content="{Binding Id}"/>
                <sdk:Label x:Name="LabelDesc" Margin="0,0,0,4" 
                  d:LayoutOverrides="Width, Height" Grid.Column="1" 
                  Content="{Binding Desc}"/>
                <CheckBox x:Name="CheckBoxShow_LB2" Content="" 
                  Margin="0,0,0,3" d:LayoutOverrides="Width, Height" 
                  Grid.Column="2" IsChecked="{Binding Show_LB2, Mode=TwoWay}"/>
                <CheckBox x:Name="CheckBoxCount_TB2" Content="" 
                  Margin="0,0,0,3" d:LayoutOverrides="Width, Height" 
                  Grid.Column="3" IsChecked="{Binding Count_TB, Mode=TwoWay}"/>
            </Grid>
        </DataTemplate>
    </UserControl.Resources>
 
    <Grid x:Name="LayoutRoot" Background="White">
        <Grid.RowDefinitions>
            <RowDefinition/>
            <RowDefinition Height="10"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="10"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <ListBox x:Name="ListBox1" 
          ItemsSource="{Binding ITEMS.Items, ElementName=userControl}" 
          ItemTemplate="{StaticResource DataTemplate1}"/>
        <ListBox x:Name="ListBox2" Grid.Column="2" 
          ItemsSource="{Binding ITEMS.Items, Converter=
            {StaticResource ToListBox2Converter}, ElementName=userControl}" 
          ItemTemplate="{StaticResource DataTemplate1}"/>
        <TextBlock x:Name="TextBox1" HorizontalAlignment="Center" 
          TextWrapping="Wrap" Text="{Binding ITEMS.SomeOtheMember, ElementName=userControl}" 
          VerticalAlignment="Top" Grid.Row="2" Grid.ColumnSpan="3"/>
 
    </Grid>
</UserControl>

ListBox1 binded to ITEMS holds the entire collection of item.ListBox2 binded to ITEMS with the ToListBox2Converter converter holds a collection of items with the Show_LB2 set to true. TextBox1 holds the value of the SomeOtherMember member of ITEMS.

public partial class MainPage : UserControl
{
    private static DependencyProperty ITEMSProperty = 
      DependencyProperty.Register("ITEMS", typeof(Items_Class), 
      typeof(MainPage), new PropertyMetadata(new Items_Class()));
    public Items_Class ITEMS
    {
        get { return (Items_Class)GetValue(ITEMSProperty); }
        set { SetValue(ITEMSProperty, value); }
    }

    public MainPage()
    {
        InitializeComponent();

        //Populate ITEMS
        ITEMS.Items.Add(new Item_Class(1, "desc 1", false, false));
        ITEMS.Items.Add(new Item_Class(2, "desc 2", true, true));
        ITEMS.Items.Add(new Item_Class(3, "desc 3", true, false));
        ITEMS.Items.Add(new Item_Class(4, "desc 4", false, true));
        ITEMS.Items.Add(new Item_Class(5, "desc 5", true, false));
        ITEMS.Items.Add(new Item_Class(6, "desc 6", false, true));
        ITEMS.Items.Add(new Item_Class(7, "desc 7", true, false));
        ITEMS.Items.Add(new Item_Class(8, "desc 8", false, false));
    }
}

Points of Interest

Not that I can really explain it but the TwoWay binding seems to work just fine.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here