Friday, December 23, 2011

WPF 4.5 - Accessing DataBound Collections on Non-UI Thread

WPF has ItemsControl to show a collection of items. In MVVM, ItemsControl is data-bound with a collection in the view model. These collections might take a long time to load when the form is being shown. They might be constantly updated throughout the life cycle of the view. If we do it in the main application thread then it might cost the responsiveness of the application. Hence the need for a background thread for these operations. Well, the world has not been so simple.


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:
  1. AccessCollection
  2. DisableCollectionSynchronization
  3. EnableCollectionSynchronization
This also have some overloads. Let's see how we can fix our little example with these methods. We really can go around by just using EnableCollectionSynchronization. It is to report a collection to be accessible in non-UI threads. In order to provide thread synchronization, we need to provide the synchronization mechanism. Simply we can use the overload which needs lock object. Let's update the view model as follows:
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:

2 comments:

Imadulhaqe said...

I have been visiting your blog for quite some time. Just wanted to appreciate your work. May ALLAH give you more success.

Imad.

Muhammad Shujaat Siddiqi said...

Thanks Imad, It's just for people like yourself. We should be sharing whatever knowledge we have in order to increase that and start the discussion.