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 CollectionExtensions
class 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 ObservableCollection
for 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 ItemTemplate
of 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();
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.