Build a Custom NumericUpDown Cell and Column for the DataGridView Control

 

Régis Brid
Microsoft Corporation

March 2006

Applies to:
   Microsoft Visual Studio 8.0
   Microsoft .NET Framework 2.0

Summary: In Microsoft Visual Studio 8.0, Windows Forms comes with a new grid control called the DataGridView. This article describes how to build a custom cell and column based on the Windows Forms NumericUpDown control for use in the DataGridView control. (28 printed pages)

Download the associated code sample, DGV_NUPD.exe.

Contents

Introduction
Characteristics of the Cell
Cell, Column and Editing Control Classes
Cell Implementation Details
Editing Control Implementation Details
Column Implementation Details
Screenshot of a DataGridViewNumericUpDownColumn
Conclusion

Introduction

Windows Forms 2.0 offers several cell and column types for its DataGridView control. For example, the product ships with a text-box–based cell and column (DataGridViewTextBoxCell/DataGridViewTextBoxColumn) and a check-box–based duo (DataGridViewCheckBoxCell/DataGridViewCheckBoxColumn) among others. Even though the product comes with a rich set of cell and column types, some developers may want to create their own cell and column types to extend the functionality of the grid. Thankfully the DataGridView control architecture is extensible enough that such custom cells and columns can be built and used in the grid. This document explains how to create and use a cell and column that easily lets users enter numeric values in the grid.

Characteristics of the Cell

The custom cell, called DataGridViewNumericUpDownCell, reuses the user interface of the Windows Forms NumericUpDown control (Figure 1).

Aa730881.winf_numericupdown_1(en-US,VS.80).gif

Figure 1. The Windows Forms NumericUpDown control

Benefits

The standard DataGridViewTextBoxCell cell type already permits end users to view and enter numeric values in their grids. However the custom DataGridViewNumericUpDownCell offers some additional advantages:

  • The input is automatically limited to numeric values.
  • The input is automatically restricted within a certain range.
  • The precision of the input (i.e., desired digits after the comma) can be predefined as well.

Deriving from the textbox cell type

Because the DataGridViewNumericUpDownCell cell type is close in functionality and look to the standard DataGridViewTextBoxCell cell type, it makes sense to derive from that class. By doing this, the custom cell can take advantage of numerous characteristics of its base class and the work involved in creating the cell is significantly reduced. It is possible however to directly derive from the DataGridViewCell type that is the base type of all cells provided in Windows Forms 2.0.

Editing experience

The cells used by the DataGridView control can be classified into three categories depending on editing experience.

  1. Cells without editing experience. These are read-only cells that don't accept user input. The standard DataGridViewButtonCell is an example.
  2. Cells with simple editing experience. These are cells that accept limited user input such as the standard DataGridViewCheckBoxCell. For that cell, the only user interactions that change the cell value are clicking the checkbox or hitting the spacebar.
  3. Cells with complex editing experience. These are cells that provide a rich user interaction for changing their value. They require a Windows Forms control to be shown to enable that complex user input. Examples are the DataGridViewTextBoxCell cell and the DataGridViewComboBoxCell.

Since the DataGridViewNumericUpDownCell cell provides a rich user interaction, it falls into the third category and requires a control to be brought up when the user wants to change the value of the cell. This control is called the editing control, and in this case it derives from the NumericUpDown control. All editing controls need to implement the IDataGridViewEditingControl interface. This standardizes the interactions between the grid/cell and the editing control.

Cell, Column and Editing Control Classes

