Dapfor .Net Grid Introduction
Part1: Data Types
Part2: Data Binding
Part3: Event-driven model
Part4: Data formatting and presentation
Part5: Threadsafety
Part6: Threadsafe BindingList
Part7: Appearance and painting
Part8: Cell highlighting
Part9: Data filtering
Part10: Data sorting
Part11: Data grouping
Part12: Headers and columns
Part13: Editors
Part14: Performance. Practical recommendations
Part15: Real-time blotter
Part16: Currency converter
Part17: Instrument browser
Part18: Order book
Part19: Basket Viewer
.Net Grid Tutorial Part3: Event-driven model
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 with a data object property. For example, if we have Product class, Label control can be bound to Name property of this control.
class Product
{
private readonly string _name;
public Product(string name)
{
_name = name;
}
public string Name
{
get { return _name; }
}
}
Product product = new Product("Some product");
label1.DataBindings.Add(new Binding("Text", product, "Name"));
In grids, Product class objects can be stored in collections such as BindingList<T>. When the 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, whose setter fires a notification on value change over the specified interface.
class Product : INotifyPropertyChanged
{
private readonly string _name;
private double _price;
public Product(string name)
{
_name = name;
}
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 and if they implement
INotifyPropertyChanged interface, the collection subscribes to every object inside it. When the 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 implementation of notification, BindingList handler has serious performance issues (see this for more information). Dapfor.Net.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.
After the purpose of INotifyPropertyChanged and IBindingList interfaces has become clear, let’s look at grid's reaction 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 via grid binding to collections via Grid.DataSource/Row.DataSource or by adding objects via Grid.Rows.Add()/Row.Add() 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 Control.Invoke() /Control.BeginInvoke() 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 a value used for grouping 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 look at the example of data updating in real-time.
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");
}
}
}
private void FirePropertyChanged(string field)
{
if(PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(field));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Objects of these classes can be added to the grid one by one:
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.
Product product1 = new Product("Product 1");
Product product2 = new Product("Product 2");
Product product3 = new Product("Product 3");
BindingList<Order> orders = new BindingList<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.DataSource = orders;
Now let's add a timer that will simulate data update in real time.
Random random = new Random();
timer = new Timer();
timer.Tick += delegate
{
Order order = orders[random.Next(orders.Count)];
order.Price = random.Next(50, 150) + random.NextDouble();
};
timer.Interval = 500;
timer.Start();
Now
let’s add a filter that displays only the most significant changes (e.g. when value change exceeds 10%) of the initial price. As we can see, nothing has changed from the business logic point of
view.
class DeltaFormat : IFormat
{
public string Format(IDataField dataField)
{
Order order = (Order) dataField.DataAccessor.DataObject;
double delta = Math.Abs(order.Price - order.InitialPrice) / order.InitialPrice;
return string.Format("{0} {1:#} %", order.Price > order.InitialPrice ? "+" : "-", 100 * delta);
}
public bool CanParse(string text, IDataField dataField) { return false; }
public void Parse(string text, IDataField dataField) {}
}
Column unboundColumn = new Column("unboundColumn", "% Change");
unboundColumn.Format = new DeltaFormat();
grid.Headers[0].Add(unboundColumn);
class Order : INotifyPropertyChanged
{
...
private readonly double _initialPrice;
public Order(Product product, double price, long quantity)
{
...
_initialPrice = price;
}
...
public double InitialPrice
{
get { return _initialPrice; }
}
public double Price
{
get { return _price; }
set
{
if (_price != value)
{
_price = value;
FirePropertyChanged(string.Empty);
FirePropertyChanged("Price");
}
}
}
...
public event PropertyChangedEventHandler PropertyChanged;
}
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;
});
Let’s add grouping and sorting
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;
The event-driven data model works equally well with or without any types of hierarchies described in .Net Grid tutorial (Part2: Data binding).
It also supports declarative hierarchy building when any hierarchy levels can be updated.
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.DataSource = orders;
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. Receiving data from TCP/IP is a typical example. For demonstration purposes let's replace System.Windows.Forms timer with a timer operating in non-GUI thread:
System.Threading.Timer timer = new System.Threading.Timer(delegate
{
Order order = orders[random.Next(orders.Count)];
order.Price = random.Next(50, 200) + random.NextDouble();
}, null, 0, 500);
The grid will still receive and handle 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 is capable of processing many thousands of notifications per second.