Collection views in WPF

We’ve seen (for simple binding and also using converters to convert from one data type to another) how to use data binding to keep a dependency property of a control synchronized with the value of a field in an object. These examples, however, worked only when the data to which we were binding consisted of a single object. In real life, we often wish to bind to a list of data objects, so we’ll start having a look at that here.

We’ll use as an example the case where our data consists of a list of books. The interface displays the properties of a single book at a time, but we wish to be able to step through the list and display the properties of each book in order. As a bonus, we would also like to sort the books, and apply a filter to the list to pick out the cheapest books. The interface we’ll be using looks like this:

The buttons have the following effects:

  • First: go to the first book in the list.
  • Previous: go to the book immediately preceding the current book.
  • Next: go to the book immediately following the current book.
  • Last: go to the last book in the list.
  • Add: add a new book to the list.
  • Sort: sort the books in ascending order by price.
  • Cheap: display only those books with a price under 20.00.

The interface was constructed in Expression Blend and uses standard techniques. We won’t go into the interface here, but if you want a look, download the code (link at end of the post) and look at the MainWindow.xaml file.

Before we get into the code, we need to explain some of the theory behind data binding with lists. As you can see from the interface, the controls (TextBoxes) bind to the properties of only one book at a time, so somehow we need a way of extracting this information from the overall list. Also, we need a way of keeping track of where we are in the list so that the Previous and Next buttons work properly.

If all we were concerned with was creating a data structure which stores a list of books, we would create a Book class containing author, title and price fields, and then create a list of Books by using WPF’s built-in generic List type, as in List<Book>. We might then think that we’d be faced with having to write a lot of code to keep track of a position in the list (as well as do the sorting and filtering that we want in this case). However, WPF provides a magical interface called ICollectionView the implementation of which does all this for us. So what exactly is a collection view?

A collection view, as its name implies, provides a customizable view of a collection, such as a list. The job of the collection view is to allow us to move around the list, as well as performing other tasks such as sorting and filtering. As it’s a view, it provides a read-only interface to the list, so it’s not possible to change anything in the original list by means of the collection view.

A collection view is automatically created for us when we bind to a List object. That is, the act of defining a List object as a data context will create a collection view, and it is in fact this view that we bind to rather than the original List object itself. Once the view has been created, we can manipulate our position in the list, as well as sort and filter the list, by using methods of the view, rather than methods of the original list. At all times, it is the CurrentItem of the view that is used as the single data item within the list to which controls are bound. Thus changing the CurrentItem (for example, by stepping through the list) changes which item is bound to a control, with the result that the values displayed by the control change as we change the CurrentItem.

Let’s see how all this works in practice. First, we define the Book class, which must implement INotifyPropertyChanged since ultimately it is a Book object to which the controls bind. At this level, data binding works just the same way as in our earlier examples where we considered binding to a single object. The Book class should thus look familiar if you’ve read the earlier posts:

  class Book : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;
    protected void Notify(string propName)
    {
      if (this.PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs(propName));
      }
    }

    string author;

    public string Author
    {
      get { return author; }
      set
      {
        if (author.Equals(value)) { return; }
        author = value;
        Notify("Author");
      }
    }

    string title;

    public string Title
    {
      get { return title; }
      set { if (title.Equals(value)) { return; }
        title = value;
        Notify("Title");
      }
    }

    decimal price;

    public decimal Price
    {
      get { return price; }
      set { if (price == value) { return; }
        price = value;
        Notify("Price");
      }
    }

    public Book() { }
    public Book(string author, string title, decimal price)
    {
      this.author = author;
      this.title = title;
      this.price = price;
    }
  }

Note that we’re using C#’s decimal data type to store the price, since we don’t want any round-off errors.

