A New Grid Control in Windows Forms
Code download available at:CuttingEdge0504.exe(191 KB)
Building a Simple Data Layer
Customizing the DataGridView Control
The Data Connector
Grid controls are essential in many of today's apps. Until now, though, most developers using Visual Basic® have had to buy third-party components to get an effective, easy to use grid component. The Windows® Forms DataGrid turned out to lack too many features for the average developer. Third-party grid controls are often more feature-rich than anything found in a system framework like the Microsoft® .NET Framework. But for in-house and personal applications that don't need a professional quality control, the new DataGridView in the forthcoming .NET Framework 2.0 is a viable alternative.
The DataGridView control is the successor to the Windows Forms DataGrid version 1.x. Once you have dropped the control onto a Windows Form, you'll see the popup window shown in Figure 1. You can declaratively set a number of features—add new rows, edit and delete the current row, reorder columns, bind the control to its data, and style it as needed.
Figure 1** DataGridView Smart Tag **
The DataGridView class allows customization of cells, rows, columns, and borders through properties such as DefaultCellStyle, ColumnHeadersDefaultCellStyle, CellBorderStyle, and GridColor. This alone exceeds the capabilities of the Windows Forms DataGrid version 1.x.
You can use a DataGridView control to display data with or without an underlying data source. If you don't specify a data source, you can create columns and rows that contain data and add them directly to the DataGridView using the control's user interface. Alternatively, you can set the usual DataSource and DataMember properties, bind the control to a data source, and have it automatically populated with data.
The DataGridView eliminates the most commonly reported snags of the previous version, including data caching. In version 1.x, there are no special capabilities for working with a very large amount of data. You have to either bind the control to the whole DataSet or implement a hand-written caching system. In the .NET Framework 2.0, the DataGridView control can work in virtual mode. Setting the VirtualMode property to true enables the control to display a subset of the available data (you can also use VirtualMode to provide a mix of bound and unbound columns).
You create a DataGridView with a set number of rows and columns and then handle the CellValueNeeded event to populate the cells. Virtual mode requires the implementation of an underlying data cache to handle the population, editing, and deletion of DataGridView cells based on actions of the user. As you can see, the mechanism is not entirely automatic but is much simpler than in version 1.x.
Data binding takes advantage of some new IDE features available in Visual Studio® 2005. As you can see in Figure 2, any data-bound Windows control can be bound to a variety of data sources—server databases such as SQL Server, local databases like Microsoft Access or SQL Server™ 2005 files, Web service methods, or custom business objects. The object that gets bound to the control is always a collection; the preceding data sources are just the storage media. Visual Studio 2005 generates classes and code to contain bindable data. In doing so, it requires you to select the data entities to use from the selected source.
Figure 2** Data Source Config Wizard **
Figure 3 shows the dialog box for picking up a table from a Microsoft Access database. Around that table, Visual Studio 2005 would construct an XSD schema file to represent a typed DataSet. In addition, the typed DataSet would be empowered with additional methods to load data on demand, optionally in parameterized way.
Figure 3** Declaring the Data Source **
The wizard and the options shown in Figure 2 and Figure 3 might change before the .NET Framework 2.0 ships. That's why I'll just give you the big picture here. To show you the new DataGridView control in action, I'll examine a handwritten data layer in which ADO.NET classes retrieve any data and custom collection objects convey data to the grid control.
Building a Simple Data Layer
A key enhancement in the .NET Framework 2.0, generic types are especially useful for creating custom collections in a snap. In .NET, generics are programming elements that can work with a variety of data types. You specify the data type when you declare the element. Among other things, generics can be used to create strongly typed collections. Here's an example:
Public Class EmployeeCollection Inherits Collection(Of Employee) End Class
Suppose you want to create and bind an array of objects representing employee data. You load database records from an Employee table into an instance of the Employee class and group employees together within a custom collection (shown in Figure 4). To create a custom collection in .NET Framework 1.x, you derive a new class from CollectionBase and override a couple of members—at the very minimum, the indexer property Item and the method Add. This action requires you to write a great deal of code, and the actual collection is only apparently typed. In reality, the custom collection is an ArrayList object, and anything you add to it, or get from it, is cast to the proper type before use.
Figure 4 A Pretty Simple Data Layer
Imports System.Collections.Generic Imports System.Data Imports System.Data.OleDb ' Class designed to hold employee data Public Class Employee Private _id As Integer Private _name As String Private _title As Integer Private _country As Integer Public Property ID() As Integer Get Return _id End Get Set(ByVal value As Integer) _id = value End Set End Property ... // other properties similar End Class ' Collection of customers Public Class EmployeeCollection : Inherits BindingList(Of Employee) End Class Public Class DataHelper Public Function Load() As EmployeeCollection Dim ds As New DataSet Dim adapter As New OleDbDataAdapter("SELECT * FROM Employees", _ MySettings.Value.Connection) adapter.Fill(ds) Dim rows As New EmployeeCollection For Each row As DataRow In ds.Tables(0).Rows Dim e As New Employee e.ID = Int32.Parse(row("employeeid")) e.Name = row("lastname").ToString() e.Title = Int32.Parse(row("title")) e.Country = Int32.Parse(row("country")) rows.Add(e) Next Return rows End Function End Class
Generics provide a dual advantage when you're moving data across tiers. You write much less code, and the code you do write is easier to maintain. Performance is significantly improved because no boxing is required for value types and no cast is performed in order to access the object in a strongly typed fashion.
Figure 4 shows a simple data layer expressed as a custom collection and a load method. The data is captured using ADO.NET classes and then placed into individual Employee objects for binding to the grid. Should you go with the "manual" approach described here, or are you better off exploiting the design-time capabilities of Visual Studio 2005? Get the best of both worlds: compose all of your low-level ADO.NET code into a class with a few public methods to perform the usual database operations. Next, bind this intermediate class to the control using the Object option shown in Figure 2.
The following code snippet shows how to fill a EmployeeCollection object and bind it to an instance of the DataGridView control:
Dim loader As New DataHelper() Dim data As EmployeeCollection = loader.Load() DataGridView1.DataSource = data
Figure 5 shows the grid in action. It doesn't look much different from an old-style DataGrid control.
Figure 5** The DataGridView Control in Action **
Customizing the DataGridView Control
Can you spot a drawback in the grid shown in Figure 5? The titles and countries are listed through numbers instead of more intelligible names. On the other hand, those numbers are the visible sign of a sensible data schema. Those numbers, in fact, represent foreign keys from cross-referenced tables. From the database design point of view, so far so good, but what about grid rendering? You need to transform those numbers into names—the country, the title, or whatever else they might stand for. In the .NET Framework 1.x, you first create a custom column type through an ad hoc class and then you add it to the grid's column collection. The DataGridView control has both enhanced capabilities and a richer set of built-in column types.
Figure 6** Editing the Columns of the DataGridView Control **
Figure 6 shows how to change the column type and all the choices available. By default, a grid column is rendered using a textbox column just as in version 1.x. To effectively render foreign keys, you need a combobox column bound to a distinct data source and a different pair of display/value members. The dialog in Figure 6 lets you change the column type to DataGridViewComboBoxColumn. Once you've done so, you can add a few extra properties to the column class to fill the combobox with both bound and unbound values. For example, you can set the DataSource property of the DataGridViewComboBoxColumn to the object that contains the bound items—all the titles and the countries to choose from. The DisplayMember property indicates the field to display through the combobox; ValueMember indicates the field to get and set the real value on the master table. Figure 7 illustrates the result.
Figure 7** Managing Foreign Keys in a DataGridView Control **
In read-only mode, the combobox doesn't drop down and is limited to showing the bound value. (You can also make use of the combobox column's DisplayStyle property here to hide the drop-down arrow. This makes the UI look even better.) In edit mode, use the combobox to select the new value for the cell. If you have to change the country cell, you can pick up the new value from a list of country names rather than from a meaningless sequence of numbers.
In addition to combobox and classic textbox columns, you can also have image, link, and button columns. Link and button columns display their items as clickable elements. Each click on the contents of an item is bubbled up as a CellContentClick event.
Each column can have its own set of visual properties as shown in Figure 8. You can control padding, colors, fonts, and borders. Column settings can be set declaratively, programmatically on a per-column basis, or for all the columns at once through the DefaultCellStyle property.
Figure 8** Setting the Styles of a Column **
The following code snippet sets the background color for alternating rows to override the default background set for all cells. In addition, it sets the Format property on the HireDate column to cause the bound date object to be formatted as "Month, Year."
grid.DefaultCellStyle.BackColor = Color.LightGray; grid.AlternatingRowsDefaultCellStyle.ForeColor = Color.White; grid.AlternatingRowsDefaultCellStyle.BackColor = Color.Black; grid.Columns["HireDate"].DefaultCellStyle.Format = "y";
The width of each column can be set in a relative manner, unlike in Windows Forms 1.x grids. Aside from assigning a column a given number of pixels, you can require that the column determine its width automatically and adjust it based on the displayed data. The property AutoSizeCriteria can be set to any of the following values: None, HeaderOnly, Rows, HeaderAndRows, and HeaderAndDisplayedRows. HeaderOnly and Rows indicate that the column should be wide enough to display the entire header or the largest row. HeaderAndRows takes into account the width of the header and that of the largest row. Finally, HeaderAndDisplayedRows considers only the displayed rows and the header.
If the column types supported out-of-the-box by the new Windows Forms DataGridView control don't meet your needs, you can create your own column types with cells that host controls of your choice. Creating custom column types is possible in version 1.x; it's as easy as deriving a class from a known base class. In the .NET Framework 2.0, things are even easier because of a new infrastructure for templating.
One thing you won't find in the Beta 1 list of column types—the set is subject to change by the time the platform ships—is a calendar column to display and edit dates in an easy fashion. To create a calendar column, you must define a class that derives from DataGridViewColumn and extend it with a custom cell template. The cell template is the area that is occupied by individual grid cells. The content of the cell template depends on the user interface of the column. Link columns contain a hyperlink, standard columns include a textbox, combo columns make up their user interface with a combobox. A custom column populates the cell template with any combination of Windows controls. The cells of this column display dates in ordinary text cells, but when the user edits a cell, a DateTimePicker control appears, as in Figure 9. Let's see how to create a calendar column.
Figure 9** The Custom Calendar Column in Action **
In Figure 10, the CalendarColumn class inherits from DataGridViewColumn and overrides the CellTemplate property. The CellTemplate property returns a DataGridViewCell object that represents the content of the cell. As Figure 10 demonstrates, the CalendarColumn constructor uses a made-to-measure cell object—the CalendarCell class.
Figure 10 Custom Calendar Column Class
Public Class CalendarColumn : Inherits DataGridViewColumn Public Sub New() MyBase.New(New CalendarCell()) End Sub Public Overrides Property CellTemplate() As DataGridViewCell Get Return MyBase.CellTemplate End Get Set(ByVal value As DataGridViewCell) If Not (value Is Nothing) And _ Not value.GetType().IsAssignableFrom( _ GetType(CalendarCell)) Then Throw New InvalidCastException("Must be a CalendarCell") End If MyBase.CellTemplate = value End Set End Property End Class
CalendarCell derives from the DataGridViewTextBoxCell class to avoid having to reimplement textbox display functionality. Alternatively, you can make it inherit from DataGridViewCell. The code for the CalendarCell class is split in two parts. One includes public properties such as the format of the date. The other portion of the code is the view editing control; that is, a class that derives from Control and implements the IDataGridViewEditingControl interface. This control provides the user interface when a particular cell enters into edit mode. For the CalendarCell, this user interface consists of a DateTimePicker control. For more details, take a look at the source code available for download from the link at the top of this article.
How can you use this new column with a DataGridView control? Once you bring the new column class into the project, it automatically becomes visible in the dropdown list of available column types. You can assign the calendar type to a column declaratively in the Visual Studio 2005 designer or proceed programmatically, as shown here:
Dim col As New CalendarColumn() col.HeaderText = "Hire Date" col.DataPropertyName = "HireDate" Me.DataGridView1.Columns.Add(col)
The date picker control shows up only if the grid is working in edit mode. If the column is marked as read-only, the date picker remains hidden and the cell displays only static text.
The Data Connector
In the code download for this article, you'll see that the sample DataGridView is bound to a particular type of data source. A data source can be an enumerable data object or a data connector. The DataGridView accepts any of the following interfaces: IList, IListSource, IBindingList, IBindingListView.
IList and IListSource are old acquaintances to developers experienced in .NET. They are implemented by collections and special ADO.NET objects such as DataTable and DataSet. IBindingList has also been around since the .NET Framework 1.0 and derives from IList, adding support for change notification, AddNew semantics, and sorting (the new BindingList<T> generic class implements IBindingList). IBindingListView, new to the .NET Framework 2.0, further extends the IBindingList interface to add advanced sorting and filtering capabilities. The DataConnector class implements the IBindingListView interface.
In Windows Forms 2.0, the DataConnector component, renamed to BindingSource in Beta 2, is the data source object of choice; through it you can bind to a variety of data sources. You can bind the control to a data connector and have the connector component, in turn, bind to another data source or populate with data using a business object. The DataConnector binds to a physical data source through the DataSource and DataMember properties, as shown in the following code:
Me.EmployeesDataConnector.DataMember = "Employees" Me.EmployeesDataConnector.DataSource = Me.MyEmployeesDataSet
When you connect a data source to a DataGridView control, Visual Studio 2005 creates three elements—the data connector, a data adapter, and a typed DataSet. The data adapter executes any query that fills the typed DataSet. The typed DataSet, in turn, is wrapped by the connector and populates the bound control.
The data connector provides a layer of abstraction between a Windows form and its data. All further interaction between form and data, including navigating, sorting, filtering, and updating, is performed through calls to the DataConnector.
As mentioned, a DataConnector encapsulates data and provides members for accessing that data. The Current property retrieves the current item; the property List retrieves the entire list. Currency management is handled automatically, but a number of related events are exposed anyway to allow for further customization. These events include CurrentItemChanged, CurrentChanged, DataSourceChanged, ListChanged, and BindingComplete.
It is worth noting that data sources bound to a DataConnector component can also be managed through the DataNavigator class. DataNavigator offers a VCR-like user interface for navigating the items of a list and performing a few common operations (Insert new, delete, update) on the currently selected record.
The DataGridView class provides advanced sorting capabilities. Each column is given a SortMode property that determines whether the column is sortable or not. By default, all columns are sortable. To change this, set the SortMode property of a given column to NotSortable. You can do that both programmatically and declaratively at design time. The SortMode property also accepts two other values: Automatic (the default) and Programmatic. In both cases, sorting takes place in the default way, alphabetically. If automatic sorting is enabled, the sort direction glyph is displayed; otherwise, you have to show it yourself.
If the grid is configured for programmatic sorting, you have to rely on the sorting capabilities of the data source object in a data-bound scenario. If you're using a grid not bound to any data source, then you need to take care of the sorting yourself through the Sort method, shown here:
Dim titleComp As New TitleComparer DataGridView1.Sort(titleComp)
The Sort method performs its magic by taking an instance of a class that implements the IComparer interface and determines the new order by comparing pairs of values.
All data-driven Windows and Web applications require a grid component. Grids need a good deal of extra functionality that the Windows Forms DataGrid control you get from the .NET Framework 1.x lacks. In the .NET Framework 2.0, the DataGridView control makes up for some of the limitations of the previous version. You'll find an improved user interface with more column types, more customization options, and even a virtual working mode that gets data only when required.
In this column I've only just scratched the surface of the DataGridView control with a sneak preview of the features that will be available with the next version of the .NET Framework. Note that the code and the discussion is based on the Beta 1 release and therefore all of the details are subject to change before the final release. In addition, you can almost certainly expect more features by the time the new version of the Windows Forms framework ships. Stay tuned.
Send your questions and comments for Dino to firstname.lastname@example.org.
Dino Esposito is a Wintellect instructor and consultant based in Italy. Author of Programming ASP.NET and the new book Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching ASP.NET/ADO.NET classes and speaking at conferences. Get in touch with Dino at email@example.com or join the blog at weblogs.asp.net/despos.