- DataGridView Overview
- Basic Data Binding with the DataGridView
- Controlling Modifications to Data in the Grid
- Programmatic DataGridView Construction
- Custom Column Content with Unbound Columns
- Displaying Computed Data in Virtual Mode
- Using the Built-In Column Types
- Built-In Header Cells
- Handling Grid Data Edits
- Automatic Column Sizing
- Column and Row Freezing
- Using the Designer to Define Grids
- Column Reordering
- Defining Custom Column and Cell Types
- Utilizing Cell-Oriented Grid Features
- Formatting with Styles
- Where Are We?
Defining Custom Column and Cell Types
With the DataGridView, you are already leaps and bounds ahead of the DataGrid for presenting rich data because of the built-in column types that it supports out of the box. But there are always custom scenarios that you will want to support to display custom columns. Luckily, another thing the DataGridView makes significantly easier is plugging in custom column and cell types.
If you want to customize just the painting process of a cell, but you don’t need to add any properties or control things at the column level, you have an event-based option rather than creating new column and cell types. You can handle the CellPainting event and draw directly into the cell itself, and you can achieve pretty much whatever you want with the built-in cell types and some (possibly complex) drawing code. But if you want to be able to just plug your column or cell type in a reusable way with the same ease as using the built-in types, then you can derive your own column and cell types instead.
The model you should follow for plugging in custom column types matches what you have already seen for the built-in types: You need to create a column type and a corresponding cell type that the column will contain. You do this by simply inheriting from the base DataGridViewColumn and DataGridViewCell classes, either directly or indirectly, through one of the built-in types.
The best way to explain this in detail is with an example. Say I wanted to implement a custom column type that lets me display the status of the items represented by the grid’s rows. I want to be able to set a status using a custom-enumerated value, and cells in the column will display a graphic indicating that status based on the enumerated value set on the cell. To do this, I define a StatusColumn class and a StatusCell class (I disposed of the built-in type naming convention here of prefixing DataGridView on all the types because the type names get sooooooooooo long). I want these types to let me simply set the value of a cell, either programmatically or through data binding, to one of the values of a custom-enumerated type that I call StatusImage. StatusImage can take the values Green, Yellow, or Red, and I want the cell to display a custom graphic for each of those based on the value of the cell. Figure 6.7 shows the running sample application with this behavior.
Figure 6.7 Custom Column and Cell Type Example
Defining a Custom Cell Type
To achieve this, the first step is to define the custom cell type. If you are going to do your own drawing, you can override the protected virtual Paint method from the DataGridViewCell base class. However, if the cell content you want to present is just a variation on one of the built-in cell types, you should consider inheriting from one of them instead. That is what I did in this case. Because my custom cells are still going to be presenting images, the DataGridViewImageCell type makes a natural base class. My StatusCell class isn’t going to expose the ability to set the image at random, though; it is designed to work with enumerated values. I also want the cell value to be able to handle integers as long as they are within the corresponding numeric values of the enumeration, so that I can support the common situation where enumerated types are stored in a database as their corresponding integer values. The code in Listing 6.4 shows the StatusCell class implementation.
Example 6.4. Custom Cell Class
namespace CustomColumnAndCell { public enum StatusImage { Green, Yellow, Red } public class StatusCell : DataGridViewImageCell { public StatusCell() { this.ImageLayout = DataGridViewImageCellLayout.Zoom; } protected override object GetFormattedValue(object value, int rowIndex, ref DataGridViewCellStyle cellStyle, TypeConverter valueTypeConverter, TypeConverter formattedValueTypeConverter, DataGridViewDataErrorContexts context) { string resource = "CustomColumnAndCell.Red.bmp"; StatusImage status = StatusImage.Red; // Try to get the default value from the containing column StatusColumn owningCol = OwningColumn as StatusColumn; if (owningCol != null) { status = owningCol.DefaultStatus; } if (value is StatusImage || value is int) { status = (StatusImage)value; } switch (status) { case StatusImage.Green: resource = "CustomColumnAndCell.Green.bmp"; break; case StatusImage.Yellow: resource = "CustomColumnAndCell.Yellow.bmp"; break; case StatusImage.Red: resource = "CustomColumnAndCell.Red.bmp"; break; default: break; } Assembly loadedAssembly = Assembly.GetExecutingAssembly(); Stream stream = loadedAssembly.GetManifestResourceStream(resource); Image img = Image.FromStream(stream); cellStyle.Alignment = DataGridViewContentAlignment.TopCenter; return img; } } }
The first declaration in this code is the enumeration StatusImage. That is the value type expected by this cell type as its Value property. You can then see that the StatusCell type derives from the DataGridViewImageCell, so I can reuse its ability to render images within the grid. There is a default status field and corresponding property that lets the default value surface directly. The constructor also sets the ImageLayout property of the base class to Zoom, so the images are resized to fit the cell with no distortion.
The key thing a custom cell type needs to do is either override the Paint method, as mentioned earlier, or override the GetFormattedValue method as the StatusCell class does. This method will be called whenever the cell is rendered and lets you handle transformations from other types to the expected type of the cell. The way I have chosen to code GetFormattedValue for this example is to first set the value to a default value that will be used if all else fails. The code then tries to obtain the real default value from the containing column’s DefaultValue property if that column type is StatusColumn (discussed next). The code then checks to see if the current Value property is a StatusImage enumerated type or an integer, and if it is an integer, it casts the value to the enumerated type.
Once the status value to be rendered is determined, the GetFormattedValue method uses a switch-case statement to select the appropriate resource name corresponding to the image for that status value. You embed bitmap resources in the assembly by adding them to the Visual Studio project and setting the Build Action property on the file to Embedded Resource. The code then uses the GetManifestResourceStream method on the Assembly class to extract the bitmap resource out of the assembly, sets the alignment on the cellStyle argument passed into the method, and then returns the constructed image as the object from the method. The object that you return from this method will be the one that is passed downstream to the Paint method as the formatted value to be rendered. Because this doesn’t override the Paint method, the implementation of my DataGridViewImageCell base class will be called, and it expects an Image value to render.
Defining a Custom Column Type
So now you have a custom cell class that could be used in the grid, but you also want to have a custom column class that contains StatusCells and can be used for setting up the grid and data binding. If you were going to use the custom cell type completely programmatically, you could just construct an instance of the DataGridViewColumn base class and pass in an instance of a StatusCell to the constructor, which sets that as the CellTemplate for the column. However, that approach wouldn’t let you use the designer column editors covered in Figures 6.4 and 6.5 to specify a bound or unbound column of StatusCells. To support that, you need to implement a custom column type that the designer can recognize. As long as you’re implementing your own column type, you also want to expose a way to set what the default value of the StatusImage should be for new rows that are added. The implementation of the StatusColumn class is shown in Listing 6.5.
Example 6.5. Custom Column Class
namespace CustomColumnAndCell { public class StatusColumn : DataGridViewColumn { public StatusColumn() : base(new StatusCell()) { } private StatusImage m_DefaultStatus = StatusImage.Red; public StatusImage DefaultStatus { get { return m_DefaultStatus; } set { m_DefaultStatus = value; } } public override object Clone() { StatusColumn col = base.Clone() as StatusColumn; col.DefaultStatus = m_DefaultStatus; return col; } public override DataGridViewCell CellTemplate { get { return base.CellTemplate; } set { if ((value == null) || !(value is StatusCell)) { throw new ArgumentException( "Invalid cell type, StatusColumns can only contain StatusCells"); } } } } }
You can see from the implementation of StatusColumn that you first need to derive from the DataGridViewColumn class. You implement a default constructor that passes an instance of your custom cell class to the base class constructor. This sets the CellTemplate property on the base class to that cell type, making it the cell type for any rows that are added to a grid containing your column type.
The next thing the class does is define a public property named DefaultStatus. This lets anyone using this column type to set which of the three StatusImage values should be displayed by default if no value is explicitly set on the grid through data binding or programmatic value setting on a cell. The setter for this property changes the member variable that keeps track of the current default. The DefaultStatus property on the column is accessed from the StatusCell.GetFormattedValue method, as described earlier.
Another important thing for you to do in your custom column type is to override the Clone method from the base class, and in your override, return a new copy of your column with all of its properties set to the same values as the current column instance. This method is used by the design column editors to add and edit columns in a grid through the dialogs discussed in Figures 6.4 and 6.5.
The last thing the custom column class does is to override the CellTemplate property. If someone tries to access the CellTemplate, the code gets it from the base class. But if someone tries to change the CellTemplate, the setter checks to see if the type of the cell being set is a StatusCell. If not, it raises an exception, preventing anyone from programmatically setting an inappropriate cell type for this column. This doesn’t prevent you from mixing other cell types into the column for a heterogeneous grid (as shown earlier in the section on programmatically creating the grid).
Now that you have defined the custom cell and column types, how can you use them? Well, you can define them as part of any Windows application project type in Visual Studio, but generally when you create something like this, you are doing it so you can reuse it in a variety of applications. Whenever you want reuse code, you need to put that code into a class library. So if you define a class library project, add the classes just discussed to the class library, along with the images you want to use for displaying status as embedded resources in the project. This creates an assembly that you can then reference from any Windows application that you want to use the column and cell type within. All you need to do is set a reference to that assembly from the Windows Forms project in which you want to use them, and the custom column types will display in the Add Column dialog, as shown in Figure 6.8 (StatusColumn, in this case).
Figure 6.8 Custom Column Types in the Add Column Dialog
Within your Windows Forms application, you can programmatically add StatusColumns to a grid, or use the designer to do so. If you add the column through the designer and then look at it in the Edit Columns dialog, you will see that DefaultStatus appears in the property list and is settable as an enumerated property with its allowable values (see Figure 6.9).
Figure 6.9 Custom Column Properties in the Edit Columns Dialog
With a column of this type added to the grid, you can either populate the grid programmatically with either of the types that the cell is able to handle for values (either StatusImage values or integers within the value range of StatusImage), or you can data bind to it with a collection of data that contains those values. Here is a simple example of setting the values programmatically on a grid containing two columns: a text box column and a StatusColumn. Note that you can set the values with either the enumerated value or with an appropriate integer value.
m_Grid.Rows.Add("Beer Bottler", StatusImage.Green); m_Grid.Rows.Add("Beer Bottle Filler", 1); //StatusImage.Yellow = 1 m_Grid.Rows.Add("Bottle capper", StatusImage.Red);
The CustomColumnAndCell sample application in the download code also demonstrates creating a data set and data binding against the status column.