Columns With Style

Ulrich Sprick

Prev
Next
Index

8
A Simple Combobox Column

Introduction

My first attempt to create a custom datagrid columnstyle was to implement a combobox column. I ran into lots of trouble, and, because of too many problems at once, failed. Now, after a profound tour through the depths of custom columnstyles, I made my second attempt. Armed with the knowledge of the previous lessons, I hope this time I'll succeed...

The implementation follows the pattern of the TextboxColumn.7.cs. The columnstyle hosts a control derived from Windows.Forms.ComboBox.

Download the source code for this article.

Hosted Combobox Control Implementation

The purpose of EscapeFlag, EditStarted and column has not changed. The initializing constructor stores a reference to the hosting column:

public class DatagridCombobox : System.Windows.Forms.ComboBox
{
    // Implements a combobox that is hosted by the combobox columnstyle
    //

    public bool EscapeFlag = false;
        // Set if the combobox recognizes an escape key
        // used to prevent the grid from loosing focus

    public bool EditStarted = false;
        // set if value changed while visible.
        // Used to prevent hiding the combobox in ConcedeFocus()
        // Used to switch on caret control

    protected DatagridComboboxColumn column;
        // a reference to the hosting column

    public DatagridCombobox( DatagridComboboxColumn column )
    {
        // initializing constructor
        this.column = column;
        this.Visible = false;
    }
    ...
}

The OnClick() method switches from cell navigation mode to text cursor control mode by setting the EditStarted flag. OnTextChanged() is identical, as well as the key input processing in ProcessKeyMessage() and ProcessCmdKey():

protected override void OnClick( EventArgs e )
{
    // switch on caret control
    this.EditStarted = true;
    base.OnClick( e );
}

protected override void OnTextChanged( EventArgs e )
{
    // notify the grid that the cell has been modifed
    if ( this.Visible )
    {
        Helper.Trace( "OnSelectedItemChanged()" );
        this.EditStarted = true;
        ((IDataGridColumnStyleEditingNotificationService)this.column).ColumnStartedEditing(
            this
            );
    }
    base.OnSelectedItemChanged( e );
}

ProcessKeyMessage() now has a minimum implementation. The TAB key workaround for the textbox is not needed for the combobox. The ESC key is caught to prevent the grid from loosing focus when the user cancels edits.

protected override bool ProcessKeyMessage( ref Message m )
{
    // Hide key messages from the grid
    // Helper.Trace( "ProcessKeyMessage()", key );
    switch ( (Keys)((int) m.WParam) )
    case Keys.Escape:
        // Flag escape to Commit() and prevent sound
        this.EscapeFlag = true;
        return true;

    default:
        break;
    }

    return base.ProcessKeyMessage( ref m );
}

The ProcessCmdKey() method (like the textbox) captures the the Left and Right keys in cell navigation mode, and translates them in (Shift) TAB keys. Additionally, Enter and F2 toggle the caret control betweeb cell navigation and caret control mode:

protected override bool ProcessCmdKey( ref Message m, Keys keyData )
{
    // Support for text cursor control
//          Helper.Trace( "ProccessCmdKey()", m.Msg, keyData, m.WParam, System.String.Format("{0:X}", m.LParam.ToInt32()) );
    switch ( keyData )
    {
        case Keys.Left:
            // exchange keys for cell navigation
            if ( this.EditStarted ) break;
            keyData = Keys.Tab | Keys.Shift;
            return this.ProcessDialogKey( keyData );

        case Keys.Right:
            // exchange keys for cell navigation
            if ( this.EditStarted ) break;
            keyData = Keys.Tab;
            return this.ProcessDialogKey( keyData );

        case Keys.Enter:
            // toggle caret control mode
            goto case Keys.F2;

        case Keys.F2:
            // toggle caret control mode
            this.EditStarted ^= true;
            if ( this.EditStarted ) this.SelectionStart = this.Text.Length;
            else this.SelectAll();
            return true;
    }

    // default behaviour
    return base.ProcessCmdKey( ref m, keyData );
}

The Column Style Implementation

Nothing new at the columnstyle constructor: It instantiates a new DatagridCombobox and passes a reference to the columnstyle to the constructor:

public class DatagridComboboxColumn : DataGridColumnStyle
{
    // Implements the combobox columnstyle

    public DatagridCombobox Combobox;
        // the hosted edit control

    public DatagridComboboxColumn()
    {
        // default constructor
        this.Combobox = new DatagridCombobox( this );
    }
    ...
}

SetDataGridInColumn() adds or removes the combobox to/from the controls collection of the grid:

protected override void SetDataGridInColumn( DataGrid grid )
{
    // add the Combobox to the controls collection of the grid
    Helper.Trace( "SetDataGridInColumn()", grid );
    if ( this.Combobox.Parent != grid )
    {
        if ( this.Combobox.Parent != null ) this.Combobox.Parent.Controls.Remove( this.Combobox);
        if ( grid != null ) grid.Controls.Add( this.Combobox );
    }
}

The Paint method prints the column data. The cell value is retrieved from the data source with GetColumnValueAtRow(), tranlated into a string representation and drawn within the cell boundaries:

protected override void Paint( Graphics g, Rectangle bounds, CurrencyManager source, int rowNum)
{
    this.Paint( g, bounds, source, rowNum, false );
}

protected override void Paint( Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, bool alignToRight )
{
//          Helper.Trace( "Paint()", rowNum );
    object value = this.GetColumnValueAtRow( source, rowNum );
    string text;
    if ( value == null || value == DBNull.Value ) text = this.NullText;
    else text = value.ToString();
    Brush bgBrush = new SolidBrush( this.DataGridTableStyle.BackColor );
    Brush fgBrush= new SolidBrush( this.DataGridTableStyle.ForeColor );
    g.FillRectangle( bgBrush, bounds );
    g.DrawString( text, this.DataGridTableStyle.DataGrid.Font, fgBrush, bounds );
}

The Edit() methods prepares the column for editing. Here is a difference to the textbox column implementation: The Textbox had to select it's contents - this is not necessary for a combobox. The cell value is retrieved from the data source and converted into a string as in the Paint() method above, and then assigned to the Combobox.Text property. The last step is to make the combobox visible and give it the focus:

