GridControl tutorial part3: Event-driven model
Binding controls to data object properties
Data binding is used by almost all modern applications. It provides simple means to separate the data layer from the presentation layer. Generally, binding means connecting a graphical control property to a data object property. For example, if we have Product class, Label control can be bound to Name property of this control.
C# |
---|
public class Product { public string Name { get; set; } }
|
Below you can see an example of binding Label control to Product object:
XAML |
---|
<Window x:Class="Test4.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="184" Width="347" xmlns:Test4="clr-namespace:Test4">
<Window.Resources> <Test4:Product x:Key="someProduct" Name="My Product"/> </Window.Resources>
<Grid> <Label Content="{Binding Path=Name, Source={StaticResource someProduct}}" Height="28" HorizontalAlignment="Left" Margin="38,50,0,0" VerticalAlignment="Top" /> </Grid> </Window>
|
Binding data objects to grids
In grids, Product class objects can be stored in collections such as BindingList(T). When a grid is bound to a collection, values returned by Product object properties are displayed in cells.
Let’s see what happens, when values in Product object start changing. Microsoft component model provides INotifyPropertyChanged interface that can notify controls of changes in data objects. Let’s say, Product class has Price property, and setter of this property fires a notification on value change over the specified interface.
C# |
---|
class Product : INotifyPropertyChanged { private readonly string _name; private double _price;
public Product(string name) { _name = name; }
[Field("ProductName")] public string Name { get { return _name; } }
public double Price { get { return _price; } set { if(_price != value) { _price = value; if(PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs("Price")); } } } }
public event PropertyChangedEventHandler PropertyChanged; }
|
When a regular control is bound to Price property, it gets a notification, requests new price value from Product object, and displays this value in the control.
When BindingList(T) collection is created, it checks data types that it will use. If they implement INotifyPropertyChanged interface, the collection subscribes to every object inside it. When a collection receives a notification, it transforms the object to IBindingList.ListChanged with ItemChanged value notifying the grid of changes in the collection. However, due to incorrect notification implementation, BindingList(T) handler has serious performance issues (see this for more information). Dapfor.Wpf.dll library provides an improved implementation of this container that significantly accelerates BindingList(T) performance with objects implementing INotifyPropertyChanged interface and ensures thread safety of the collection.
Now, when the purpose of INotifyPropertyChanged and IBindingList interfaces is clear, let’s see how the grid reacts on notification. When the grid works with data implementing INotifyPropertyChanged interface, it subscribes to changes of every object no matter how it has been added to the grid (either by grid binding to collections via GridControl.ItemsSource / Row.ItemsSource or by adding objects via GridControl.Rows.Add(Object) / Row.Add(Object) methods). When the grid gets a notification from such object, it first checks whether thread synchronization is required. If needed, the grid performs such synchronization using Dispatcher.Invoke(Delegate, Object[]) / Dispatcher.BeginInvoke(Delegate, Object[]) method depending on selected thread model. On the next stage, it checks whether the row has to be moved to a new required position if sorting is used, whether it should be hidden or displayed based on filtering and whether it complies with grouping conditions. If the grouping value has changed, the grid moves the row to the relevant group, creating new groups and removing old groups as necessary.
To demonstrate power and convenience of the event-driven model, let’s review an example of data updating in real-time.
C# |
---|
public class Order : INotifyPropertyChanged { private readonly Product _product; private double _price; private long _quantity;
public Order(Product product, double price, long quantity) { _product = product; _price = price; _quantity = quantity; }
[CompositeField] public Product Product { get { return _product; } }
public double Price { get { return _price; } set { if (_price != value) { _price = value; FirePropertyChanged("Price"); } } }
public long Quantity { get { return _quantity; } set { if (_quantity != value) { _quantity = value; FirePropertyChanged("Quantity"); } } }
public event PropertyChangedEventHandler PropertyChanged;
private void FirePropertyChanged(string field) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(field)); } } }
|
Objects of these classes can be added to the grid one by one:
C# |
---|
Product product = new Product("Some product"); Order order = new Order(product, 123.32, 15); grid.Rows.Add(order);
|
However, the grid usually connects to binding lists.
C# |
---|
Product product1 = new Product("Product 1"); Product product2 = new Product("Product 2"); Product product3 = new Product("Product 3");
ThreadSafeBindingList<Order> orders = new ThreadSafeBindingList<Order>(); orders.Add(new Order(product1, 123.32, 15)); orders.Add(new Order(product2, 110.11, 54)); orders.Add(new Order(product1, 151.93, 46)); orders.Add(new Order(product3, 98.16, 44)); orders.Add(new Order(product2, 165.91, 64)); orders.Add(new Order(product1, 74.96, 77));
grid.ItemsSource = orders;
|
Now let's add a timer that will simulate data update in real time.
C# |
---|
Random random = new Random(); DispatcherTimer timer = new DispatcherTimer(); timer.Tick += delegate { Order order = orders[random.Next(orders.Count)]; order.Price = random.Next(50, 150) + random.NextDouble(); };
timer.Interval = TimeSpan.FromMilliseconds(500); timer.Start();
|
Now let’s add a filter that displays only the most significant changes (e.g. greater than by 10%) of the initial price. As we can see, nothing has changed in the business logic.
C# |
---|
class Order : INotifyPropertyChanged { private readonly double _initialPrice;
public Order(Product product, double price, long quantity) { _initialPrice = price; } public double InitialPrice { get { return _initialPrice; } }
}
grid.Filter = new Filter(delegate(Row row) { Order order = (Order) row.DataObject; double delta = Math.Abs(order.Price - order.InitialPrice); bool bigChange = delta/order.InitialPrice > 0.10; return !bigChange; });
|
The Order object doesn't provide information on price changes. It only contains information on initial and current price. To view the difference, we can use unbound columns feature of the grid. This approach is based on setting a grid column with an identifier that doesn't correspond to any business object field. To view the required information we can use an arbitrary converter that will calculate the price difference and return it as a formatted string value. An example of such converter is shown below.
C# |
---|
public class DeltaConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { Cell cell = (Cell) parameter;
Order order = cell != null && cell.Row != null ? cell.Row.DataObject as Order : null; if (order != null) { double delta = Math.Abs(order.Price - order.InitialPrice) / order.InitialPrice; return string.Format("{0} {1:#} %", order.Price > order.InitialPrice ? "+" : "-", 100 * delta); } return value; }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return Binding.DoNothing; } }
|
Now let's show how to declare an unbound column with the required converter in XAML code.
XAML |
---|
<Window x:Class="TestApplication.Window5" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Dapfor Wpf Grid: unbound column" xmlns:my="clr-namespace:Dapfor.Wpf.Controls;assembly=Dapfor.Wpf" xmlns:TestApplication="clr-namespace:TestApplication" Height="276" Width="588">
<Window.Resources> <TestApplication:DeltaConverter x:Key="customConverter"/> </Window.Resources>
<Grid> <my:GridControl Name="grid"> <my:GridControl.Headers> <my:Header> <my:Header.Columns> <my:Column Id="ProductName" Title="Product" /> <my:Column Id="Quantity" Title="Quantity" /> <my:Column Id="Price" Title="Price" /> <my:Column Id="unboundColumn" Title="% Delta" ValueConverter="{StaticResource customConverter}"/> </my:Header.Columns> </my:Header> </my:GridControl.Headers> </my:GridControl> </Grid> </Window>
|
We have to note that when price changes in a data object, the grid has to change text in multiple cells at once. They are Price and % Delta cells. The Order class doesn't contain a property with a name corresponding to unbound column identifier. To update grid content we can slightly change Price setter code so that when the grid gets a notification from INotifyPropertyChanged interface, it updates text not in a single cell but in the entire row. For this purpose we only have to set property name to null.
C# |
---|
internal class Order : INotifyPropertyChanged { public double Price { get { return _price; } set { if (_price != value) { _price = value;
FirePropertyChanged(null);
FirePropertyChanged("Price"); } } }
public event PropertyChangedEventHandler PropertyChanged;
private void FirePropertyChanged(string field) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(field)); } } }
|
An example of working with unbound column, filter and real-time updates of business objects is shown below.
Now we shall add data sorting and grouping:
C# |
---|
column grid.Headers[0].GroupingEnabled = true; grid.Headers[0]["ProductName"].Grouped = true; grid.Headers[0]["ProductName"].Visible = false;
grid.Headers[0]["ProductName"].SortDirection = SortDirection.Ascending; grid.Headers[0]["Price"].SortDirection = SortDirection.Ascending;
|
Hierarchical binding
The event-driven data model works equally well with or without any types of hierarchies described in Wpf Grid tutorial (Part2: Data binding). It also supports declarative hierarchy building when any hierarchy levels can be updated.
C# |
---|
Row rowCategory = grid.Rows.Add(new string[] { "Some category" }); rowCategory.Expanded = true;
Product product1 = new Product("Product 1"); Product product2 = new Product("Product 2");
...
BindingList<Order> orders = new BindingList<Order>(); orders.Add(new Order(product1, 123.32, 15)); orders.Add(new Order(product2, 110.11, 54));
... rowCategory.ItemsSource = orders; |
Thread safety
Now a few words on thread protection. Since threads firing notifications to the grid are systematically synchronized with the main thread, business object values can be modified from any thread. A typical example is receiving data from TCP/IP. For demonstration purposes let's replace DispatcherTimer with a timer running in non-GUI thread:
C# |
---|
System.Threading.Timer timer = new System.Threading.Timer(state => { Order order = _orders[random.Next(_orders.Count)]; order.Price = random.Next(50, 200) + random.NextDouble(); }, null, 0, 500);
|
The grid still receives and handles data although notifications arrive from a secondary thread. Grids and other GUI controls without such protection generate InvalidOperationException in such cases.
Finally, we come to the issue of performance. When notifications from IBindingList or INotifyPropertyChanged interfaces are received, it doesn’t perform any operations with rows outside the visible area. In other words, the grid has been developed to minimize consumption of CPU resources and can process many thousands notifications per second.
Back to Wpf GridControl tutorial