Next, we’ll look at creating a list of Books in which we can store a number of Books. However, rather than use the List generic type, we use an ObservableCollection instead. The reason for this is that this class implements the INotifyCollectionChanged interface, which means that any changes to the list (such as adding or deleting items) notify any controls bound to the list so they can update themselves. Apart from that difference, we can treat an ObservableCollection pretty much like a List. The relevant code from the MainWindow class is:

    public MainWindow()
    {
      this.InitializeComponent();
      InitializeLibrary(4);
      LayoutRoot.DataContext = library;
      UpdateButtonStates();
    }

    ObservableCollection<Book> library;
    private void InitializeLibrary(int numBooks)
    {
      library = new ObservableCollection<Book>();
      for (int i = 1; i <= numBooks; i++)
      {
        library.Add(new Book("Author " + i, "Title " + i, BookPrice()));
      }
    }

    Random rand = new Random();
    private decimal BookPrice()
    {
      decimal price = rand.Next(0, 5000) / 100m;
      return price;
    }

If you look in MainWindow.xaml, you’ll see that each of the three TextBoxes has its Text property bound to one of the fields in Book (that is, the Author TextBox is bound to the “Author” property, and so on).

We initialize the list of books by calling InitializeLibrary(). This merely generates a number of books with an author of “Author <number>” and a title of “Title <number>”; the price is a random value between 0.01 and 50.00.

On line 5, we define library as the data context of LayoutRoot (which is the Grid layout on which the interface is built). Since library is an ObservableCollection<Book>, this automatically creates a collection view which is wrapped around the list, and which will be used to determine which Book from the library is used as the current item to which the controls are bound. For example, if the current item is set to the first item in the list, then it is the first Book (the one with Author 1, etc) that is used in the binding to the three TextBoxes.

If we ran the code at this point (without even defining click event handlers for the buttons), the TextBoxes would display the properties from the first Book in library. In order to add functionality to the buttons and allow us to step through the list of Books in the library, we need to acquire and use the collection view.

First, how do we get hold of the collection view in the first place? There is a static method which allows this to be done:

    private CollectionView GetLibraryView()
    {
      return (CollectionView)CollectionViewSource.GetDefaultView(library);
    }

We need to provide an explicit cast to CollectionView, since the GetDefaultView() method is defined only as returning something that implements the ICollectionView interface. The CollectionView class is a WPF class that implements this interface and is in fact what GetDefaultView() returns. CollectionView also provides a few properties that aren’t defined in the ICollectionView interface, so it’s useful to deal with the class explicitly rather than just with the interface.

Note that GetDefaultView() does not create the collection view; that is done when the library is assigned as the data context. GetDefaultView() merely retrieves the collection view object that already exists. Thus successive calls to GetDefaultView() don’t erase any of the changes to the view that other method calls might make.

The first method that uses some of the view properties is the UpdateButtonStates() method that we call from the constructor above:

    private void UpdateButtonStates()
    {
      CollectionView libraryView = GetLibraryView();
      previousButton.IsEnabled = libraryView.CurrentPosition > 0;
      nextButton.IsEnabled = libraryView.CurrentPosition < libraryView.Count - 1;
    }

The view maintains a CurrentPosition marker which points at the location of the CurrentItem. Note that this is not necessarily the same as the position of the item in the original list, since a view can sort or filter the list items and generate a new order for the original items. All of this is done without affecting the data or its order in the original list; the view maintains a separate catalogue of the elements of the list and their new order.

UpdateButtonStates() disables the Previous button if CurrentPosition is 0. It also disables the Next button if the CurrentPosition is at the end of the view of the list. (Note that the Count property is not provided by ICollectionView; it is something that is added in the CollectionView class, which is one reason why we deal with the class explicitly.)

We can now add event handlers for the four buttons that move around the list:

    private void firstButton_Click(object sender, RoutedEventArgs e)
    {
      CollectionView libraryView = GetLibraryView();
      libraryView.MoveCurrentToFirst();
      UpdateButtonStates();
    }

    private void previousButton_Click(object sender, RoutedEventArgs e)
    {
      CollectionView libraryView = GetLibraryView();
      libraryView.MoveCurrentToPrevious();
      UpdateButtonStates();
    }

    private void nextButton_Click(object sender, RoutedEventArgs e)
    {
      CollectionView libraryView = GetLibraryView();
      libraryView.MoveCurrentToNext();
      UpdateButtonStates();
    }

    private void lastButton_Click(object sender, RoutedEventArgs e)
    {
      CollectionView libraryView = GetLibraryView();
      libraryView.MoveCurrentToLast();
      UpdateButtonStates();
    }

