DevCity.NET - http://devcity.net
The Hidden Power of the DataGrid Control - Part 1
http://devcity.net/Articles/122/1/article.aspx
Dmytro Lapshyn

Dmytro Lapshyn works as a CTO for Validio Ukraine, which is an official partner of Validio Software, LLC. He previously worked as a programmer in a volunteer student scientific and production group "Programmist" of Kharkov Technical University of Radio-Electronics.

During more than 7 years of his programmer career Dmytro has developed various applications including desktop, client-server and Internet development. He has been working with Microsoft technologies since 1998 and has been developing with Microsoft .NET since the Beta 2 release in 2001. His primary areas of expertise are Visual Basic, ASP, COM+ and .NET.

Dmytro is 27 years old and lives in Kharkov, Ukraine. He has a Bachelors and a Masters degree in Computer Systems Security from Kharkov Technical University of Radio-Electronics.

Company Profile

Founded in 1992, Alis Software successfully operated in software development market for 5 years. In 1997 the company was acquired by Miik Ltd and became its Information Technologies Division. Three years of flourishing accompanied by the dynamic growth made the division the priority of the company business and since 2000 Miik Ltd. almost entirely proceeded to producing software, computer graphics, and Web applications in partnership with foreign and national companies for clients both in Ukraine and abroad.

In summer 2005, Information Technologies Division of MIIK Ltd. was reorganized into Validio Ukraine.

Validio Software provides outsourced software development services to high-tech companies and businesses that rely on technology. Based in Seattle, Washington, Validio's services include design, management, and implementation of complete projects using experienced development teams, as well as providing skilled development resources for customer driven projects. By maintaining staff of qualified software developers and experienced project managers in both the U.S. and Ukraine, Validio offers its clients technical expertise that is both scalable and cost effective.

 
by Dmytro Lapshyn
Published on 1/13/2005
 

Data Grid is one of the most widely used types of controls among developers. Many kinds of data have a tabular nature and are best represented as a grid on a computer screen. The importance of providing users with really consistent and intuitive user interface can hardly be underestimated, and a well-tailored grid could play a primary role in many user interface concepts.

Windows Forms developers are lucky, as Microsoft has included a ready-made DataGrid control into both of the Windows Forms and Web Forms sub-systems of its .NET Framework. However, many developers who have used the Windows Forms DataGrid control might have had a feeling of it being somewhat rigid or even limited. In other words, while some customization can be achieved with several mouse clicks or keystrokes, and some others can be done with just a few lines of code, there are amendments which either require significant efforts or cannot be implemented at all. For example, there is no way to alter column header appearance other than taking over painting of the whole grid exterior.

Having bumped into several such limitations myself, and having spent long hours on searching the Internet and digging the MSDN Library, I have decided to share my DataGrid experience with the developer community in the hope of saving valuable time and even more valuable nerve cells for those who accept the DataGrid challenge in a quest for better user interface experience.


Introduction

Part 1

Introduction

Data Grid is one of the most widely used types of controls among developers. Many kinds of data have a tabular nature and are best represented as a grid on a computer screen. The importance of providing users with really consistent and intuitive user interface can hardly be underestimated, and a well-tailored grid could play a primary role in many user interface concepts.

Windows Forms developers are lucky, as Microsoft has included a ready-made DataGrid control into both of the Windows Forms and Web Forms sub-systems of its .NET Framework. However, many developers who have used the Windows Forms DataGrid control might have had a feeling of it being somewhat rigid or even limited. In other words, while some customizations can be achieved with several mouse clicks or keystrokes, and some others can be done with just a few lines of code, there are amendments which either require significant efforts or cannot be implemented at all. For example, there is no way to alter column header appearance other than taking over painting of the whole grid exterior.

Having bumped into several such limitations by myself, and having spent long hours on searching the Internet and digging the MSDN Library, I have decided to share my DataGrid experience with the developer community in hope to save valuable time and even more valuable nerve cells for those who accepts the DataGrid challenge in a quest for better user interface experience.

A Closer Look

The DataGrid control requires quite a complex environment with a number of helper objects (see Figure 1: Data Grid and its Environment). This environment is usually created behind the scenes, as DataGrid is smart enough to provide reasonable defaults. When these defaults are not satisfactory, the developer can always take part in the environment creation process and replace the default instances of the helper objects with customized ones.

As shown on Figure 1: Data Grid and its Environment, the DataGrid environment consists of the two main areas – the Look and Feel area and the Working with Data area. The Working with Data area is responsible for enabling interaction between the DataGrid and the bound data source. The Look and Feel area is responsible for DataGrid appearance and behavior and enables the developer to customize various DataGrid user interface aspects such as background color, column widths or even custom column styles. We will get back to the Look and Feel area later and now will focus on how the DataGrid works with data.


DataGrid and its environment
Figure 1: Data Grid and its Environment


A Few Words on Data Binding

We will begin our story with a few words on how Windows Forms data binding works. When a Windows Forms control is bound to a data source, a link called binding is established between the control and the data source. This link is responsible for supplying the control with data from the data source as well as for sending back the updated data. In addition, the link can take care of formatting the data for display and parsing the user input if necessary. The link logic is encapsulated in the System.Windows.Forms.Binding class

Another group of objects called binding managers are responsible for synchronizing controls bound to the same data source. They maintain current position within the data source and facilitate data addition, modification and removal. Binding managers inherit from the abstract System.Windows.Forms.BindingManagerBase class. There are two standard implementations of the binding managers – CurrencyManager and PropertyManager. You may find out more about these classes from the MSDN Library documentation.

Binding managers are in turn governed by binding contexts. Binding contexts are in charge of creation, storing and returning binding managers for each unique combination of a data source and a data member. There is only one binding context object per form.

All binding managers within a form are synchronized, so the data binding mechanics ensures that all binding managers for each unique combination of a data source and a data member maintain the same position.

 

Data Grid and Data Binding

The DataGrid control relies on the data binding architecture we have outlined in the previous section. At the same time, there are some specifics worth a more detailed explanation.

We will start from types of data sources the DataGrid control can be bound to. The most popular types are of course DataTable and DataSet, which are followed by DataView. However, DataGrid is not limited to ADO .NET types and is also capable of handling single-dimension arrays as well as various kinds of lists implementing either the IList or the IListSource interface.

As you have probably noted, all these data sources share a common feature – they all can be uniformly treated as lists having a known number of items. This determines the type of binding manager the DataGrid control uses: it is the CurrencyManager class. In most cases, a single CurrencyManager instance is enough, but when a DataGrid is bound to a DataSet having multiple tables with relations established between them, multiple CurrencyManagers can be created to serve each related table. We will hereafter use the word “list” to define a sequence of items managed by a single CurrencyManager instance.

The DataGrid control exposes two properties related to its data source. The DataSource property gets or sets a data source object the DataGrid instance is bound to. As the DataGrid control is limited with working with a single list at a time, the DataSource property is complemented with the DataMember property to specify a particular list within the DataSource to work with.

The DataMember property contents may vary depending on the nature of the data source and has a generic name “navigation path”. Valid examples of navigation paths are the name of a table in a data set or the name of a relation between master and detail tables in the same data set.

When being bound to a data source, the DataGrid control first creates a CurrencyManager instance for the list specified with the DataMember property value. We will call this instance a primary CurrencyManager instance. Other instances of the CurrencyManager class can be created on demand when, for example, the user navigates a relationship between the parent and the child table within a bound DataSet.

This leads us to the following line of code that returns a CurrencyManager instance a DataGrid instance currently cooperates with:

CurrencyManager cm = this.BindingContext[ theGrid.DataSource, theGrid.DataMember];

You might be concerned at this point about the reason why one should ever bother with CurrencyManagers, and I promise you will see how important their role is.

Data Grid Navigation Mechanics

The DataGrid control maintains a notion of a current row. This row is visually marked with a little black triangle displayed in the corresponding row header. The notion of a current row is used for data editing purposes and for navigating between parent and child tables. DataGrid does not, however, track the current row by itself, relying on its CurrencyManager(s) instead as shown on Figure 2.


Figure 2

You have control over the DataGrid’s current row through the CurrentRowIndex property. This property enables you to retrieve and update current row index corresponding to the primary CurrencyManager’s position. In other words, this property reflects the position within the parent list, and a relation is navigated, the value of this property doesn’t change.

Note though that there is no property to control the position for any of the auxiliary CurrencyManagers. This limitation, however, is no more an obstacle for us, as we already know how to access the CurrencyManager instance for the current list displayed in the DataGrid.

 

Data Grid Editing Explained

The DataGrid control enables the user with powerful in-place editing capabilities accompanied with basic data validation. This exterior simplicity, however, involves non-trivial logic behind the scenes which we are going to explore.

We will start our exploration from the sequence in which data editing is carried out. The sequence starts from the moment the user clicks on a cell or presses the F2 key when a cell has previously been selected (for now we assume that the user works with an existing cell, as the sequence is slightly different when a new row is added and we will get back to it a little bit later). The grid brings the cell into the editable mode in response by calling the Edit method of the corresponding DataGridColumnStyle object. The column style, in turn, obtains the current cell value with its GetColumnValueAtRow method, displays an editing control (most commonly, a TextBox) within the cell and finally populates the control with the formatted representation of the current cell value.

Now the user can enter a new value into the cell. Once the cell value has changed, the DataGrid notifies the user that the current row has started editing by displaying a little pencil icon in the corresponding row header (see Figure 3).


Figure 3: Current Row is Being Edited

Behind the scenes, it is the responsibility of the corresponding DataGridColumnStyle to notify the grid that the cell has started editing by calling its own ColumnStartedEditing method.

TIP: It is important to notify the grid at the very moment the cell value has actually started to change, not at the moment the editing control is being displayed to the user.

Nothing worth our attention happens until the user has typed a new value and moved off the cell or has pressed Escape to restore the old value. Either way, the corresponding DataGridColumnStyle is put back to work with its Commit or Abort method invoked respectively. The abortion sequence is straightforward: the editing control is being hidden and the old cell value is restored from the data source. The updating sequence is much more complicated and consists of two basic phases:

  1. Data Validation
  2. Data Commit

The Data Validation phase may be different for various DataGridColumnStyles; we will take DataGridTextBoxColumn as an example. With this column style, the value entered is ensured that:

  • It matches the pre-defined column format specified by the column’s Format and FormatInfo properties;
  • It can be converted to the target data type determined by the data source. In plain English, this means that if the grid is bound to a DataTable and the underlying data column type is System.Int32, the column style checks whether the value entered can be successfully converted to a value of this type.
Note that the second check is the only check made against the data source. This means, in particular, that a Null value would easily pass the validation phase even if the corresponding column didn’t allow null values.

The column style actually pushes the new value back to the data source upon the Data Commit phase by calling its SetColumnValueAtRow method. If there were some problems, the update operation is rolled back and the user is notified about the error by displaying a message box. Believe me, this hard-coded behavior can sometimes be a real scourge as the developer might very likely wish to provide her own type of reaction on such an error. I am still racking my brain why DataGrid developers wouldn’t just throw an exception instead and let the developer decide how to handle it.

TIP: For DataTables and DataSets, the underlying row that has been modified changes its RowState property value to Modified. This can be used later to determine which rows has been edited by the user and should be somehow processed.
 

Final

Now, as promised, we get back to differences in the DataGrid editing logic that take place when a new data row is added. One should be aware that not all types of data sources support adding new rows, only those which implement the IBindingList interface actually do. Therefore, we will assume that the DataGrid is bound to a DataSet or to a DataTable for our further explanation. The IBindingList interface is actually implemented by the DataView and the DataViewManager classes, so a DataView instance is implicitly created for each DataGrid’s CurrencyManager in case of a DataTable or a DataSet bound to it.

Now that the prerequisites have been discussed we can move on. When the user clicks on the new row addition area (the one marked with a small “*” icon), a new data row is created in the corresponding DataView but not yet in the DataTable the view has been created for. If we examine the RowState property for the corresponding DataRow, we find out that the data row is marked as Detached, which means the row does not belong to any DataTable yet.

The usual editing sequence is then carried out as if the new row was any other row. The next difference is introduced at the point when the new row values have been either committed or the editing have been rolled back. Upon the successful commit, the new row is actually added to the data source and its RowState is changed to “Added”. If the editing has been rolled back, the new row is completely removed from the DataView – this prevents the user from a multitude of blank rows that are added every time she clicks on the asterisk-marked area.

TIP: For DataTables and DataSets, the underlying row that has been added is marked with the Added value of its RowState property. This can be used later to determine which rows has been added by the user and should be somehow processed.

Another interesting point I would like to discuss is how editing works for a sorted DataGrid. As you might have noticed, if a cell being edited belongs to a column the grid is currently sorted by, the row “jumps” to its correct place in the sort order when the user has entered new cell value and moves off the cell. While we cannot prevent this jumping, we can determine the old index of the row before it jumps. To do that, we will subscribe to the ListChanged event of the DataView.

CurrencyManager cm = (CurrencyManager)this.BindingContext[dataGrid.DataSource, dataGrid.DataMember];

DataView theView = (DataView)cm.List;
theView.ListChanged += ListChangedEventHandler(ListChangedHandler);

Now, within the handler, we will check the ListChangedType property to react only on ItemMoved notifications we are interested in. When such a notification arrives, we can analyze the values of the OldIndex and the NewIndex properties to determine the old row index and the new row index respectively.

private void ListChangedHandler( object sender, ListChangedEventArgs e) {
    if (e.ListChangedType == ListChangedType.ItemMoved) {
        int oldIndex = e.OldIndex;
        int oldIndex = e.NewIndex;
    }
}

And finally, a simple trick to force the DataGrid to complete current editing. This trick will be useful any time you want to complete editing programmatically – for example, when the user clicks the OK button in a dialog box. You will need to perform two actions:
Tell the DataGrid to exit editing mode and save the data.

  1. Tell the corresponding CurrencyManager to push the data back to the data source.

Calling the DataGrid’s EndEdit method carries out the first step. This method requires a reference to the DataGridColumnStyle instance for the column being edited, so you will have to create grid column styles manually – you won’t be able to access the grid column styles created by default.

The corresponding DataRow fields will have been updated by the time the DataGrid quits the editing mode, but row status won’t be changed – and if the row have just been created, it won’t be attached to any table.

The CurrencyManager’s EndCurrentEdit method should be called to complete the second step. After this call the row will have correct status and will be added to the DataTable if necessary.

Conclusion

In the first part of the article we have taken a closer look at the inner life of the data grid control and have taken a walk through the intricacies of its navigation and data editing logic. We have also written several pieces of code to add small but convenient features to the DataGrid control. At this point you might wish to start implementing your extended version of the DataGrid, and I absolutely encourage you to do so as well as I would suggest including these small features for a start.