The minimum requirement for being able to use a custom cell type is to develop one class for that cell type. If it has a rich editing experience (case #3 above) and can't use one of the standard editing controls, DataGridViewTextBoxEditingControl and DataGridViewComboBoxEditingControl, then a second class needs to be created for the custom editing control. Finally the creation of a custom column class is optional since any cell type can be used in any column type or the base DataGridViewColumn (because columns can be heterogeneous). For example, the DataGridViewNumericUpDownCell cell could be used in a DataGridViewLinkColumn or DataGridViewColumn column. Creating a special column type however makes it easier in many cases to use the custom cell type. The custom column typically replicates the specific properties of the custom cell. For the DataGridViewNumericUpDownCell type, three classes were created in three files: DataGridViewNumericUpDownCell in DataGridViewNumericUpDownCell.cs, DataGridViewNumericUpDownEditingControl in DataGridViewNumericUpDownEditingControl.cs and finally DataGridViewNumericUpDownColumn in DataGridViewNumericUpDownColumn.cs. All the classes were put in the DataGridViewNumericUpDownElements namespace.

Cell Implementation Details

Let's focus first on how to create the custom DataGridViewNumericUpDownCell cell itself.

Class definition and constructor

As mentioned earlier, the DataGridViewNumericUpDownCell class derives from the DataGridViewTextBoxCell class found in Windows Forms 2.0.

namespace DataGridViewNumericUpDownElements
{
    public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell
    {
        public DataGridViewNumericUpDownCell()
        {
            ...
        }
    }
}

Defining cell properties

The DataGridViewNumericUpDownCell class replicates some of the key properties exposed by the NumericUpDown control: DecimalPlaces, Increment, Maximum, Minimum, and ThousandSeparator.

Note   The DataGridViewNumericUpDownCell could be enriched further by exposing the Hexadecimal property of the NumericUpDown control.

It is typical for cells that are based on a particular Windows Forms control to replicate some of its properties. For example, the DataGridViewComboBoxCell class exposes a property called MaxDropDownItems just like the ComboBox control.

As an example of a cell property implementation, following is the implementation of the DecimalPlaces property:

public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell
{
    private int decimalPlaces;       // Caches the value of the DecimalPlaces property

    public DataGridViewNumericUpDownCell()
    {
        ...
        this.decimalPlaces = DATAGRIDVIEWNUMERICUPDOWNCELL_defaultDecimalPlaces;
    }

    [
        DefaultValue(DATAGRIDVIEWNUMERICUPDOWNCELL_defaultDecimalPlaces)
    ]
    public int DecimalPlaces
    {
        get
        {
            return this.decimalPlaces;
        }

        set
        {
            if (value < 0 || value > 99)
            {
                throw new ArgumentOutOfRangeException("The DecimalPlaces property cannot be smaller than 0 or larger than 99.");
            }
            if (this.decimalPlaces != value)
            {
                SetDecimalPlaces(this.RowIndex, value);
                // Assure that the cell or column gets repainted and autosized if needed
                OnCommonChange();
            }
        }
    }

    internal void SetDecimalPlaces(int rowIndex, int value)
    {
        this.decimalPlaces = value;
        if (OwnsEditingNumericUpDown(rowIndex))
        {
            this.EditingNumericUpDown.DecimalPlaces = value;
        }
    }
}

Note   The SetDecimalPlaces function is accessed from the DataGridViewNumericUpDownColumn class. See the full code for the implementations of OnCommonChange(), OwnsEditingNumericUpDown(int rowIndex) and the EditingNumericUpDown property.

Key properties to override

When creating a custom cell type for the DataGridView control, the following base properties from the DataGridViewCell class often need to be overridden.

The EditType property

The EditType property points to the type of the editing control associated with the cell. The default implementation in DataGridViewCell returns the System.Windows.Forms.DataGridViewTextBoxEditingControl type. Cell types that have no editing experience or have a simple editing experience (i.e., don't use an editing control) must override this property and return null. Cell types that have a complex editing experience must override this property and return the type of their editing control.

Implementation for the DataGridViewNumericUpDownCell class:

public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell
{
    // Type of this cell's editing control
    private static Type defaultEditType = typeof(DataGridViewNumericUpDownEditingControl);

    public override Type EditType
    {
        get
        {
            return defaultEditType; // the type is DataGridViewNumericUpDownEditingControl
        }
    }
}

The FormattedValueType property

The FormattedValueType property represents the type of the data displayed on the screen, i.e., the type of the cell's FormattedValue property. For example, for the DataGridViewTextBoxCell class it is System.String, as for a DataGridViewImageCell it is System.Drawing.Image or System.Drawing.Icon. The DataGridViewNumericUpDownCell displays text on the screen, so its FormattedValueType is System.String, which is the same as the base class DataGridViewTextBoxCell. So this particular property does not need to be overridden here.

The ValueType property

The ValueType property represents the type of the underlying data, i.e., the type of the cell's Value property. The DataGridViewNumericUpDownCell class stores values of type System.Decimal. So its implementation of ValueType is as follows:

public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell
{
// Type of this cell's value.
    private static Type defaultValueType = typeof(System.Decimal);

    public override Type ValueType
    {
        get
        {
            Type valueType = base.ValueType;
            if (valueType != null)
            {
                return valueType;
            }
            return defaultValueType;
        }
    }
}

Key methods to override

When developing a custom cell type, it is also critical to check if the following virtual methods need to be overridden.

The Clone() method

The DataGridViewCell base class implements the ICloneable interface. Each custom cell type typically needs to override the Clone() method to copy its custom properties. Cells are cloneable because a particular cell instance can be used for multiple rows in the grid. This is the case when the cell belongs to a shared row. When a row gets unshared, its cells need to be cloned. Following is the implementation for the DataGridViewNumericUpDownCell:

public override object Clone()
{
    DataGridViewNumericUpDownCell dataGridViewCell = base.Clone() as DataGridViewNumericUpDownCell;
    if (dataGridViewCell != null)
    {
        dataGridViewCell.DecimalPlaces = this.DecimalPlaces;
        dataGridViewCell.Increment = this.Increment;
        dataGridViewCell.Maximum = this.Maximum;
        dataGridViewCell.Minimum = this.Minimum;
        dataGridViewCell.ThousandsSeparator = this.ThousandsSeparator;
    }
    return dataGridViewCell;
}

The KeyEntersEditMode(KeyEventArgs) method

Cell types that have a complex editing experience (i.e., cells that use an editing control) need to override this method to define which keystrokes force the editing control to be shown (when the grid's EditMode is EditOnKeystrokeOrF2). For the DataGridViewNumericUpDownCell type, digits and the negative sign need to bring up the editing control. Therefore the implementation of this method is:

public override bool KeyEntersEditMode(KeyEventArgs e)
{
    NumberFormatInfo numberFormatInfo = System.Globalization.CultureInfo.CurrentCulture.NumberFormat;
    Keys negativeSignKey = Keys.None;
    string negativeSignStr = numberFormatInfo.NegativeSign;
    if (!string.IsNullOrEmpty(negativeSignStr) && negativeSignStr.Length == 1)
    {
        negativeSignKey = (Keys)(VkKeyScan(negativeSignStr[0]));
    }

    if ((char.IsDigit((char)e.KeyCode) ||
        (e.KeyCode >= Keys.NumPad0 && e.KeyCode <= Keys.NumPad9) ||
        negativeSignKey == e.KeyCode ||
        Keys.Subtract == e.KeyCode) &&
        !e.Shift && !e.Alt && !e.Control)
    {
        return true;
    }
    return false;
}

The InitializeEditingControl(int, object, DataGridViewCellStyle) method

This method is called by the grid control when the editing control is about to be shown. This occurs only for cells that have a complex editing experience of course. This gives the cell a chance to initialize its editing control based on its own properties and the formatted value provided. The DataGridViewNumericUpDownCell's implementation is as follows:

public override void InitializeEditingControl(int rowIndex, 
        object initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
{
    base.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle);
    NumericUpDown numericUpDown = this.DataGridView.EditingControl as NumericUpDown;
    if (numericUpDown != null)
    {
        numericUpDown.BorderStyle = BorderStyle.None;
        numericUpDown.DecimalPlaces = this.DecimalPlaces;
        numericUpDown.Increment = this.Increment;
        numericUpDown.Maximum = this.Maximum;
        numericUpDown.Minimum = this.Minimum;
        numericUpDown.ThousandsSeparator = this.ThousandsSeparator;
        string initialFormattedValueStr = initialFormattedValue as string;
        if (initialFormattedValueStr == null)
        {
            numericUpDown.Text = string.Empty;
        }
        else
        {
            numericUpDown.Text = initialFormattedValueStr;
        }
    }
}

The PositionEditingControl(bool, bool, Rectangle, Rectangle, DataGridViewCellStyle, bool, bool, bool, bool) method

Cells control the positioning and sizing of their editing control. They typically do that based on their size and style (particularly the alignment settings). The grid control calls this method whenever the editing control needs to be repositioned or sized.

public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell
{        
    private Rectangle GetAdjustedEditingControlBounds(Rectangle editingControlBounds, 
                    DataGridViewCellStyle cellStyle)
    {
        // Add a 1 pixel padding on the left and right of the editing control
        editingControlBounds.X += 1;
        editingControlBounds.Width = Math.Max(0, editingControlBounds.Width - 2);

        // Adjust the vertical location of the editing control:
        int preferredHeight = cellStyle.Font.Height + 3;
        if (preferredHeight < editingControlBounds.Height)
        {
            switch (cellStyle.Alignment)
            {
                case DataGridViewContentAlignment.MiddleLeft:
                case DataGridViewContentAlignment.MiddleCenter:
                case DataGridViewContentAlignment.MiddleRight:
                    editingControlBounds.Y += (editingControlBounds.Height - preferredHeight) / 2;
                    break;
                case DataGridViewContentAlignment.BottomLeft:
                case DataGridViewContentAlignment.BottomCenter:
                case DataGridViewContentAlignment.BottomRight:
                    editingControlBounds.Y += editingControlBounds.Height - preferredHeight;
                    break;
            }
        }
        return editingControlBounds;
    }

    public override void PositionEditingControl(bool setLocation,
                                                bool setSize,
                                                Rectangle cellBounds,
                                                Rectangle cellClip,
                                                DataGridViewCellStyle cellStyle,
                                                bool singleVerticalBorderAdded,
                                                bool singleHorizontalBorderAdded,
                                                bool isFirstDisplayedColumn,
                                                bool isFirstDisplayedRow)
    {
        Rectangle editingControlBounds = PositionEditingPanel(cellBounds,
                                                              cellClip,
                                                              cellStyle,
                                                              singleVerticalBorderAdded,
                                                              singleHorizontalBorderAdded,
                                                              isFirstDisplayedColumn,
                                                              isFirstDisplayedRow);
        editingControlBounds = GetAdjustedEditingControlBounds(editingControlBounds, cellStyle);
        this.DataGridView.EditingControl.Location = new Point(editingControlBounds.X, editingControlBounds.Y);
        this.DataGridView.EditingControl.Size = new Size(editingControlBounds.Width, editingControlBounds.Height);
    }
}

The DetachEditingControl() method

This method is called by the grid control whenever the editing control is no longer needed because the editing session is ending. Custom cells may want to override this method to do some clean-up work. Here's the DataGridViewNumericUpDownCell implementation:

[
    EditorBrowsable(EditorBrowsableState.Advanced)
]
public override void DetachEditingControl()
{
    DataGridView dataGridView = this.DataGridView;
    if (dataGridView == null || dataGridView.EditingControl == null)
    {
        throw new InvalidOperationException("Cell is detached or its grid has no editing control.");
    }

    NumericUpDown numericUpDown = dataGridView.EditingControl as NumericUpDown;
    if (numericUpDown != null)
    {
        // Editing controls get recycled. Indeed, when a DataGridViewNumericUpDownCell cell gets edited
        // after another DataGridViewNumericUpDownCell cell, the same editing control gets reused for 
        // performance reasons (to avoid an unnecessary control destruction and creation). 
        // Here the undo buffer of the TextBox inside the NumericUpDown control gets cleared to avoid
        // interferences between the editing sessions.
        TextBox textBox = numericUpDown.Controls[1] as TextBox;
        if (textBox != null)
        {
            textBox.ClearUndo();
        }
    }

    base.DetachEditingControl();
}

The GetFormattedValue(object, int, ref DataGridViewCellStyle, TypeConverter, TypeConverter, DataGridViewDataErrorContexts) method

This method is called by the grid control whenever it needs to access the formatted representation of a cell's value. For the DataGridViewNumericUpDownCell, the Value is of type System.Decimal and the FormattedValue is of type System.String. So this method needs to convert a decimal into a string. The base implementation does this conversion, but the DataGridViewNumericUpDownCell still needs to override the default behavior to make sure the string returned corresponds exactly to what a NumericUpDown control would display.

protected override object GetFormattedValue(object value,
                                            int rowIndex,
                                            ref DataGridViewCellStyle cellStyle,
                                            TypeConverter valueTypeConverter,
                                            TypeConverter formattedValueTypeConverter,
                                            DataGridViewDataErrorContexts context)
{
    // By default, the base implementation converts the Decimal 1234.5 into the string "1234.5"
    object formattedValue = base.GetFormattedValue(value, rowIndex, ref cellStyle, 
              valueTypeConverter, formattedValueTypeConverter, context);
    string formattedNumber = formattedValue as string;
    if (!string.IsNullOrEmpty(formattedNumber) && value != null)
    {
        Decimal unformattedDecimal = System.Convert.ToDecimal(value);
        Decimal formattedDecimal = System.Convert.ToDecimal(formattedNumber);
        if (unformattedDecimal == formattedDecimal)
        {
            // The base implementation of GetFormattedValue (which triggers the 
                       CellFormatting event) did nothing else than 
            // the typical 1234.5 to "1234.5" conversion. But depending on the 
                       values of ThousandsSeparator and DecimalPlaces,
            // this may not be the actual string displayed. The real formatted 
                       value may be "1,234.500"
            return formattedDecimal.ToString((this.ThousandsSeparator ? "N" : "F") + this.DecimalPlaces.ToString());
        }
    }
    return formattedValue;
}

The GetPreferredSize(Graphics, DataGridViewCellStyle, int, Size) method

Whenever a grid auto-sizing feature is invoked, some cells are asked to determine their preferred height, width, or size. Custom cell types can override the GetPreferredSize method to calculate their preferred dimensions based on their content, style, properties, etc. Since the DataGridViewNumericUpDownCell cell only differs slightly from its base class, the DataGridViewTextBoxCell, as far as its representation on the screen is concerned, it calls the base implementation and returns a corrected value by taking into account the up/down buttons.

protected override Size GetPreferredSize(Graphics graphics, 
               DataGridViewCellStyle cellStyle, int rowIndex, Size constraintSize)
{
    if (this.DataGridView == null)
    {
        return new Size(-1, -1);
    }

    Size preferredSize = base.GetPreferredSize(graphics, cellStyle, rowIndex, 
               constraintSize);
    if (constraintSize.Width == 0)
    {
        const int ButtonsWidth = 16; // Account for the width of the up/down buttons.
        const int ButtonMargin = 8;  // Account for some blank pixels between the text and buttons.
        preferredSize.Width += ButtonsWidth + ButtonMargin;
    }
    return preferredSize;
}

The GetErrorIconBounds(Graphics, DataGridViewCellStyle, int) method

Custom cell types can override this method to customize the location of their error icon. By default the error icon is shown close to the right edge of the cell. But because the DataGridViewNumericUpDownCell has up/down buttons close to that right edge, it overrides the method such that the error icon and buttons don't overlap.

protected override Rectangle GetErrorIconBounds(Graphics graphics, 
                     DataGridViewCellStyle cellStyle, int rowIndex)
{
    const int ButtonsWidth = 16;

    Rectangle errorIconBounds = base.GetErrorIconBounds(graphics, cellStyle, rowIndex);
    if (this.DataGridView.RightToLeft == RightToLeft.Yes)
    {
        errorIconBounds.X = errorIconBounds.Left + ButtonsWidth;
    }
    else
    {
        errorIconBounds.X = errorIconBounds.Left - ButtonsWidth;
    }
    return errorIconBounds;
}

The Paint(Graphics, Rectangle, Rectangle, int, DataGridViewElementStates, object, object, string, DataGridViewCellStyle, DataGridViewAdvancedBorderStyle, DataGridViewPaintParts) method

This method is a critical one for any cell type. It is responsible for painting the cell. The DataGridViewNumericUpDownCell's Paint implementation uses a NumericUpDown control to do the painting. It sets various properties on the control and calls the NumericUpDown.DrawToBitmap(...) function, then the Graphics.DrawImage(...) one. This is a convenient approach for cell types that directly imitate a particular Control. However it is not very efficient and won't work for some controls like the RichTextBox or custom controls that don't support WM_PRINT. A more efficient alternative would be to paint the cell piece by piece so that it looks like a NumericUpDown control.

protected override void Paint(Graphics graphics, Rectangle clipBounds, 
                    Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState,
                              object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle,
                              DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
    if (this.DataGridView == null)
    {
        return;
    }

    // First paint the borders and background of the cell.
    base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, 
               formattedValue, errorText, cellStyle, advancedBorderStyle, 
               paintParts & ~(DataGridViewPaintParts.ErrorIcon | DataGridViewPaintParts.ContentForeground));

    Point ptCurrentCell = this.DataGridView.CurrentCellAddress;
    bool cellCurrent = ptCurrentCell.X == this.ColumnIndex && ptCurrentCell.Y == rowIndex;
    bool cellEdited = cellCurrent && this.DataGridView.EditingControl != null;

    // If the cell is in editing mode, there is nothing else to paint
    if (!cellEdited)
    {
        if (PartPainted(paintParts, DataGridViewPaintParts.ContentForeground))
        {
            // Paint a NumericUpDown control
            // Take the borders into account
            Rectangle borderWidths = BorderWidths(advancedBorderStyle);
            Rectangle valBounds = cellBounds;
            valBounds.Offset(borderWidths.X, borderWidths.Y);
            valBounds.Width -= borderWidths.Right;
            valBounds.Height -= borderWidths.Bottom;
            // Also take the padding into account
            if (cellStyle.Padding != Padding.Empty)
            {
                if (this.DataGridView.RightToLeft == RightToLeft.Yes)
                {
                    valBounds.Offset(cellStyle.Padding.Right, cellStyle.Padding.Top);
                }
                else
                {
                    valBounds.Offset(cellStyle.Padding.Left, cellStyle.Padding.Top);
                }
                valBounds.Width -= cellStyle.Padding.Horizontal;
                valBounds.Height -= cellStyle.Padding.Vertical;
            }
            // Determine the NumericUpDown control location
            valBounds = GetAdjustedEditingControlBounds(valBounds, cellStyle);

            bool cellSelected = (cellState & DataGridViewElementStates.Selected) != 0;

            if (renderingBitmap.Width < valBounds.Width ||
                renderingBitmap.Height < valBounds.Height)
            {
                // The static bitmap is too small, a bigger one needs to be allocated.
                renderingBitmap.Dispose();
                renderingBitmap = new Bitmap(valBounds.Width, valBounds.Height);
            }
            // Make sure the NumericUpDown control is parented to a visible control
            if (paintingNumericUpDown.Parent == null || !paintingNumericUpDown.Parent.Visible)
            {
                paintingNumericUpDown.Parent = this.DataGridView;
            }
            // Set all the relevant properties
            paintingNumericUpDown.TextAlign = DataGridViewNumericUpDownCell.TranslateAlignment(cellStyle.Alignment);
            paintingNumericUpDown.DecimalPlaces = this.DecimalPlaces;
            paintingNumericUpDown.ThousandsSeparator = this.ThousandsSeparator;
            paintingNumericUpDown.Font = cellStyle.Font;
            paintingNumericUpDown.Width = valBounds.Width;
            paintingNumericUpDown.Height = valBounds.Height;
            paintingNumericUpDown.RightToLeft = this.DataGridView.RightToLeft;
            paintingNumericUpDown.Location = new Point(0, -paintingNumericUpDown.Height - 100);
            paintingNumericUpDown.Text = formattedValue as string;

            Color backColor;
            if (PartPainted(paintParts, DataGridViewPaintParts.SelectionBackground) && cellSelected)
            {
                backColor = cellStyle.SelectionBackColor;
            }
            else
            {
                backColor = cellStyle.BackColor;
            }
            if (PartPainted(paintParts, DataGridViewPaintParts.Background))
            {
                if (backColor.A < 255)
                {
                    // The NumericUpDown control does not support transparent back colors
                    backColor = Color.FromArgb(255, backColor);
                }
                paintingNumericUpDown.BackColor = backColor;
            }
            // Finally paint the NumericUpDown control
            Rectangle srcRect = new Rectangle(0, 0, valBounds.Width, valBounds.Height);
            if (srcRect.Width > 0 && srcRect.Height > 0)
            {
                paintingNumericUpDown.DrawToBitmap(renderingBitmap, srcRect);
                graphics.DrawImage(renderingBitmap, new Rectangle(valBounds.Location, valBounds.Size),
                                   srcRect, GraphicsUnit.Pixel);
            }
        }
        if (PartPainted(paintParts, DataGridViewPaintParts.ErrorIcon))
        {
            // Paint the potential error icon on top of the NumericUpDown control
            base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, 
                       value, formattedValue, errorText,
                       cellStyle, advancedBorderStyle, DataGridViewPaintParts.ErrorIcon);
        }
    }
}