These should all be fairly obvious, since we’re using the methods from CollectionView to move around the list.

Next, we look at how we can sort the list. The event handler for the Sort button is:

    private void sortButton_Click(object sender, RoutedEventArgs e)
    {
      CollectionView libraryView = GetLibraryView();
      if (libraryView.SortDescriptions.Count != 0)
      {
        libraryView.SortDescriptions.Clear();
        sortButton.Content = "Sort";
      }
      else
      {
        libraryView.SortDescriptions.Add(new SortDescription("Price", ListSortDirection.Ascending));
        sortButton.Content = "Clear";
      }
      libraryView.MoveCurrentToFirst();
      UpdateButtonStates();
    }

A collection view maintains a list of SortDescriptions, which are applied in order. In this case, the Sort button is a toggle between a sorted and unsorted list. If there are SortDescriptions present (which would result in the list being sorted), we clear any existing SortDescriptions, which restores the list to its original order. If there are no SortDescriptions, then we add one which sorts the books by their price, in ascending order. In each case, we change the button’s caption to show what pressing it again will do.

Note that we don’t need to call a Sort() method to actually do the sorting; the simple act of adding a SortDescription to the view causes sorting to take place. If we had added a second SortDescription, then any books with the same price would be sorted according to the second SortDescription, and so on for further SortDescriptions. Remember that sorting affects only the order of the Books in the view; it does not change the order of the data in the original list.

Filtering is done by applying a Predicate (a condition which must be either true or false) to the list of Books, and retaining only those Books for which the Predicate is true. Again, note that filtering affects only the view of the list, and does not delete anything from the original list. Thus we could apply one filter that selects books with a price under 20.00, and then apply another filter for books over 20.00. Each filter would alter the view of the list, but not the list itself. The code for the filter is:

    private void filterButton_Click(object sender, RoutedEventArgs e)
    {
      CollectionView libraryView = GetLibraryView();
      if (libraryView.Filter != null)
      {
        libraryView.Filter = null;
        filterButton.Content = "Cheap";
      }
      else
      {
        libraryView.Filter = new Predicate<object>(book => ((Book)book).Price < 20);
        filterButton.Content = "All";
      }
      libraryView.MoveCurrentToFirst();
      UpdateButtonStates();
    }

Again, we are treating the filter button as a toggle between a filtered and unfiltered list. We’ve used the Predicate generic type defined in recent versions of C# (see here). The argument to the Predicate constructor tests if the Book’s price is less than 20. Again, all we need do is define the filter; we don’t need to call any explicit Filter() method.

Finally, we look at adding an element to the original list. Since a collection view doesn’t allow us to change the original list, we need to do this by dealing with the ObservableCollection<Book> object directly. Once we’ve added the new Book, we can then re-acquire the collection view for the new list. Because we defined the library as an ObservableCollection<Book>, any additions we make will be automatically notified to controls bound to the library, so the displays will be updated without the need for any further code.

    private void addButton_Click(object sender, RoutedEventArgs e)
    {
      int newBookNum = library.Count + 1;
      library.Add(new Book("Author " + newBookNum, "Title " + newBookNum, BookPrice()));
      CollectionView libraryView = GetLibraryView();
      libraryView.MoveCurrentToLast();
      UpdateButtonStates();
    }

This little program should give you a fair idea of what the collection view can do. There are a few other features as well, so have a stroll through the documentation to see what you can do. One thing this post doesn’t address, however, is how to display more than one element from a list at the same time, so we’ll get to that later.

Code available here.

Advertisements
Post a comment or leave a trackback: Trackback URL.

Comments

Trackbacks

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: