As we know that the UI elements have affinity to the UI thread in WPF. It also does not allow playing with the elements collection bound as DataSource on any thread other than UI thread. And believe me, this really hurts !!! All we had were a few workarounds but no real solution. With WPF 4.5 Developer's preview, the situation improves a little. It is a step forward in the direction for providing these updates in some other thread. Although I do think that the way it is provided could be a little better than that but whatever makes my collection available to a non-UI thread. I don't really mind.
Let's understand this neat feature by creating a simple example. Let's have a simple view with a ListBox. The ListBox is supposed to display the history of signals received from a central server.
We can design the above view in XAML as follows:
<Window x:Class="MVVMCollectionNonUIThread.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:MVVMCollectionNonUIThread" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <local:MainWindowViewModel /> </Window.DataContext> <Grid> <Grid.RowDefinitions> <RowDefinition Height="30" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Border Background="Navy" Grid.Row="0"> <TextBlock Text="WPF 4.5 - Collections Access Across Threads" FontSize="20" Foreground="White" FontWeight="Bold" TextAlignment="Center" VerticalAlignment="Center" /> </Border> <ListBox Margin="3,5,3,5" Grid.Row="1" ItemsSource="{Binding RandomList}" /> </Grid> </Window>The above view has MainWindowViewModel set as the DataContext. It expects the DataContext to have a collection, named RandomList. This collection is data-bound to the ListBox so it should have the history of instances when the signal is received from the server. Now let's start defining the view model as per expectation.
namespace MVVMCollectionNonUIThread { using System.Collections.ObjectModel; using System.Timers; using System.Windows.Data; class MainWindowViewModel { Timer _t1; ObservableCollection<string> _randomList; public ObservableCollection<string> RandomList { get { if (_randomList == null) { _randomList = new ObservableCollection<string>(); } return _randomList; } } public MainWindowViewModel() { _t1 = new Timer(300); _t1.Elapsed += new System.Timers.ElapsedEventHandler(_t1_Elapsed); _t1.Start(); } void _t1_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { RandomList.Add(string.Format("Signal Time : {0}", e.SignalTime)); } } }In order to simulate the signal reception, we have used System.Timers.Timer. The timer would tick after at least 300 ms. This event is handled by _t1_Elapsed. We are just adding an item to RandomList each time the timer ticks. This event handler is to simulate the signal received from the server. Generally, this client / server communication is handled on a different thread like this handler. Since we are following through this post, if we run the application this results in the EXPECTED exception when the item is being added to RandomList in _t1_Elapsed (non-UI thread) [This is generally UNEXPECTED and comes as a surprise if developer doesn't know about it yet.]
FYI: In Winform, we can cause Elapsed event for System.Timers.Timer to be raised in UI thread by setting the SyncrhnizingObject property. We have discussed about this in our timers discussion [http://shujaatsiddiqi.blogspot.com/2010/10/timers-for-net-applications.html]
WPF 4.5 Developer's preview has provided certain new static methods in BindingOperations class to fix this behavior. The list of new methods I could find is as follows:
- AccessCollection
- DisableCollectionSynchronization
- EnableCollectionSynchronization
namespace MVVMCollectionNonUIThread { using System.Collections.ObjectModel; using System.Timers; using System.Windows.Data; class MainWindowViewModel { Timer _t1; object _lockObj = new object(); ObservableCollection<string> _randomList; public ObservableCollection<string> RandomList { get { if (_randomList == null) { _randomList = new ObservableCollection<string>(); } return _randomList; } } public MainWindowViewModel() { BindingOperations.EnableCollectionSynchronization(RandomList, _lockObj); _t1 = new Timer(300); _t1.Elapsed += new System.Timers.ElapsedEventHandler(_t1_Elapsed); _t1.Start(); } void _t1_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { RandomList.Add(string.Format("Signal Time : {0}", e.SignalTime)); } } }Here we just have added BindingOperations.EnableCollectionSynchronization to the view model's contructor for the RandomList collection. We have used the instance member _lockObj as the lock object. That's it! Let's run this now!
Just one thing. The feature is available through BindingOperations available in PresentationFramework assembly. Some developers like to keep their view models in a separate project and they don't like referencing this assembly in that project.
Download: