WPF 4.5 is available as part of the Visual Studio 11 Developer Preview released at
the Build 2011 conference, and is part of the .NET Framework
version 4.5. WPF 4.5 addresses several important customer requests
including the ability to have cross-thread change notification for
collections - the topic of this post.
Update 1/20/2012: I have
posted a set of updates to this post with additional
information about the details, as well as a few code changes. Be
sure to read that post as well as this one.
The Problem
As I explained in my post on cross-thread change notification, Silverlight
and WPF have the concept of the UI thread and background (or
secondary) threads. Code on a background thread is not allowed to
interact with UI elements (controls, etc.) on the foreground
thread. You can find more information about the general problem in
that post.
What happens when you need to add and remove items from a
collection from that background thread? That's pretty common in
applications which must keep a list updated (stock trading is an
obvious example). When the collection raises the change
notification events, the binding system and listeners, on the UI
thread, have code executed. Remember, an event is just a delegate,
or really, just a function pointer. So, essentially, the collection
is calling a function on the UI thread. That's not allowed, and it
throws an exception.
The approach pre-4.5 was to dispatch all calls to add items to
the collection. The diagram from the Silverlight post applies
here.
If code on the background thread needs to access elements on the
UI thread, set a property for example, it needs to dispatch the
call to the UI thread using the dispatcher or synchronization
context. Get enough of this happening, such as when constantly
updating a collection, and you can run into some real performance
problems.
The 4.5 Solution
We actually have a good solution for this in WPF 4.5. The
BindingOperations class includes the
EnableCollectionSynchronization function. This function helps the
binding system and the collection intelligently handle change
notification.
Let's look at an example. The application, when run, will look
like this:
The stocks in the ListBox are loaded by clicking the "Load"
button. To test the change notification, click the Keep Loading
button. For this example, the Keep Updating button doesn't do
anything (more on that when I cover in-place sorting and updating
in another post)
XAML
Just for grins, and to show its inclusion in 4.5, I've put the
functionality into another new addition to WPF proper: the Ribbon.
I didn't use commands although they are supported.
<RibbonWindow x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Ribbon>
<RibbonTab Header="First Tab">
<RibbonGroup Header="Group 1">
<RibbonButton x:Name="Foo" Label="Foo!" />
<RibbonButton x:Name="Bar"
Label="Bar!" />
</RibbonGroup>
<RibbonGroup Header="Group 2">
<RibbonButton x:Name="Load"
Click="Load_Click"
Label="Load" />
<RibbonButton x:Name="KeepLoading"
Click="KeepLoading_Click"
Label="Keep loading" />
<RibbonButton x:Name="KeepUpdating"
Label="Keep Updating" />
</RibbonGroup>
<RibbonGroup Header="Count">
<TextBlock Text="{Binding Stocks.Count}" />
</RibbonGroup>
</RibbonTab>
<RibbonTab Header="Second Tab">
</RibbonTab>
<RibbonTab Header="Stuff that doesn't go anywhere else">
</RibbonTab>
</Ribbon>
<Grid Margin="0,156,0,0">
<TextBlock VerticalAlignment="Top"
FontSize="20"
Text="WPF Rules" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox ItemsSource="{Binding Stocks}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.CacheLengthUnit="Page"
VirtualizingPanel.CacheLength="2,2"
VirtualizingPanel.ScrollUnit="Item">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Symbol}"
Grid.Column="0" />
<TextBlock Text="{Binding Value}"
Grid.Column="1" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</Grid>
</RibbonWindow>
For this example, the primary pieces to pay attention to are the
ListBox which is bound to the stocks, and the ribbon buttons to
Load and Update the stocks collection. Those use the Stock model
item via the ViewModel.
The Model
The model consists of a single class, called Stock.
using System.ComponentModel;
namespace WpfApplication2.Model
{
class Stock : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _symbol;
public string Symbol
{
get { return _symbol; }
set { _symbol = value; NotifyPropertyChanged("Symbol"); }
}
private decimal _value;
public decimal Value
{
get { return _value; }
set { _value = value; NotifyPropertyChanged("Value"); }
}
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The ViewModel
The ViewModel is where the action is. First the listing, and
then the explanation.
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Data;
using WpfApplication2.Model;
namespace WpfApplication2.ViewModel
{
class MainViewModel
{
public ObservableCollection<Stock> Stocks { get; set; }
private object _stocksLock = new object();
public MainViewModel()
{
Stocks = new ObservableCollection<Stock>();
BindingOperations.EnableCollectionSynchronization(Stocks, _stocksLock);
}
private Random _random = new Random();
public void AddNewItems()
{
for (int i = 0; i < 100; i++)
{
var item = new Stock();
for (int j = 0; j < _random.Next(2, 4); j++)
{
item.Symbol += char.ConvertFromUtf32(_random.Next(
char.ConvertToUtf32("A", 0),
char.ConvertToUtf32("Z", 0)));
}
item.Value = (decimal)(_random.Next(100, 6000) / 100.0);
Stocks.Add(item);
Debug.WriteLine(item.Symbol);
}
}
public void StartAddingItems()
{
Task.Factory.StartNew(() =>
{
while (true)
{
AddNewItems();
Thread.Sleep(500);
}
});
}
}
}
When the MainViewModel is instantiated, the collection is
created. Next, I call the EnableCollectionSynchronization method,
providing it the collection and an object to use as a lock. The
lock object is nothing special, just an instance of the CLR object
type. This single method makes cross-thread collection change
notification possible.
The StartAddingItems method spins up another thread, using the
Task parallel library, which then loads items into the collection.
The data is randomly generated, but includes a large number of
updates. When you run the application, you'll see that the ListBox
is able to be quickly updated and that there are no cross-thread
exceptions reported.
If you want to cause the exceptions to happen as normal, comment
out the BindingOperations.EnableCollectionSynchronization call.
Summary
WPF 4.5 includes a number of key targeted performance and
capability features, one of which is cross-thread collection change
notification. Enabling the change notification is as simple as the
inclusion of a lock object and a single function call. Versus the
manual dispatching approach, this can be a real performance win,
not to mention save you some coding time.