The ToString() method

This method returns a compact string representation of the cell. The DataGridViewNumericUpDownCell's implementation follow the standard cells' standard.

public override string ToString()
{
return "DataGridViewNumericUpDownCell { ColumnIndex=" + ColumnIndex.ToString(CultureInfo.CurrentCulture) + ", 
                                        RowIndex=" + RowIndex.ToString(CultureInfo.CurrentCulture) + " }";
}

Editing Control Implementation Details

Now let's dig into the details of creating the editing control DataGridViewNumericUpDownEditingControl for the custom cell.

Class definition and constructor

All editing controls need to implement the IDataGridViewEditingControl interface that standardizes the interactions between the grid or cell and the editing control. They also need to derive from System.Windows.Forms.Control.

namespace DataGridViewNumericUpDownElements
{
    class DataGridViewNumericUpDownEditingControl : NumericUpDown, IDataGridViewEditingControl
    {
        public DataGridViewNumericUpDownEditingControl()
        {
            // The editing control must not be part of the tabbing loop
            this.TabStop = false;
        }
    }
}

Implementation of the IDataGridViewEditingControl interface

The DataGridViewNumericUpDownEditingControl control implements that interface via virtual properties and methods so that developers can derive from it and customize the behavior.

The properties and methods are:

// Property which caches the grid that uses this editing control
public virtual DataGridView EditingControlDataGridView

// Property which represents the current formatted value of the editing control
public virtual object EditingControlFormattedValue

// Property which represents the row in which the editing control resides
public virtual int EditingControlRowIndex

// Property which indicates whether the value of the editing control has changed or not
public virtual bool EditingControlValueChanged

// Property which determines which cursor must be used for the editing panel, 
i.e. the parent of the editing control.
public virtual Cursor EditingPanelCursor

// Property which indicates whether the editing control needs to be repositioned when its value changes.
public virtual bool RepositionEditingControlOnValueChange

// Method called by the grid before the editing control is shown so it can adapt to the provided cell style.
public virtual void ApplyCellStyleToEditingControl(DataGridViewCellStyle dataGridViewCellStyle)

// Method called by the grid on keystrokes to determine if the editing control is interested in the key or not.
public virtual bool EditingControlWantsInputKey(Keys keyData, bool dataGridViewWantsInputKey)

// Returns the current value of the editing control.
public virtual object GetEditingControlFormattedValue(DataGridViewDataErrorContexts context)

// Called by the grid to give the editing control a chance to prepare itself for the editing session.
public virtual void PrepareEditingControlForEdit(bool selectAll)

Notifying the grid of value changes

On top of implementing the IDataGridViewEditingControl interface, an editing control typically needs to forward the fact that its content changed to the grid via the DataGridView.NotifyCurrentCellDirty(...) method. Often an editing control must override protected virtual methods to be notified of the content changes and be able to forward the information to the grid. In this particular case, the DataGridViewNumericUpDownEditingControl overrides two methods:

protected override void OnKeyPress(KeyPressEventArgs e)
protected override void OnValueChanged(EventArgs e)

Column Implementation Details

As mentioned earlier, the creation of a custom column type is optional. The DataGridViewNumericUpDownCell can be used by any column type, including the base DataGridViewColumn type:

DataGridViewColumn dataGridViewColumn = 
      new DataGridViewColumn(new DataGridViewNumericUpDownElements.DataGridViewNumericUpDownCell());
...
DataGridViewNumericUpDownCell dataGridViewNumericUpDownCell = 
      dataGridViewColumn.CellTemplate as DataGridViewNumericUpDownCell;
dataGridViewNumericUpDownCell.DecimalPlaces = 3;

Custom columns typically expose the special properties of the cell type they're associated with. For example, the DataGridViewNumericUpDownColumn exposes the DecimalPlaces, Increment, Maximum, Minimum, and ThousandSeparator properties.

Class definition and constructor

The DataGridViewNumericUpDownColumn class simply derives from the DataGridViewColumn class and its constructor uses a default DataGridViewNumericUpDownCell for the cell template.

namespace DataGridViewNumericUpDownElements
{
    public class DataGridViewNumericUpDownColumn : DataGridViewColumn
    {
        public DataGridViewNumericUpDownColumn() : base(new DataGridViewNumericUpDownCell())
        {
        }
    }
}

