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 Part 2: Data Binding
Data binding links a data layer with graphical controls and enables data independency of its presentation. Data binding is broadly used in WinForms applications, while in WPF applications it is practically the only data presentation method. Correct organization of data binding is the foundation of well-built applications. Any mistakes at the design stage may turn out costly later on. This tutorial is dedicated to .Net Grid methods of work with data and data binding organization.
All grids use binding to data collections of (IList, IBindingList, IListSource) type as the main and often the only method of working with data. Besides, connection to IBindingList, Dapfor NetGrid significantly expands data binding features enabling operations in unbound mode. Using grid in each of these modes will be reviewed in detail below.
Unbound data in the grid
In this mode, data is not located in a binding list but is added to the grid one by one. Data can be added on any hierarchy level via Row type objects.
grid.Headers.Add(new Header());
grid.Headers[0].Add(new Column("IntValue"));
grid.Headers[0].Add(new Column("DoubleValue"));
grid.Headers[0].Add(new Column("StringValue"));
Row row1 = grid.Rows.Add(new MyCustomClass(10, 11.11, "item 1"));
row1.Add(new MyCustomClass(11, 11.22, "subitem 1"));
row1.Add(new MyCustomClass(12, 11.22, "subitem 2"));
Row row2 = grid.Rows.Add(new MyCustomClass(20, 22.11, "item 2"));
row2.Add(new MyCustomClass(22, 22.22, "subitem 1"));
row2.Add(new MyCustomClass(22, 22.22, "subitem 2"));
.Net Grid makes it easy adding any number of headers, thus changing the presentation from the treelist with a single to multi-header grid.
grid.Headers.Add(new Header());
grid.Headers[1].Add(new Column("DoubleValue"));
grid.Headers[1].Add(new Column("StringValue"));
In the unbound mode .Net Grid supports all data types, described in .Net Grid tutorial (Part1: Data types)
Row row = grid.Rows.Add(new object[] { 10, 11.12, "some string 1" });
IDictionary<string, object> dataObject2 = new Dictionary<string, object>();
dataObject2.Add("IntValue", 20);
dataObject2.Add("DoubleValue", 21.33);
row.Add(dataObject2);
Row childRow = row.Add(new UnboundValueAccessor());
childRow["StringValue"].Value = "some value";
Unbound mode is convenient for creating a grid hierarchy and in situations when data quantity remains unchanged or when data is only added but not removed. At the same time, data is not fully separated from presentation layer as it is necessary to have a reference to grid to add new data or remove it. If data objects implement INotifyPropertyChanged interface, the grid is fully functional event-driven threadsafe control that is controlled by data layer notifications (i.e. automatically updates, highlights, sorts, and regroups rows).
Bound mode
Data contained in IList, IBindingList or IListSource collections. The grid provides Grid.DataSource property for data binding. Bound collections are convenient for adding and removing data and for complete separation of the data layer from the presentation layer. These collections may also contain objects that implement INotifyPropertyChanged interface. Just like in the unbound mode, the grid subscribes to notifications of these objects and therefore becomes an event-driven grid with automated data sorting, filtering and grouping.
The grid provides a lot of options of work with data objects in the bound mode. Let’s first review common work methods that are implemented in any grid (and then let’s consider some specific cases that highlight NetGrid advantages
as compared to other grids)
Data binding without hierarchy creation
It is the easiest method of binding. Data is added to container that implements IList or IBindingList interface. Container choice depends on whether data will be added or removed during application execution. If data quantity is constant, it is recommended to use List<your type> container. If data quantity may change, it is better to choose BindingList<your type> container. (Note that BindingList<T> has serious performance issues when working with objects that implement INotifyPropertyChanged. Click here for more details). All data types, described in .Net Grid tutorial (Part1: Data types) can be used as the data type. They may be either arbitrary classes or UnboundValueAccessor supporting variable number of fields.
grid.Headers.Add(new Header());
grid.Headers[0].Add(new Column("IntValue"));
grid.Headers[0].Add(new Column("DoubleValue"));
grid.Headers[0].Add(new Column("StringValue"));
BindingList<MyCustomClass> bindingList = new BindingList<MyCustomClass>();
bindingList.Add(new MyCustomClass(10, 11.12, "some string 1"));
bindingList.Add(new MyCustomClass(20, 21.33, "some string 2"));
grid.DataSource = bindingList;
Binding to multiple data sources
In real world applications it is often necessary to connect multiple data sources with different data types to the grid simultaneously. It is not easy to do it with standard Microsoft interfaces. Many grid vendors ignore this problem making developers create inheritance relations between a single base type and multiple classes of different types in a single IBindingList. Dapfor NetGrid easily overcomes this limitation using IListSource interface, so it is enough to create a simple implementation of this interface to provide the grid with a collection of data sources, while the grid subscribes to each of the sources and works with them together.
class ListSource : IListSource
{
private readonly IList _sources;
public ListSource(IList source1, IList source2)
{
IList<IList> sources = new List<IList>();
sources.Add(source1);
sources.Add(source2);
_sources = new ReadOnlyCollection<IList>(sources);
}
public IList GetList()
{
return _sources;
}
public bool ContainsListCollection
{
get { return true; }
}
}
BindingList<MyCustomClass> collection1 = new BindingList<MyCustomClass>();
collection1.Add(new MyCustomClass(11, 11.11, "subitem 1"));
collection1.Add(new MyCustomClass(22, 22.22, "subitem 2"));
BindingList<UnboundValueAccessor> collection2 = new BindingList<UnboundValueAccessor>();
collection2.Add(new UnboundValueAccessor());
collection2[0]["StringValue"].Value = "dynamic field";
grid.DataSource = new ListSource(collection1, collection2);
collection1.Add(new MyCustomClass(33, 33.33, "newly added object"));
collection2.Add(new UnboundValueAccessor());
collection2[0]["DoubleValue"].Value = 1.234;
collection2[1]["IntValue"].Value = 100;
collection2[1]["StringValue"].Value = "newly added field";
Data binding and hierarchy (mixing bound and unbound data)
Modern applications require grids that can present data hierarchically. Although data binding is a simple concept, it is hard to use it for hierarchy building since IBindingList has not been intended for storing hierarchy data. There are multiple approaches to using data binding together with hierarchy. Most of these approaches involve expanding BindingList functionality by mixing data of different levels inside the container. It can involve use of primary-foreign keys constraints or inheritance of BindingList<T> base class expanding its functionality. However, these solutions work poorly, consume a lot of CPU and memory resources and are hard to debug. Partially the reason for this is that grids of different vendors don’t work with multiple data collections simultaneously. So, what does Dapfor NetGrid offer?
As it has already been mentioned, Dapfor NetGrid supports both bound and unbound data. It provides access to rows via Grid.Rows or Grid.Nodes accessors without regard to method that has been used to fill the grid. The grid returns objects of Row type. Developers can use NetGrid to easily create hierarchy by calling Row.Add() method that is equivalent to working in the unbound mode.
BindingList<MyCustomClass> collection = new BindingList<MyCustomClass>();
collection.Add(new MyCustomClass(11, 11.11, "item 1"));
collection.Add(new MyCustomClass(22, 22.22, "item 2"));
grid.DataSource = collection;
Row rootRow = grid.Nodes[1].Add(new UnboundValueAccessor());
rootRow["IntValue"].Value = 200;
rootRow["StringValue"].Value = "subitem 1";
Row row2 = grid.Nodes[1].Add(new MyCustomClass(33, 33.33, "subitem 2"));
Besides, every Row object has Row.DataSource property that can be used to connect any data source. In turn, the grid will be subscribed to events of this source and process them by performing required synchronization with GUI thread.
Row row1 = grid.Rows.Add(new UnboundValueAccessor());
row1["IntValue"].Value = 200;
row1["StringValue"].Value = "subitem 1";
Row row2 = grid.Rows.Add(new MyCustomClass(33, 33.33, "subitem 2"));
BindingList<MyCustomClass> collection = new BindingList<MyCustomClass>();
collection.Add(new MyCustomClass(11, 11.11, "item 1"));
collection.Add(new MyCustomClass(22, 22.22, "item 2"));
row1.DataSource = collection;
The above method may be convenient, and is a good addition to other hierarchy building methods.
Declarative hierarchical binding
As it has been shown above, Net Grid can work with various data sources including hierarchical ones. At the same time, IBindingList doesn’t contain hierarchy information. After deep analysis of numerous applications we have made a conclusion that data objects may contain hierarchical information by themselves. There are a lot of such examples, i.e. author writing various numbers of articles or books, a department with employees, a financial index (e.g. CAC40 containing 40 largest French enterprises), etc. Therefore, this information may already be contained in an application business layer. For example, author-books relation can be expressed as follows:
class Author
{
private readonly IList<Book> _books = new List<Book>();
private readonly string _name;
public Author(string name)
{
_name = name;
}
public IList<Book> Books
{
get { return _books; }
}
public string Name
{
get { return _name; }
}
}
class Book
{
private readonly Author _author;
private readonly string _name;
public Book(Author author, string name)
{
_author = author;
_name = name;
}
public Author Author
{
get { return _author; }
}
public string Title
{
get { return _name; }
}
}
A list of authors stored in IBindingList can be bound to the grid.
BindingList<Author> authors = new BindingList<Author>();
Author author = new Author("Agata Kristi");
author.Books.Add(new Book(author, "Second front"));
author.Books.Add(new Book(author, "Shameful Star"));
authors.Add(author);
author = new Author("Conan Doyle");
author.Books.Add(new Book(author, "The Blanched Soldier"));
author.Books.Add(new Book(author, "The Mazarin Stone"));
authors.Add(author);
grid.DataSource = authors;
However, hierarchy won’t be created as the grid has no information of author-books hierarchy structure. Dapfor’s framework provides an attribute to inform the grid about it by marking Author.Books as having a special significance for hierarchy building.
class Author
{
...
[HierarchicalField]
public IList<Book> Books
{
get { return _books; }
}
...
}
Now let’s consider a case when an author writes a new book that should be added to the collection.
author.Books.Add(new Book(author, "His Last Bow"));
Nothing happens, as the book collection is represented by List<Book> data type. If this collection is replaced with BindingList<Book>, the grid will get notifications of any changes in collection and automatically display them. The grid also checks whether sorting, filtering or grouping is required, i.e. the application has sufficiently complex behaviour (grouping, sorting and filtering), while the developer needs to implement only a few simple classes.
class Author
{
private readonly IList<Book> _books = new BindingList<Book>();
...
}
Two important notes:
The grid supports combinations of any data types. For example, instead of arbitrary class of Book type, it is possible to use Dictionary<string, object> or UnboundValueAccessor with variable number of fields.
class Author
{
private readonly IList<UnboundValueAccessor> _books = new BindingList<UnboundValueAccessor>();
private readonly string _name;
public Author(string name)
{
_name = name;
}
[HierarchicalField]
public IList<UnboundValueAccessor> Books
{
get { return _books; }
}
public string Name
{
get { return _name; }
}
}
BindingList<Author> authors = new BindingList<Author>();
Author author = new Author("Agata Kristi");
authors.Add(author);
UnboundValueAccessor book = new UnboundValueAccessor();
book["Title"].Value = "Second front";
book["Genre"].Value = "Detective story";
author.Books.Add(book);
book = new UnboundValueAccessor();
book["Title"].Value = "Shameful Star";
book["Genre"].Value = "Detective story";
author.Books.Add(book);
author = new Author("Conan Doyle");
authors.Add(author);
grid.DataSource = authors;
The second note is that data can be added to the grid in multiple ways:
grid.Rows.Add(...);
grid.DataSource = ...;
or at any available hierarchy level, e.g., like this:
Row row = grid.Rows.Add(new object[] { "Detective stories" });
row.DataSource = authors;
In all cases an Author-Books hierarchy shall be built starting from the specified hierarchy level.
Composite objects
The above example of declarative data binding is not the only one. Continuing the idea of declarative data binding, Dapfor has implemented the concept of composite object. As it has already been mentioned, it is recommended to use objects of arbitrary classes. Properties of these objects return values that are then displayed in corresponding grid cells. However, there are cases when a class object doesn’t have the required property but only refers to another object that contains the required information. There are a lot of such examples – a book written by the author or stock quoted at a certain market. In both cases book or stock objects refer to other objects. However, when these objects are displayed in the grid, it is better to display not just object characteristics but some information taken from referenced objects.
class Author
{
...
public string Name
{
get { return _name; }
}
}
class Book
{
private readonly Author _author;
private readonly string _name;
public Book(Author author, string name)
{
_author = author;
_name = name;
}
public Author Author
{
get { return _author; }
}
public string Title
{
get { return _name; }
}
}
As shown in the example, the book object provides information of publication date and name and has a reference to the author. If we have a binding list with books of different authors, it would be more convenient to see the author’s name in the grid in addition to book name and publication date.
BindingList<Book> books = new BindingList<Book>();
Author author = new Author("Agata Kristi");
books.Add(new Book(author, "Second front"));
books.Add(new Book(author, "Detective story"));
author = new Author("Conan Doyle");
books.Add(new Book(author, "The Blanched Soldier"));
books.Add(new Book(author, "The Mazarin Stone"));
grid.DataSource = books;
Without declarative binding this can be achieved by adding new fields to book
class to return values received from Author object or by creating a new class combining properties of both objects and intended only for displaying in the grid. However, it is not a good solution. On the one hand, the code becomes bulky and contains duplicate information. On the other hand, the book object has properties that it shouldn‘t have. Besides, changing author class signature may cause problems as changes might also impact the book object. Declarative binding offered by Dapfor’s framework makes things different. Marking Book.Author field as a composite field makes the grid process it in a special way by combining Book and Author object fields. When a book is displayed, the grid calls Book.Author property but doesn’t display the Author object in a cell. When a header contains a column with FirstName identifier, the grid first searches Book object for this field and if it is not found, the grid searches the Author object. The business logic remains the same. There are different Author and Book objects, their fields are not duplicated and author data can be accessed by calling book.Author.FirstName. However, the grid displays data of both objects. It is possible to use any data type instead of Author (including data types with variable number of fields).
class Book
{
...
[CompositeField]
public Author Author
{
get { return _author; }
}
...
}
If the book and the author have properties with the same names (e.g. Name), Author.Name property can be market with
FieldAttribute attribute for displaying purposes.
public class Author
{
...
[Field("AuthorName")]
public string Name
{
get { return _name; }
}
}
In this case, if the grid has a column with the same identifier, values received from Author.Name will be
displayed in the corresponding cell.
grid.Headers.Add(new Header());
grid.Headers[0].Add(new Column("Title"));
grid.Headers[0].Add(new Column("AuthorName", "Author"));