Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#

ObservableCollection notification on member change - How to ObservableCollectionEx

Rate me:
Please Sign up or sign in to vote.
4.25/5 (7 votes)
30 Sep 2013CPOL3 min read 68.8K   906   15   4
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 <code>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

C#
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

C#
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

C#
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).

C#
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.

C#
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

XML
<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.

C#
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer
Portugal Portugal
Database and Silverlight applications designer and developer.

Comments and Discussions

 
Questionanother example of extending observable collection Pin
Member 128056361-May-17 0:45
Member 128056361-May-17 0:45 
BugHorrible performance Pin
Athari30-Sep-13 14:25
Athari30-Sep-13 14:25 
Your ToListBox2Converter value converter recreates the whole collection whenever any property of any collection item is changed. As long as you have only 8 items, it won't cause serious issues, but it would greatly hinder performance if there's a considerable number of items. It may also cause issues with selection, but I'm not sure.

The ObservableCollectionEx class you're using is fine, but it is absolutely not suited for filtering like this.

The CollectionExtensions class is redundant in your sample, because it's enough to call ToList as collections are recreated every time and observability capabilities of collections returned from ToListBox2Converter are not used. It merely causes redundant event subscriptions.
“Today is the first day of the rest of your life.”

AnswerRe: Horrible performance Pin
Jorge J. Martins30-Sep-13 22:40
professionalJorge J. Martins30-Sep-13 22:40 
GeneralRe: Horrible performance Pin
ISanti1-Oct-13 7:13
ISanti1-Oct-13 7:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.