protected override void Edit( CurrencyManager source, int rowNum, Rectangle bounds, bool readOnly, string instantText, bool cellIsVisible )
{
    // prepare the cell for editing
    Helper.Trace( "Edit()", rowNum, instantText + cellIsVisible );
    if ( ! this.Combobox.Visible
    {
        // initialize the combobox value
        object value = this.GetColumnValueAtRow( source, rowNum );
        if ( value == null ) this.Combobox.Text = this.NullText;
        else this.Combobox.Text = value.ToString();
        // show the edit control in the position of the current cell
        this.Combobox.Bounds = bounds;
        this.Combobox.Show();
    }
    this.Combobox.Focus();
}

Combobox initialization is performed only if the combobox is not visible. This prevents the combobox from loosing the first user character input when editing a combobox cell in the add-new-row of the grid - similar to the textbox column.

Abort() is called as a response to ESC,

protected override void Abort( int rowNum )
{
    // discard edits
    Helper.Trace( "Abort()", rowNum, this.MappingName );
    this.Combobox.EditStarted = false;
    this.Combobox.EscapeFlag = false;
    this.Combobox.Hide();
}

whereas Commit() stores the current value in the data source. The check on the combobox visibility ensures that the code is executed more than once, and prevents execution for a different row (which would cause that datasource field to be unexpetedly overwritten):

protected override bool Commit( CurrencyManager source, int rowNum )
{
    // Save current value to the data source
    Helper.Trace( "Commit()", rowNum );
    if ( this.Combobox.Visible )
    {
        // prevent updating wrong rows
        this.SetColumnValueAtRow( source, rowNum, this.Combobox.Text );
        this.Combobox.Hide();
        this.Combobox.EditStarted = false;
        // prevent loosing focus
        if ( this.Combobox.EscapeFlag )
        {
            this.Combobox.EscapeFlag = false;
            this.Combobox.Parent.Focus();
        }
    }
    return true;
}

Both methods reset the Escape and EditStarted flags to their initial values, so that cell edits always starts in the same state.

As soon as the user starts to modify the cell contents, Combobox.OnTextChanged() calls ColumnStartedEditing(), which causes the grid to call ConcedeFocus(). Checking the EditStarted flag prevent the combobox from beeing hidden in this case:

protected override void ConcedeFocus()
{
    // hide the control after editing in the add-new-row
    Helper.Trace( "ConcedeFocus()", "EditStarted=" + this.Combobox.EditStarted );
    if ( ! this.Combobox.EditStarted ) this.Combobox.Hide();
}

GetMinimumHeight(), GetPreferredHeight() and GetPreferredSize() need not to be mentioned, I think.

Filling the Dropdown List

Filling the dropdown list is done in the constructor of the form. The Items.AddRange() method takes a string array constructor with an initializer list as parameter:

public Form1()
{
    // initialize form
    InitializeComponent();
    // additional inits: create and add a combobox column
    DatagridComboboxColumn comboboxColumn;
    comboboxColumn = new DatagridComboboxColumn();
    comboboxColumn.HeaderText = "City";
    comboboxColumn.MappingName = "City";
    comboboxColumn.Width = 100;
    this.dataGridTableStyle1.GridColumnStyles.Add( comboboxColumn );
    // fill the dropdown list
    comboboxColumn.Combobox.Items.AddRange( new string[] { "New York", "Milano", "Paris", "Bielefeld" });
}

Adding a few strings should be sufficient for the first rounds. The source above is in ComboboxColumn.1.cs.

Now, how does it behave? Well, not bad, as long as you use the mouse. You can change an existing cell value by picking a dropdown list item, or type in a new value. Even creating a new row works, somehow. Missing points:

  1. No cell navigation with up/down/home/end keys and combinations with Ctrl and Shift.
  2. In the add-new-row of the grid, when typing a new value, the first letter is lost if you start with editing the City column.
  3. Alt-down does not show the dropdown list.

Improving Cell Navigation

In ProcessCmdKey(), the default case selector of the keyData switch does the job. Unfortunately, ProcessDialogKey() does not work for all key and modifier combinations. So I tried to call the ProcessKeyMessage() method directly, hoping that this bypasses the combobox code and guides the key message to the grid for processing. And, in fact - it does! If the grid is not beeing edited, a call to ProcessKeyMessage() yields back the lost cell navigation feature.

A helper is used to store the key without Control/Shift/Alt modifacators. This makes the following switch much simpler. Other keys are guided to the base class method:

protected override bool ProcessCmdKey( ref Message m, Keys keyData )
{
    // Support for text cursor control
    switch ( keyData )
    {
        case ...

        default:
            // special treatment for navigation keys
            // mask out Shift/Control/Alt modification keys
            Keys helper = keyData;
            helper &= ~(Keys.Shift | Keys.Control | Keys.Alt);
            // treat navigation keys
            switch ( helper )
            {
                case Keys.Left:
                case Keys.Right:
                case Keys.Up:
                case Keys.Down:
                case Keys.Home:
                case Keys.End:
                case Keys.PageUp:
                case Keys.PageDown:
                    // allow cell navigation
                    if ( this.EditStarted ) break;
                    return base.ProcessKeyMessage( ref m );

                default:
                    break;
            }

            // no navigation key
            break;
    }

    // default behaviour
    return base.ProcessCmdKey( ref m, keyData );
}

Dropdown Control

A look at the program trace in the output window shows that the Alt-Down key does not come through to ProcessKeyMessage(), so ProcessCmdKey() is the method of choice. Here, ProcessDialogKey() does not work as it did for Up and Down keys (see above). So I decided to control the dropdown state by code:

protected override bool ProcessCmdKey( ref Message m, Keys keyData )
{
    // Support for text cursor control
    switch ( keyData )
    {
        ...
        case Keys.Down | Keys.Alt:
            // toggle the dropdown state
            this.DroppedDown ^= true;
            this.EditStarted = true;
            return true;

        case Keys.Enter:
        case Keys.F2:
            // toggle caret control mode
            this.EditStarted ^= true;
            // put the caret behind the text
            if ( this.EditStarted ) this.SelectionStart = this.Text.Length;
            else
            {
                this.SelectAll();
                // hide the dropdown listbox
                this.DroppedDown = false; 
            }
            return true;
    }

    // default behaviour
    return base.ProcessCmdKey( ref m, keyData );
}

Additionally, if one ends editing by Enter or F2, the dropdown list must explicitly be hidden again.

Key Input Loss

In the add-new-row of the grid, the first character is lost if the user enters a new cell value. This is similar to the textbox column. To overcome this problem, I modified the Edit() method in the following way:

protected override void Edit( CurrencyManager source, int rowNum, Rectangle bounds, bool readOnly, string instantText, bool cellIsVisible )
{
    // Prepare the cell for editing
    Helper.Trace( "Edit()", rowNum, "instantText=", instantText, "CellIsVisible=", cellIsVisible );
    if ( this.Combobox.EditStarted )
    {
        // in add-new-row, combobox is still visible
        // put the focus back on the control
        this.Combobox.Focus();
        // prevent overwriting the user input
        this.Combobox.Select( this.Combobox.Text.Length, 0 );
    }
    else
    {
        // show the edit control in the position of the current cell
        object value = this.GetColumnValueAtRow( source, rowNum );
        if ( value == null ) this.Combobox.Text = this.NullText;
        else this.Combobox.Text = value.ToString();
        this.Combobox.Bounds = bounds;
        this.Combobox.Show();
        this.Combobox.Focus();
    }
}

If the textbox if visible, I assume we are creating a new row and entering text in the combobox. The focus is taken from the grid and put back on the combobox. This selects the combobox text (which consist of one (the first) character). To prevent this character from beeing overwritten with the next character input, the text is deselected and the text cursor is placed behind the last (and only) character with the Select() method.

The code described above is in ComboboxColumn.2.cs.

Edits in a New Row

There is another problem with creating a new row with the combobox column. Try to select an entry from the dropdown list with the keyboard (Alt-Down to drop down the list, Down to selet the first entry). As soon as the combobox value changes - that is, when you select the first dropdown list entry - the dropdown list disappears. This is because the grid removes the focus from the hosted control when ColumnStartedEditing() is called. You can check this by commenting out the Combobox.Focus() call. Unforturnately, loosing focus causes the combobox to hide it's dropdown list. You have to dropdown the list again. Pretty uncool...

First I tried to keep the focus on the combobox somehow, but with no luck. I guess I had to overwrite some grid method for this. But that is not on the list. Not yet...

Next, I tried to restore the dropped down status of the list in Edit(). The approach is based on the fact that ConcedeFocus() is called before Edit() only if the cell value is changed in the add-new-row.

protected override void ConcedeFocus()
{
    // hide the control after editing in the add-new-row
    Helper.Trace( "ConcedeFocus()", "EditStarted=", this.Combobox.EditStarted );
    // Set the drop down flag if the list is visible and edit started
    if ( this.Combobox.EditStarted ) this.droppedDown |= this.Combobox.DroppedDown;
    else this.Combobox.Hide();
}

protected override void Edit( CurrencyManager source, int rowNum, Rectangle bounds, bool readOnly, string instantText, bool cellIsVisible )
{
    // Prepare the cell for editing
    Helper.Trace( "Edit()", rowNum, "instantText=", instantText, "CellIsVisible=", cellIsVisible );
    if ( this.Combobox.EditStarted )
    {
        // in add-new-row, combobox is still visible
        // put focus back on the control
        this.Combobox.Focus();
        // prevent overwriting the user input
        this.Combobox.Select( this.Combobox.Text.Length, 0 );
        // restore dropdown status
        this.Combobox.DroppedDown = this.droppedDown;
        this.droppedDown = false;
    }
    else...
}

The droppedDown flag is set in ConcedeFocus() if EditStarted is true. In Edit(), this flag is used to restore the dropped down state of the list. Then Edit() resets the flag.

Unfortunately, dropping down the list causes the combobox to raise a click event - which in turn sets the EditStarted flag. And leads to another problem: If you dropdown the combobox list in the add-new-row, either by clicking the dropdown button, or by entering Alt-Down, and then activate a different cell, but still in the add-new-row, the combobox does not disappear as it should. The reason is that ConcedeFocus() now refuses to hide the combobox because EditStarted is true, although the cell has not been changed! This calls for another helper flag:

public class DatagridCombobox : System.Windows.Forms.ComboBox
{
    public bool ValueChanged = false;
        // set in OnTextChanged(), passed to ConcedeFocus() to set the
        // Dropdown flag properly.

	...

    protected override void OnTextChanged( EventArgs e )
    {
        // notify the grid that the cell has been modifed
        if ( this.Visible )
        {
            Helper.Trace( "OnSelectedItemChanged()" );
            this.EditStarted = true;
            this.ValueChanged = true;
            ((IDataGridColumnStyleEditingNotificationService)this.column).ColumnStartedEditing(
                this
                );
        }
        base.OnSelectedItemChanged( e );
    }

ValueChanged is evaluated in CondedeFocus(), and reset in Edit() (not shown):

protected override void ConcedeFocus()
{
    // hide the control after editing in the add-new-row
    Helper.Trace( "ConcedeFocus()", "EditStarted=" + this.Combobox.EditStarted );
    // Set the drop down flag if the list is visible and edit started
    if ( this.Combobox.ValueChanged ) this.droppedDown |= this.Combobox.DroppedDown;
    else this.Combobox.Hide();
}

You can find the code in ComboboxColumn.3.cs. A quick test shows that the solution is working somehow, but not perfect:

The listbox flicker appears only if you start editing a new row with a combobox column. If you start editing for with the ID column for example, then the add-new-row becomes "permanent" as soon as you leave that cell.

Conclusion

Success this time? No, not really. The problems described above make me think, that this approach is not the ideal solution for a combobox column. Currently I am thinking of a different approach: I'll try to compose a combobox from a textbox, a listbox and a button. So, one more try, and: See you the next article!

History

2003-09-09 Initial publication.

Feedback

If you have a question or want to send a comment, just mail to ulrich.devcom@i-syn.com"

 


Back to Top