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.

C# Copy imageCopy
//Build a header with columns
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"));

//Add data object at the top-level and attach 2 children of the same type
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"));

//Add data other data object with 2 children
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"));
Single-header treelist view

.Net Grid makes it easy adding any number of headers, thus changing the presentation from the treelist with a single to multi-header grid.

C# Copy imageCopy
grid.Headers.Add(new Header());
grid.Headers[1].Add(new Column("DoubleValue"));
grid.Headers[1].Add(new Column("StringValue"));
Multi-header grid

In the unbound mode .Net Grid supports all data types, described in .Net Grid tutorial (Part1: Data types)

C# Copy imageCopy
//Add an array of objects on the top level
Row row = grid.Rows.Add(new object[] { 10, 11.12, "some string 1" });

//Add a dictionary as a child object
IDictionary<string, object> dataObject2 = new Dictionary<string, object>();
dataObject2.Add("IntValue", 20);
dataObject2.Add("DoubleValue", 21.33);
row.Add(dataObject2);

//Add an object with variable number of fields as a child of the row
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.

    C# Copy imageCopy
    //Build a header with columns
    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"));
    
    //Build and attach data
    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 2
  • 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.

    C# Copy imageCopy
    //Implementation of IListSource interface providing access to two collections
    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; }
        }
    }
    
    //Create a collection of custom objects
    BindingList<MyCustomClass> collection1 = new BindingList<MyCustomClass>();
    collection1.Add(new MyCustomClass(11, 11.11, "subitem 1"));
    collection1.Add(new MyCustomClass(22, 22.22, "subitem 2"));
    
    //Create other collection with objects supporting variable number of fields
    BindingList<UnboundValueAccessor> collection2 = new BindingList<UnboundValueAccessor>();
    collection2.Add(new UnboundValueAccessor());
    collection2[0]["StringValue"].Value = "dynamic field";
    
    //Bind grid to both collections together
    grid.DataSource = new ListSource(collection1, collection2);
    
    //Add an object to the first collection
    collection1.Add(new MyCustomClass(33, 33.33, "newly added object"));
    
    //Add other object to the second collection
    collection2.Add(new UnboundValueAccessor());
    
    //Update some fields in the second collection
    collection2[0]["DoubleValue"].Value = 1.234;
    collection2[1]["IntValue"].Value = 100;
    collection2[1]["StringValue"].Value = "newly added field";
    Binding to multiple collections
  • 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.

    C# Copy imageCopy
    //Create a collection of custom objects
    BindingList<MyCustomClass> collection = new BindingList<MyCustomClass>();
    collection.Add(new MyCustomClass(11, 11.11, "item 1"));
    collection.Add(new MyCustomClass(22, 22.22, "item 2"));
    
    //Bind grid to the collection 
    grid.DataSource = collection;
    
    //Add some objects as children of root items
    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"));
    Adding unbound data on the next hierarchical level

    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.

    C# Copy imageCopy
    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"));
    
    //Create a collection of custom objects
    BindingList<MyCustomClass> collection = new BindingList<MyCustomClass>();
    collection.Add(new MyCustomClass(11, 11.11, "item 1"));
    collection.Add(new MyCustomClass(22, 22.22, "item 2"));
    
    //Bind row1 to the collection 
    row1.DataSource = collection;
    Binding rows to data sources

    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:

Author book relation
C# Copy imageCopy
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.

C# Copy imageCopy
//Populate binding list with some authors
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);

//Bind grid to author collection
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.

C# Copy imageCopy
class Author
{
    ...

    [HierarchicalField]
    public IList<Book> Books 
    { 
        get { return _books; } 
    }

    ...
}
Declarative binding with HierarchicalField attribute

Now let’s consider a case when an author writes a new book that should be added to the collection.

C# Copy imageCopy
//Add a book to a 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.

C# Copy imageCopy
class Author
{
    private readonly IList<Book> _books = new BindingList<Book>();

    ...
}
Declarative binding to BindingList<T> collections

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.

    C# Copy imageCopy
    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; }
        }
    }
    
    //Populate binding list with some authors
    BindingList<Author> authors = new BindingList<Author>();
    Author author = new Author("Agata Kristi");
    authors.Add(author);
    
    //Add some books with variable number of fields
    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);
    
    //Bind grid to author collection
    grid.DataSource = authors;
    Declarative binding to BindingList<T> collections with objects having variable number of fields
  • The second note is that data can be added to the grid in multiple ways:

    C# Copy imageCopy
    grid.Rows.Add(...);
    grid.DataSource = ...;

    or at any available hierarchy level, e.g., like this:

    C# Copy imageCopy
    Row row = grid.Rows.Add(new object[] { "Detective stories" });
    row.DataSource = authors;
    Declarative binding to BindingList<T> collections and unbound data

    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.

C# Copy imageCopy
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.

C# Copy imageCopy
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).

C# Copy imageCopy
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.

C# Copy imageCopy
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.

C# Copy imageCopy
grid.Headers.Add(new Header());
grid.Headers[0].Add(new Column("Title"));
grid.Headers[0].Add(new Column("AuthorName", "Author"));
Declarative binding and composite fields