Data binding through Linq queries

One of the most talked about classes in WPF is ObservableCollection<T>. This is a collection class that notifies listeners whenever something is added or removed. Examples abound of using an ObservableCollection<Person> within the data model of an application. Add a person to the data model, and the view is updated.

But a problem with ObservableCollection<T> appears when you want to filter, map, or otherwise modify the collection on the way to the view. The desired way to accomplish this is to write a Linq query. But that turns the ObservableCollection<T> into an IEnumerable<T>. While the original source collection is observable, the query is not.

Filtered collections are not observable
In the following example, one list box is bound to People, while another is bound to PeopleStartingWithP. The first list is updated, but the second is not.

public class AddressBook
{
    private ObservableCollection<Person> _people = new ObservableCollection<Person>();

    public ObservableCollection<Person> People
    {
        get { return _people; }
    }

    public IEnumerable<Person> PeopleStartingWithP
    {
        get { return _people.Where(p => p.Name.StartsWith("P")); }
    }

    private Random _random = new Random();
    public void NewPerson()
    {
        _people.Add(new Person() { Name = "Person " + _random.Next(100) });
    }
}
<Window x:Class="AddressBook.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <StackPanel>
        <ListBox ItemsSource="{Binding People}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <ListBox Name="FilteredList" ItemsSource="{Binding PeopleStartingWithP}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Content="New Person" Click="NewPerson_Click"/>
    </StackPanel>
</Window>
public partial class Window1 : Window
{
    private AddressBook _addressBook = new AddressBook();

    public Window1()
    {
        InitializeComponent();
        DataContext = _addressBook;
    }

    private void NewPerson_Click(object sender, RoutedEventArgs e)
    {
        _addressBook.NewPerson();
    }
}

One commonly used solution to this problem is to programmatically set either the DataContext or ItemsSource to force the list to be updated. This works, but it completely defeats the purpose of using ObservableCollection<T>.

private void NewPerson_Click(object sender, RoutedEventArgs e)
{
    _addressBook.NewPerson();
    FilteredList.ItemsSource = _addressBook.PeopleStartingWithP;
}

Writing code that reaches back into the XAML and sets properties is backwards. This is the way things were done in Winforms. XAML is meant to be declarative. The markup should declare its own ItemsSource, and not rely on code to set it.

Query parameters are not observable
Another problem with this approach occurs when the filter in the Linq query references other data. For example, if we want the user to choose their own first letter, the list should update when a new letter is chosen.

The following code makes this work by implementing INotifyPropertyChanged and firing an event when FirstLetter is changed.

private string _firstLetter = string.Empty;

public string FirstLetter
{
    get { return _firstLetter; }
    set { _firstLetter = value; FirePropertyChanged("PeopleStartingWithFirstLetter"); }
}

public IEnumerable<Person> PeopleStartingWithFirstLetter
{
    get
    {
        if (_firstLetter == string.Empty)
            return _people;
        else
            return _people.Where(p => p.Name.StartsWith(_firstLetter));
    }
}

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

Do you see the problem? I've highlighted it for you. FirstLetter fires an event indicating that PeopleStartingWithFirstLetter has changed. That's not the property that was changed! That's the property that was affected by the change. FirstLetter is independent -- the user can change it. PeopleStartingWithFirstLetter is dependent -- it only responds to change. We've created a reverse dependency where FirstLetter knows about PeopleStartingWithFirstLetter. Again, this code is backwards.

Here's my solution
Update Controls makes data binding through linq queries a breeze. It doesn't require ObservableCollection<T>. It responds to changes to the source collection even if it is a plain-vanilla List<T>. And it even responds when the query parameters are changed. And it does all this without INotifyPropertyChanged or any backwards event registration code. Here's what the class looks like using Update Controls:

public class AddressBook
{
    private List<Person> _people = new List<Person>();
    private string _firstLetter = string.Empty;

    private Independent _indPeople = new Independent();
    private Independent _indFirstLetter = new Independent();

    public IEnumerable<Person> People
    {
        get { _indPeople.OnGet(); return _people; }
    }

    public string FirstLetter
    {
        get { _indFirstLetter.OnGet(); return _firstLetter; }
        set { _indFirstLetter.OnSet(); _firstLetter = value; }
    }

    public IEnumerable<Person> PeopleStartingWithFirstLetter
    {
        get
        {
            if (FirstLetter == string.Empty)
                return People;
            else
                return People.Where(p => p.Name.StartsWith(FirstLetter));
        }
    }

    private Random _random = new Random();
    public void NewPerson()
    {
        _indPeople.OnSet();
        _people.Add(new Person() { Name = "Person " + _random.Next(100) });
    }
}

You just need those Independent sentry objects to ride along side your data. Tell them when the data is accessed and changed, and they will notify the controls.

For a more in-depth example, please see the latest video and download the source code.

Leave a Reply

You must be logged in to post a comment.