INotifyCollectionChanged (Observable Collections) and WPF

.NET Musings

Wandering thoughts of a developer, architect, speaker, and trainer

NAVIGATION - SEARCH

INotifyCollectionChanged (Observable Collections) and WPF

In my previous post (MultiBinding and Command Parameters in WPF), I discussed how you can pass multiple values into a CommandParameter.  In this post, In this post, we will make sure the User Interface get's updated when the Collection changes (items are added or removed), and will need MultiBindings to keep the View Model clean.  If you haven't yet implement INotifyPropertyChanged on your entities, see my previous post on INotifyPropertyChanged and WPF.

We want our ComboBox (or any list control, for that matter) to accurately display the list of items in the collection when said collections change.  We get this functionality by implementing INotifyCollectionChanged (we get this out of the box with the ObservableCollection collection class, but we are going to implement it by hand first).  But before we jump into coding, lets create the framework and the failing test (albeit a UI test).

To do this, we are going to start with the Add Product functionality.  This will build on the previous MultiBinding from the Update Price command, and leverage the same Parameter and MultiValueConverter classes. We first refactor the XAML to move the resource definition to the Window as shown here:

<Window.Resources>
    <ViewModels:ChangeNotificationMultiConverter x:Key="ChangeNotificationMultiConverter"/>
</Window.Resources>

 

We also need to refactor the ChangeNotificationParameter class to include another property for the List itself.  I chose to add another property instead of create a new parameter class since we will be needing all of the fields for the Delete implementation. The new code is shown here:

public class ChangeNotificationParameter
{
    public ContentControl CallBackControl { get; set; }
    public Product Prod { get; set; }
    public IList<Product> Products { get; set; }
}

 

The last refactoring we need to conduct is in the MultiValueConverter itself to take advantage of the new property.

 

The XAML for the new button (placed in the StackPanel with the Update Price button) looks like this (passing in the Products List from the View Model and the Label from the Window):

 

 

<Button Margin="3" Content="Add Product"
                    Command="{Binding Path=AddProductCmd}">
    <Button.CommandParameter>
        <MultiBinding Converter="{StaticResource ChangeNotificationMultiConverter}">
            <MultiBinding.Bindings>
                <Binding Path="Products" />
                <Binding ElementName="CurrentValue" />
            </MultiBinding.Bindings>
        </MultiBinding>
    </Button.CommandParameter>
</Button>

 

The final implementation is for the AddProductCommand, shown here:

private class AddProductCommand : ICommand
{
    public void Execute(object parameter)
    {
        var p = parameter as ChangeNotificationParameter;
        if (p == null || p.Products == null) return;
        p.Products.Add(new Product { ID = 20, ModelName = "New Model", Inventory = 20, Price = 20M, SKU = "1234567890" });
        if (p.CallBackControl != null)
        {
            p.CallBackControl.Content = p.Products.Count.ToString();
        }
    }
    public bool CanExecute(object parameter)
    {
        return true;
    }
    public event EventHandler CanExecuteChanged;
}

 

When we run the app, we see that we have two items in the ComboBox (our two hardcoded products).  Clicking the Add Product button adds a product (confirmed by the label), but our ComboBox still shows only two items.

 

image 

 

To enable the hook up of the notifications, we need to create a custom class that implements INotifyPropertyChanged.  We also want to keep our list as an IList<Product> (our current implementation). 

 

I first create an interface with both interfaces included.  In this manner, I can still code to an interface instead of concrete classes.

public interface IProductList:IList<Product>,INotifyCollectionChanged {}

 

Now for the class itself.  I chose to use the Decorator pattern, since it provided the least friction.  There is still a bit of code to write, but less than other options I considered. The entire class is shown here:

public class ProductList:IProductList
{
    private readonly IList<Product> _products;
    public ProductList(IList<Product> products)
    {
        _products = products;
    }
    public IEnumerator<Product> GetEnumerator()
    {
        return _products.GetEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
    public void Add(Product item)
    {
        _products.Add(item);
        notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add,item));
    }
    public void Clear()
    {
        _products.Clear();
        notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
    public bool Contains(Product item)
    {
        return _products.Contains(item);
    }
    public void CopyTo(Product[] array, int arrayIndex)
    {
        _products.CopyTo(array, arrayIndex);
    }
    public bool Remove(Product item)
    {
        var removed = _products.Remove(item);
        if (removed)
        {
            notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove,item));
        }
        return removed;
    }
    public int Count
    {
        get { return _products.Count; }
    }
    public bool IsReadOnly
    {
        get { return _products.IsReadOnly; }
    }
    public int IndexOf(Product item)
    {
        return _products.IndexOf(item);
    }
    public void Insert(int index, Product item)
    {
        _products.Insert(index, item);
        notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
    public void RemoveAt(int index)
    {
        _products.RemoveAt(index);
        notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
    public Product this[int index]
    {
        get { return _products[index]; }
        set
        {
            _products[index] = value;
            notifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace,_products[index]));
        }
    }
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    private void notifyCollectionChanged(NotifyCollectionChangedEventArgs args)
    {
        if (CollectionChanged != null)
        {
            CollectionChanged(this, args);
        }
    }
}

 

For each of  the methods that alter the collection, we need to raise the CollectionChanged event.  This event has a custom Event Arguments class that takes in its constructor an enumeration of the change that took place (Add, Move, Replace, Reset, or Remove).  (One could argue whether it makes sense to make specific action calls or always declare the action as Reset, but that isn't the purpose of this post.)

 

The ViewModel needs to be changed to utilize the new collection class.  The altered code is shown here:

public ChangeNotificationViewModel()
{
    Products = new ProductList(new ProductService().GetProducts());
}
public ChangeNotificationViewModel(IList<Product> products)
{
    Products = new ProductList(products);
}
public IProductList Products { get; set; }

 

Now when you click to Add Product, the list automatically shows the new product.

 

image

 

Now, to do it the easy way!  Change the ViewModel code to use an Observable Collection, and you can eliminate the entire custom class!

public ChangeNotificationViewModel()
{
    Products = new ObservableCollection<Product>(new ProductService().GetProducts());
}
public ChangeNotificationViewModel(IEnumerable<Product> products)
{
    Products = new ObservableCollection<Product>(products);
}
public IList<Product> Products { get; set; }

 

Much simpler!

 

In my next post, we take a look into IDataErrorInfo and Error Templates in WPF.

 

Happy Coding!

Comments are closed
Managed Windows Shared Hosting by OrcsWeb