Defining column properties

Let's take a closer look at how a column type typically implements a property. The DecimalPlaces property of the DataGridViewNumericUpDownColumn class is implemented as follows for example:

[
    Category("Appearance"),
    DefaultValue(DataGridViewNumericUpDownCell.DATAGRIDVIEWNUMERICUPDOWNCELL_defaultDecimalPlaces),
    Description("Indicates the number of decimal places to display.")
]
public int DecimalPlaces
{
    get
    {
        if (this.NumericUpDownCellTemplate == null)
        {
            throw new InvalidOperationException("Operation cannot be completed because 
                    this DataGridViewColumn does not have a CellTemplate.");
        }
        return this.NumericUpDownCellTemplate.DecimalPlaces;
    }
    set
    {
        if (this.NumericUpDownCellTemplate == null)
        {
            throw new InvalidOperationException("Operation cannot be completed because 
                    this DataGridViewColumn does not have a CellTemplate.");
        }
        // Update the template cell so that subsequent cloned cells use the new value.
        this.NumericUpDownCellTemplate.DecimalPlaces = value;
        if (this.DataGridView != null)
        {
            // Update all the existing DataGridViewNumericUpDownCell cells in the column accordingly.
            DataGridViewRowCollection dataGridViewRows = this.DataGridView.Rows;
            int rowCount = dataGridViewRows.Count;
            for (int rowIndex = 0; rowIndex < rowCount; rowIndex++)
            {
                // Be careful not to unshare rows unnecessarily. 
                // This could have severe performance repercussions.
                DataGridViewRow dataGridViewRow = dataGridViewRows.SharedRow(rowIndex);
                DataGridViewNumericUpDownCell dataGridViewCell = 
                          dataGridViewRow.Cells[this.Index] as DataGridViewNumericUpDownCell;
                if (dataGridViewCell != null)
                {
                    // Call the internal SetDecimalPlaces method instead of the 
                                        property to avoid invalidation 
                    // of each cell. The whole column is invalidated later in a single 
                                        operation for better performance.
                    dataGridViewCell.SetDecimalPlaces(rowIndex, value);
                }
            }
            this.DataGridView.InvalidateColumn(this.Index);
            // TODO: Call the grid's autosizing methods to autosize the column, 
                             rows, column headers / row headers as needed.
        }
    }
}

Regarding the last comment about auto-sizing features, the custom column may want to automatically auto-size the affected grid elements, like the standard columns do. The DecimalPlaces property has an effect on the rendering of the cell and its preferred size. As a result, changing the property value may require adjusting the column's width, some rows' height, the column headers' height, and the row headers' width. All depends on their individual auto-sizing settings.

For example, if the inherited auto-size mode of the column is DataGridViewAutoSizeColumnMode.AllCells, then the protected DataGridView.AutoResizeColumn(int columnIndex, DataGridViewAutoSizeColumnMode autoSizeColumnMode, bool fixedHeight) method needs to be called with the parameter DataGridViewAutoSizeColumnMode.AllCells. Similarly, if the DataGridView.ColumnHeadersHeightSizeMode property is set to DataGridViewColumnHeadersHeightSizeMode.AutoSize, then the protected DataGridView.AutoResizeColumnHeadersHeight(int columnIndex, bool fixedRowHeadersWidth, bool fixedColumnWidth) method needs to be called, etc.

Because these methods are protected, this automatic auto-sizing can only be accomplished correctly when deriving from the DataGridView control. The custom column type can then call a public method on the derived control that does all the automatic auto-sizing work:

    ((MyDataGridView) this.dataGridView).OnGlobalColumnAutoSize(this.Index);

The derived MyDataGridView class defines a public method OnGlobalColumnAutoSize(int columnIndex) that calls AutoResizeColumn(...), AutoResizeColumnHeadersHeight(...), AutoResizeRows(...), and AutoResizeRowHeadersWidth(...) as needed.

Tip   Getting the OnGlobalColumnAutoSize(int columnIndex) method right requires a lot of work and could be the subject of an article by itself.

Here's an implementation that takes advantage of the fact that changing the column's DefaultCellStyle property internally triggers all the auto-sizing adjustments.

        // MyDataGridView.OnGlobalColumnAutoSize implementation
        public void OnGlobalColumnAutoSize(int columnIndex)
        {
            if (columnIndex < -1 || columnIndex >= this.Columns.Count)
            {
                throw new ArgumentOutOfRangeException("columnIndex");
            }
            OnColumnDefaultCellStyleChanged(new DataGridViewColumnEventArgs(this.Columns[columnIndex]));
        }

Besides the DecimalPlaces, Increment, Maximum, Minimum and ThousandSeparator properties, the column is also defining the critical CellTemplate property as follows:

[
    Browsable(false),
    DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
public override DataGridViewCell CellTemplate
{
    get
    {
        return base.CellTemplate;
    }
    set
    {
        DataGridViewNumericUpDownCell dataGridViewNumericUpDownCell = 
                   value as DataGridViewNumericUpDownCell;
        if (value != null && dataGridViewNumericUpDownCell == null)
        {
            throw new InvalidCastException("Value provided for CellTemplate must be of type 
                      DataGridViewNumericUpDownElements.DataGridViewNumericUpDownCell or derive from it.");
        }
        base.CellTemplate = value;
    }
}

The CellTemplate property is used for example when the DataGridViewRowCollection.Add() gets called. Because no explicit cells are provided, a clone of the DataGridView.RowTemplate is added. By default, the RowTemplate is populated with clones of each column's CellTemplate.

Key methods to override

A custom column typically overrides the ToString() method.

// Returns a standard compact string representation of the column.
public override string ToString()
{
    StringBuilder sb = new StringBuilder(100);
    sb.Append("DataGridViewNumericUpDownColumn { Name=");
    sb.Append(this.Name);
    sb.Append(", Index=");
    sb.Append(this.Index.ToString(CultureInfo.CurrentCulture));
    sb.Append(" }");
    return sb.ToString();
}

In some rare cases, the column type may want to expose a property that has no equivalent property at the cell level. Examples of that are DataGridViewLinkColumn.Text and DataGridViewImageColumn.Image. In those cases, the column class needs to override the Clone method to copy over that property.

// The custom Clone implementation needs to copy over all the specific properties of the column that 
// the associated cell does not expose.
public override object Clone()
{
    DataGridViewXXXColumn dataGridViewColumn = base.Clone() as DataGridViewXXXColumn;
    if (dataGridViewColumn != null)
    {
        dataGridViewColumn.YYY = this.YYY;
    }
    return dataGridViewColumn;
}

The DataGridViewNumericUpDownColumn does not have such a property so it does not need to override the Clone method.

Screenshot of a DataGridViewNumericUpDownColumn

This is a screenshot of the sample application that makes use of the custom cell and column (Figure 2).

Aa730881.winf_numericupdown_2(en-US,VS.80).gif

Figure 2. A DataGridViewNumericUpDownColumn with a few cells

Conclusion

In this article, you learned how to build a custom cell and column based on the Windows Forms NumericUpDown control for use in the DataGridView control to extend the functionality of the grid. In turn, the custom cell and column easily lets users enter numeric values in the grid.

© Microsoft Corporation. All rights reserved.