While developing the grid we paid serious attention to application thread safety. Hardware developers often face limitation of possible performance improvement by increasing processor frequency and solve this problem by threading workloads and increasing the number of processor cores. Today many processors have 4-6 cores, and tomorrow they will have 16, 32 or even more cores. Single-threaded software that uses only one core can’t provide high performance by definition. However, threading makes application development harder and may easily cause application crashes or deadlocks.
All Wpf controls are single-threaded, which means that their methods can be called only in thread where they are running, i.e. the GUI thread. This thread also processes all window messages. To make sure that application will run safely, the programmer has to synchronize threads. There are two types of synchronization – synchronous and asynchronous. These types are available via Dispatcher..::..Invoke(Delegate, array<Object>[]()[][]) and Dispatcher..::..BeginInvoke(Delegate, array<Object>[]()[][]) methods accordingly.
Writing an application that correctly supports multi-threading is a very complicated task. Errors in some parts of code may dramatically impact the application as whole. It is also very important to understand thread safety implementation, especially in such complex components as Wpf Grid.
Most Wpf GridControl methods that work with data are thread safe, i.e. such methods as GridControl.Rows.Add(Object) / Row..::..VisibleIndex etc can be called from any thread. However, it is not the most important grid feature in multi-threaded applications. The most important new feature is thread-safe handling of notifications coming from IBindingList and INotifyPropertyChanged interfaces. These interfaces are very important for data binding and enable separation of application logic from its presentation. In other words, logic shouldn’t know how it is presented GUI. There are various popular patterns of application implementation such as MVC or MVVM, but neither of these patterns considers that logic may operate in non-GUI thread. All data binding methods send business logic notifications to data presentation layer in business logic thread.
To demonstrate the importance of implementing synchronization in data presentation layer, let’s review an example of an application that gets quote changes, deals and other info from some market. All these events arrive in non-GUI thread. In event-driven model the business logic notifies GUI of these events. However, without proper synchronization with GUI thread the application won’t work correctly. When data binding is used, GUI components are bound to data sources (e.g. to IBindingList). Therefore, to send a notification via IBindingList..::..ListChanged or INotifyPropertyChanged..::..PropertyChanged, business logic should perform synchronization (i.e. call Dispatcher..::..Invoke(Delegate, array<Object>[]()[][]) / Dispatcher..::..BeginInvoke(Delegate, array<Object>[]()[][])). This requires a link to the control in the business logic. It is evident that the principle of keeping the data layer independent of the presentation layer is violated here:
XAML | Copy |
---|---|
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <DataGrid AutoGenerateColumns="True" Name="dataGrid" /> </Grid> </Window> |
C# | Copy |
---|---|
//Simple feed item public class Feed : INotifyPropertyChanged { private decimal _price; public Feed(string name, decimal price) { Name = name; _price = price; } public string Name { get; private set; } public decimal Price { get { return _price; } set { if (_price != value) { _price = value; if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("Price")); } } } } public event PropertyChangedEventHandler PropertyChanged; } //Thread-safe feed collection public class FeedCollection : BindingList<Feed> { private readonly Dispatcher _dispatcher; public FeedCollection(Dispatcher dispatcher) { _dispatcher = dispatcher; } protected override void OnListChanged(ListChangedEventArgs e) { //Synchronize and fire the notification in the UI thread _dispatcher.Invoke(new Action(() => base.OnListChanged(e))); } } // Interaction logic for MainWindow.xaml public partial class MainWindow : Window { private readonly BindingList<Feed> _feeds; public MainWindow() { InitializeComponent(); //If an item is added to unsafe BindingList, the NotSupportedException is raised //_feeds = new BindingList<Feed>(); //FeedCollection synchronizes threads _feeds = new FeedCollection(Dispatcher); //Add some items to the collection _feeds.Add(new Feed("Feed1", 10)); _feeds.Add(new Feed("Feed2", 20)); ////Bind the grid to the source dataGrid.ItemsSource = _feeds; //Add a new item in the non-UI thread ThreadPool.QueueUserWorkItem(delegate { _feeds.Add(new Feed("Feed3", 30)); }); } } |
Wpf GridControl is different. When the grid receives notifications from INotifyPropertyChanged and IBindingList, it performs synchronization with its own thread, thus removing the need to perform synchronization at the data layer and to implement unnecessary references to graphical controls.
XAML | Copy |
---|---|
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Controls="clr-namespace:Dapfor.Wpf.Controls;assembly=Dapfor.Wpf" Title="Dapfor Wpf Grid: Threadsafety" Height="200" Width="405"> <Grid> <Controls:GridControl Name="dataGrid"> <Controls:GridControl.Headers> <Controls:Header ScrollType="Stretch"> <Controls:Header.Columns> <Controls:Column Id="Name" Title="Name" /> <Controls:Column Id="Price" Title="Price" /> </Controls:Header.Columns> </Controls:Header> </Controls:GridControl.Headers> </Controls:GridControl> </Grid> </Window> |
C# | Copy |
---|---|
// Interaction logic for MainWindow.xaml public partial class MainWindow : Window { private readonly BindingList<Feed> _feeds = new BindingList<Feed>(); public MainWindow() { InitializeComponent(); //Add some items to the collection _feeds.Add(new Feed("Feed1", 10)); _feeds.Add(new Feed("Feed2", 20)); ////Bind the grid to the source dataGrid.ItemsSource = _feeds; //Add a new item in the non-UI thread ThreadPool.QueueUserWorkItem(delegate { _feeds.Add(new Feed("Feed3", 30)); }); //Update item in the non-UI thread ThreadPool.QueueUserWorkItem(delegate { _feeds[1].Price = 40; }); } } |
Now let’s look at Wpf GridControl actions after synchronization. When a notification is sent the GUI thread, the grid redraws changed elements, moves rows to the required positions if sorting is used, regroups rows if necessary and verifies filtering conditions hiding or displaying rows as required.
The data layer doesn’t see any of those operations. Its only task is to send notifications to the grid. Let’s note that the same data source (IBindingList) can be simultaneously bound to multiple grids with different hierarchy, grouping, sorting and filtering. However, data is still independent of its presentation, including multi-threading.
In effect, this approach greatly simplifies application programming as it makes it possible to concentrate on application business model implementation and not on complex aspects of synchronizing data with GUI. This enables programmer to avoid application deadlocks and synchronization errors that are hard to discover.
As we have said above, ensuring thread safety is not a trivial task for application developers. Complex modern applications may contain a lot of assemblies. Some of them may contain codes with graphical controls, others may contain business logic, various math libraries, code for TCP/IP interaction, etc. However, limitations related to GUI operation only in one thread and thread synchronization method require unneeded and dangerous dependencies of business logic from graphical components (Dispatcher..::..Invoke(Delegate, array<Object>[]()[][]) / Dispatcher..::..BeginInvoke(Delegate, array<Object>[]()[][]))). This may seriously violate the principle of business logic independence from its presentation. Dapfor Wpf GridControl doesn’t just enable thread synchronization, but also makes it possible to completely avoid such dependencies using an event-driven model. It means that if the application is well architected, business logic assemblies will not (and should not!) depend on Dapfor assemblies and other Wpf control libraries.