Columns With Style

Ulrich Sprick

Prev
Next
Index

9
Dropdown Listbox Column

Downloads

The source code for this article.

Introduction

The first approach to implement a combobox column was not completely successful. If you create a new row with the combobox by selecting an entry from the list, the grid grabs the focus from the combobox, which causes the combobox to hide the dropdown list. In the following Edit(), I was able to restore the dropdown state of the list, but this results in a visible flicker on the screen when adding new rows under certain circumstances.

Unacceptable.

This article presents a different approach: A combobox, composed of a textbox, a listbox and a button. This approach would certainly eliminate the problem with the hiding dropdown list when the grid removes the focus from the combobox, but it might bear other problems... But let me give you an outline of the code first.

In DDListboxColumn.1.cs, four classes are implemented:

Version 1

The first implementation in DDListboxColumn.a.cs is based on a very standard implementation. The column host 3 controls for editing. The Paint() method draws the cell contents, SetDataGridInColumn() registers the hosted controls in the controls collection of the grid. The Edit() method shows the textbox and the button in the cell boundaries and initializes the textbox with the value from the data source. Commit() hides the hosted controls and stores the current value back in the data source, and ConcedeFocus() unconditionally hides the controls.

To drop down the listbox, I have overridden the OnClick() of the button:

override protected void OnClick( System.EventArgs e )
{
    // Control the list dropdown
    Helper.Trace( "Button.OnClick()", "started" );
    // call overridden base class method
    base.OnClick( e );
    // position the listbox below the current cell
    this.column.Listbox.Left = this.column.Textbox.Left;
    this.column.Listbox.Top = this.column.Textbox.Top + this.column.Textbox.Height;
    this.column.Listbox.Width = this.column.Textbox.Width + this.Width;
    // show the listbox
    this.column.Listbox.Show();
    this.column.Listbox.Focus();
    Helper.Trace( "Button.OnClick()", "ended" );
}

The following image shows what's on the screen if you run the application and click on the cell button:

Interesting: The Textbox still has the focus! Here is the program trace, starting with the button click event:

13:20.089 Edit() 2 instantText=CellIsVisible=True
13:25.186 Button.OnClick() started
13:25.366 Commit() 2
13:25.526 Commit() 2
13:25.637 Commit() 2
13:25.767 Commit() 2
13:25.767 Commit() 2
13:25.767 Edit() 2 instantText=CellIsVisible=True
13:25.787 Button.OnClick() ended

After cell activation at 13:20, the button has been clicked. This causes the grid to terminate the cell edit by calling Commit() several times. Putting the focus on the listbox causes a new cell edit at 13:25.767, indicated by the Edit() trace (you can check this if you comment out the line this.column.Listbox.Focus() and click on the Listbox).

Obviously, the grid tracks the focus of the hosted control(s). If the textbox looses the focus, the grid terminates the current cell edit, indicated by the Commit() calls in the trace log. If you omit Listbox.Focus() in the code above, you can see it in more detail: The Commit() calls hide textbox and button. If you click on the listbox then, the grid makes another Commit()/Edit() call at 37:01 and starts a new edit, which puts the focus back on the textbox again:

36:55.874 Edit() 2 instantText=CellIsVisible=True
36:58.028 Button.OnClick() started
36:58.198 Commit() 2
36:58.358 Commit() 2
36:58.468 Commit() 2
36:58.608 Commit() 2
36:58.608 Button.OnClick() ended
37:01.112 Commit() 2
37:01.112 Edit() 2 instantText=CellIsVisible=True

Interesting: If you now click on the textbox, listbox or the button, nothing happens. All controls remain visible, no Commit()s, no Edit()s:

43:40.708 Button.OnClick() started
43:40.708 Button.OnClick() ended
43:42.020 Button.OnClick() started
43:42.020 Button.OnClick() ended
43:43.843 Button.OnClick() started
43:43.843 Button.OnClick() ended

This seems very promising!

But, after a while of clicking around, I found that the grid does not always call Edit() when the button has been clicked: With textbox and listbox visible, click on the Company cell of the same row. The controls hide. Then click on the City cell again. Textbox and Listbox reappear. Click on the button. The listbox shows up, and all controls are hidden when the grid calls Commit(). But there is no Edit()...

Version 2

What if we do not hide the controls in Commit() after a button click? I checked this out in DDListboxColumn.b.cs. The column got a new helper flag, called "NoHide",...

public class DatagridComboboxColumn : DataGridColumnStyle
{
    public bool NoHide = false;
        // prevents hiding the controls in Commit()

    public int CurrentRow;
        // Edit() stores the current row. Used in Button.OnClick()
}

...which is set in the button's OnClick() method:

public class DatagridDDButton : System.Windows.Forms.Button
{
    ...

    override protected void OnClick( System.EventArgs e )
    {
        // Control the list dropdown
        Helper.Trace( "Button.OnClick()", "started" );
        this.column.NoHide = true;
        // call overridden base class method
        base.OnClick( e );
        // position below the current cell
        this.column.Listbox.Left = this.column.Textbox.Left;
        this.column.Listbox.Top = this.column.Textbox.Top + this.column.Textbox.Height;
        this.column.Listbox.Width = this.column.Textbox.Width + this.Width;
        // show the listbox
        this.column.Listbox.Show();
        // this.column.Listbox.Focus();
        this.column.Textbox.Focus();
        this.column.DataGridTableStyle.DataGrid.BeginEdit( this.column, this.column.CurrentRow );
        Helper.Trace( "Button.OnClick()", "ended" );
    }
}

This prevents Commit() to hide the hosted controls:

public class DatagridComboboxColumn : DataGridColumnStyle
{
    protected override bool Commit( CurrencyManager source, int rowNum )
    {
        // Save current value to the data source
        Helper.Trace( "Commit()", rowNum );
        // prevent updating wrong rows
        if ( this.Textbox.Visible )
        {
            if ( ! this.NoHide )
            {
                // update the data source
                this.SetColumnValueAtRow( source, rowNum, this.Textbox.Text );
                // hide the hosted controls
                this.Textbox.Hide();
                this.Button.Hide();
                this.Listbox.Hide();
            }
        }
        return true;
    }
}

As a consequence, the grid does no longer call the edit method for the current cell after the cell button has been clicked. In other words, Edit() is never again called after a button click. But, fortunately, we can explicitly tell the grid to do it: this.column.DataGridTableStyle.DataGrid.BeginEdit( this.column, this.column.CurrentRow ). The current row has been stored before in the Edit() method.

Some testing reveals: Very promising!

Now there is left a similar problem with the add-new-row of the grid: ConcedeFocus() is now the method that causes trouble! Again, the NoHide flag prevents ConcedeFocus() to hide the hosted controls:

public class DatagridComboboxColumn : DataGridColumnStyle
{
    protected override void ConcedeFocus()
    {
        // Hide hosted controls
        Helper.Trace( "ConcedeFocus()" );
        if ( ! this.NoHide )
        {
            this.Textbox.Hide();
            this.Button.Hide();
            this.Listbox.Hide();
        }
    }
}

A Test: Works! Well, at the first look. Later, I ran into different problems with ColumnStartedEditing() because ConcedeFocus() did not always hide the controls. This caused two Edit() calls with no Commit() or ConcedeFocus(), which in turn caused the grid to insert a second add-new-row! Also, the application crashed sometimes...

Version 3

The next thought: If I can BeginEdit(), I also should be able to EndEdit(). This approach is in DDListboxColumn.c.cs.

Button.OnClick() controls the listbox dropdown:

public class DatagridDDButton : System.Windows.Forms.Button
{
    override protected void OnClick( System.EventArgs e )
    {
        // Control the listbox dropdown
        Helper.Trace( "Button.OnClick()" );
        // call overridden base class method
        base.OnClick( e );
        // hide or show the listbox
        if ( this.column.Listbox.Visible ) this.column.EditAction = 2; // hide
        else this.column.EditAction = 1; // show
        // start a new cell edit procedure
        // maybe you can omitt the next line...
        this.column.DataGridTableStyle.DataGrid.EndEdit( this.column, this.column.CurrentRow, false );
        this.column.DataGridTableStyle.DataGrid.BeginEdit( this.column, this.column.CurrentRow );
    }
}

Positioning and showing of the hosted controls is now the responsibility of the column.Edit() method. The property EditAction determines what to do:

  1. Initialize for cell editing (show textbox and button)
  2. Dropdown the listbox (show textbox, button and listbox)
  3. Hide the listbox (show only textbox and button)

0 is the standard value when the cell is about to be edit. In this case the textbox text is initialized from the data source, and textbox as well as the button are moved to the current cell and made visible. The Textbox.Changed flag guards textbox initialization for the case that the user enters some text in the add-new-row of the grid. The grid then calls ConcedeFocus(), followed by another Edit() for the current cell. Textbox.Changed prevents the first character from being overwritten:

public class DatagridComboboxColumn : DataGridColumnStyle
{
    public int EditAction = 0;
        // Determines the visibility of the listbox in Edit()

    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 );
        Helper.Trace( "      ", "Textbox.Text=" + this.Textbox.Text, "Textbox.Changed=" + Textbox.Changed, "EditAction=" + this.EditAction );
        switch ( this.EditAction )
        {
            case 0:
                // start cell editing: initialize and show textbox and button
                if ( ! this.Textbox.Changed )
                {
                    // store the current row
                    this.CurrentRow = rowNum;
                    // get the current cell value from the data source
                    object value = this.GetColumnValueAtRow( source, rowNum );
                    if ( value == null ) this.Textbox.Text = this.NullText;
                    else this.Textbox.Text = value.ToString();
                    // show the textbox in the boundaries of the current cell
                    this.Textbox.Bounds = bounds;
                    this.Textbox.Width -= this.Button.Width;
                    // show the button
                    this.Button.Top = this.Textbox.Top;
                    this.Button.Left = this.Textbox.Left + this.Textbox.Width;
                }
                this.Button.Show();
                this.Textbox.Show();
                this.Textbox.Focus();
                break;

            case 1:
                // dropdown listbox, no initialization from datasource
                // position listbox below the current cell
                this.Listbox.Left = bounds.Left;
                this.Listbox.Top = bounds.Top + bounds.Height;
                this.Listbox.Width = bounds.Width;
                // move the listbox above the cell if there is not enough space
                if ( this.Listbox.Top + this.Listbox.Height > this.DataGridTableStyle.DataGrid.Height )
                {
                    this.Listbox.Top = bounds.Top - this.Listbox.Height;
                }
                // select the appropriate listbox item
                this.Listbox.SelectedIndex = -1;
                this.Listbox.Text = this.Textbox.Text;
                // show the listbox
                this.Listbox.Show();
                this.Listbox.Focus();
                break;

            case 2:     // hide the listbox again, show textbox and button
                this.Listbox.Hide();
                this.Button.Show();
                this.Textbox.Show();
                this.Textbox.Focus();
                break;
        }

        // reset helpers
        this.EditAction = 0;
        this.Textbox.Changed = false;
    }
}

The case 1 comes into action if the user clicks the cell button to drop down the listbox, and case 2 if the user clicks again to hide the listbox.

46:05.297 Edit() 2 instantText= CellIsVisible=True
46:05.297        Textbox.Text= Textbox.Changed=False EditAction=0
46:08.441 Button.OnClick() 
46:08.441 Commit() rowNum=2 EditAction=1
46:08.441 Edit() 2 instantText= CellIsVisible=True
46:08.441        Textbox.Text=México D.F. Textbox.Changed=False EditAction=1
46:11.606 Button.OnClick() 
46:11.606 Commit() rowNum=2 EditAction=2
46:11.606 Edit() 2 instantText= CellIsVisible=True
46:11.606        Textbox.Text=México D.F. Textbox.Changed=False EditAction=2
46:13.899 Commit() rowNum=2 EditAction=0
46:13.909 Commit() rowNum=2 EditAction=0

The trace log above illustrates the procedure: At 46:05 I clicked on the City cell in row 2. EditAction is 0. Then I clicked on the cell button. Commit and Edit at 46:08 reflect the EndEdit()/BeginEdit() calls in Button.OnClick(), with EditAction=1. At 46:11 I clicked again to hide the listbox, which again causes the grid to call Commit() and Edit(), this time with EditAction=2. Finally, at 46:13 I clicked in a different column, which ends the cell edit procedure.

The fact that Commit() is called suggests a change to this method: The current value is not saved to the data source if the listbox is about to be dropped down or hidden. In these cases, the hosted controls are not hidden to minimize screen flicker.

public class DatagridComboboxColumn : DataGridColumnStyle
{
    protected override bool Commit( CurrencyManager source, int rowNum )
    {
        // Save current value to the data source
        Helper.Trace( "Commit()", "rowNum=" + rowNum, "EditAction=" + this.EditAction );
        // prevent updating wrong rows
        if ( this.Textbox.Visible && this.EditAction == 0 )
        {
            // update the data source
            this.SetColumnValueAtRow( source, rowNum, this.Textbox.Text );
            // hide the hosted controls
            this.Textbox.Hide();
            this.Button.Hide();
            this.Listbox.Hide();
            this.Textbox.Changed = false;
        }
        return true;
    }
}

ConcedeFocus() now unconditionally hides the hosted controls:

public class DatagridComboboxColumn : DataGridColumnStyle
{
    protected override void ConcedeFocus()
    {
        // Hide hosted controls
        Helper.Trace( "ConcedeFocus()" );
        this.Textbox.Hide();
        this.Button.Hide();
        this.Listbox.Hide();
    }
}

You can find the code above in DDListboxColumn.c.cs.

Cell Updates

Things might complicate: We have not yet called ColumnStartedEditing() in the TextChanged event of the textbox. I have implemented this in DDListboxColumn.d.cs:

public class DatagridDDTextbox : System.Windows.Forms.TextBox
{
    override protected void OnTextChanged( System.EventArgs e )
    {
        Helper.Trace( "Textbox.OnTextChange()" );
        // notifiy the grid to show the pen icon
        if ( this.Visible ) ((IDataGridColumnStyleEditingNotificationService)this.column)
        	.ColumnStartedEditing( this );
        base.OnTextChanged( e );
    }
}

Another way to change the textbox is to accept an entry from the listbox with Enter:

public class DatagridDDListbox : System.Windows.Forms.ListBox
{
    protected override bool ProcessDialogKey( Keys keydata )
    {
        Helper.Trace( "ProccessDialogKey()", keydata );
        switch ( keydata )
        {
            case Keys.Enter:
                // accept the selected listbox entry
                // hide the listbox: End current cell edits and start a new edit
                this.column.EditAction = 2;
                this.column.DataGridTableStyle.DataGrid.EndEdit( this.column, this.column.CurrentRow, false );
                this.column.DataGridTableStyle.DataGrid.BeginEdit( this.column, this.column.CurrentRow );
                // copy the entry to textbox
                this.column.Textbox.Text = this.Text;
                return true;

            default:
                break;
        }
        return base.ProcessDialogKey( keydata );
    }
}

Works for existing rows. Visibility of the listbox has no negative influence. Good. Checks in the add-new-row: Works, but the first character is lost. I used the standard recipe that already worked for the textbox: In Textbox.OnTextChanged() the flag Textbox.Changed is set:

public class DatagridDDTextbox : System.Windows.Forms.TextBox
{
    override protected void OnTextChanged( System.EventArgs e )
    {
        if ( this.Visible ) Helper.Trace( "Textbox.OnTextChange()" );
        if ( this.Visible )
        {
            // flag Edit() that the cell has been changed
            this.Changed = true;
            // notifiy the grid to show the pen icon
            ((IDataGridColumnStyleEditingNotificationService)this.column).ColumnStartedEditing( this );
        }
        base.OnTextChanged( e );
    }
}

The Changed flag is used in Edit() to prevent overwriting the user input:

public class DatagridComboboxColumn : DataGridColumnStyle
{
    protected override void Edit( CurrencyManager source, int rowNum, Rectangle bounds, bool readOnly, string instantText, bool cellIsVisible )
    {
        switch ( this.EditAction )
        {
            case 0:     // start cell editing: initialize and show textbox and button
                if ( ! this.Textbox.Changed )
                {
                    // store the current row
                    this.CurrentRow = rowNum;
                    // get the current cell value from the data source
                    object value = this.GetColumnValueAtRow( source, rowNum );
                    if ( value == null ) this.Textbox.Text = this.NullText;
                    else this.Textbox.Text = value.ToString();
                    // show the textbox in the boundaries of the current cell
                    this.Textbox.Bounds = bounds;
                    this.Textbox.Width -= this.Button.Width;
                    // show the button
                    this.Button.Top = this.Textbox.Top;
                    this.Button.Left = this.Textbox.Left + this.Textbox.Width;
                }
                this.Button.Show();
                this.Textbox.Show();
                this.Textbox.Focus();
                break;
                ...
    }
}

The code above is in DDddListboxColumn.d.cs.

Version 5: Glue Logic Implementation

Now that controlling the listbox dropdown and changing the cell value is solved for existing and new rows, it's time to implement the glue logic that makes up a combobox from the three separate controls. I have put the code in DDListboxColumn.e.cs.

Column: Operation Modes

In this version, a mode property has been added to the columnstyle implementation:

  1. Dropdown listbox, allows to enter values that are in the list only.
  2. Combobox, allows to pick values from the list, as well as entering arbitrary values in the textbox.

Note: You can toggle the mode with the button above the grid.

The mode property is implemented as int, I rather should have implemented an enumeration for this purpose. Maybe in the next revision.

Listbox Lookup

To make up a real combobox, the textbox must synchronize the listbox when the user enters text in the textbox. Textbox changes cause a listbox lookup, if the textbox is visible and focused. The listbox.FindString() method looks in the listbox items for a matching entry. If there is one, it is copied to the textbox. If not, then the previous value is restored (mode 0 (dropdown list mode) will only allow values contained in the list). This will raise the change event for the textbox again. To prevent executing the change logic in this case, the guard flag is set before the textbox.Text assignment:

public class DatagridDDTextbox : System.Windows.Forms.TextBox
{
    protected bool guardChange = false;
        // Used in OnTextChange() in mode 0 to restore the
        // caret position if a listbox lookup fails 

    protected string textBeforeChange;
        // helps in OnTextChange() in mode 0 to restore the
        // last valid value if a listbox lookup fails.

    protected int caretPosBeforeChange;
        // Used in OnTextChange() in mode 0 to restore the
        // caret position if a listbox lookup fails 

    override protected void OnTextChanged( System.EventArgs e )
    {
        if ( this.guardChange ) return;
        if (  this.Visible )
        {
            ...
            if ( this.Focused )
            {
                // listbox lookup
                this.column.Listbox.SelectedIndex = this.column.Listbox.FindString( this.Text );
                this.guardChange = true;
                if ( this.column.Listbox.SelectedIndex >= 0 )
                {
                    // accept and backup the current change
                    this.caretPosBeforeChange = this.SelectionStart;
                    this.Text = this.textBeforeChange = this.column.Listbox.Text;
	                // select text completed from the listbox lookup
	                this.SelectionStart = this.Text.Length;
	                SendKeys.Send("+{LEFT " + (this.Text.Length - this.caretPosBeforeChange).ToString() + "}" );
                }
                else if ( this.column.Mode == 0 ) // lookup failed
                {
                    // reject current change, revert to saved text
                    this.Text = this.textBeforeChange;
	                // select text completed from the listbox lookup
	                this.SelectionStart = this.Text.Length;
	                SendKeys.Send("+{LEFT " + (this.Text.Length - this.caretPosBeforeChange).ToString() + "}" );
                }
                this.guardChange = false;
            }
        }
    }
}

In mode 1, (incremental) combobox mode, if the listbox lookup failed, the textbox replaces the selected text with the input character. The character is not removed, but no text selections in this case.

If the entry has been accepted, a copy is stored in the textBeforeChange member. Additionally, the current position of the caret is backed up in caretPosBeforeChange. These will help to restore the textbox, if the next entry does not match a listbox entry.

To give some feedback about what the user has entered and what has been added by the listbox lookup, the caret is placed behind the last character entered by the user, and the rest is highlighted. Well, text selection with the caret on the left hand side of the selected text is a little bit tricky. Thanks to Herfried K. Wagner, who provided me with the trick in the newsgroups, this has been solved. If you have another solution, I'd be happy if you drop me a line!

Control Keys

The textbox processes some keys in ProcessCmdKey() to control the listbox. The Up/Down keys select an entry from the listbox and copy the selected entry into the textbox. I switched the change guard flag on to inhibit the listbox lookup in textbox.OnTextChanged():

public class DatagridDDTextbox : System.Windows.Forms.TextBox
{
    protected override bool ProcessCmdKey( ref System.Windows.Forms.Message msg, System.Windows.Forms.Keys keydata )
    {
        Helper.Trace( "ProccessCmdKey()", keydata );
        switch ( keydata )
        {
            case Keys.Down:
            case Keys.Up:
                // Pick a value from the listbox
                if ( ! this.column.Listbox.Visible ) break;
                // prev/next listbox entry
                if ( keydata == Keys.Up && this.column.Listbox.SelectedIndex > 0 )
                {
                    this.column.Listbox.SelectedIndex -= 1;
                }
                else if ( keydata == Keys.Down && this.column.Listbox.SelectedIndex < this.column.Listbox.Items.Count - 1 )
                {
                    this.column.Listbox.SelectedIndex += 1;
                }
                // copy listbox entry into textbox
                this.guardChange = true;
                this.Text = this.column.Listbox.Text;
                this.guardChange = false;
                this.SelectAll();
                return true;

Caret Control Mode

Next, F2 and Enter toggle the caret control mode. Basically, if the whole text is selected, then the cursor keys are passed to the grid for cell navigation. If not, the textbox controls the cursor keys for caret positioning. If the listbox is visible, Enter or F2 hide the listbox by restarting the edit procedure on the current cell:

            case Keys.F2:
            case Keys.Enter:
                // Manage caret control mode
                if ( this.column.Listbox.Visible )
                {
                    // hide listbox
                    this.column.EditAction = 2;
                    // start a new cell edit procedure
                    this.column.DataGridTableStyle.DataGrid.EndEdit( this.column, this.column.CurrentRow, false );
                    this.column.DataGridTableStyle.DataGrid.BeginEdit( this.column, this.column.CurrentRow );
                }
                else // no listbox, toggle caret control mode
                {
                    if ( this.SelectionLength == this.Text.Length )
                    {
                        // enable caret control with shift/ctrl left/right
                        this.SelectionLength = 0;
                        this.SelectionStart = this.Text.Length;
                    }
                    else // disable caret control
                    {
                        this.SelectAll();
                    }
                }
                return true;

Delete and Backspace

The Backspace also needs some extra care. In contrast to a standard textbox, it is not sufficient to delete the selected text on backspace. The following execution of listbox lookup code in OnTextChanged() would restore the deleted text immediately, making the Backspace key disfunctional. Therefor I interfere the Backspace key processing, delete the selected text with the guard flag on, and then pass the control back to the base class method, which will then remove the rightmost character from the text. But this time the guard flag is off, so the listbox lookup code in OnTextChanged() will take its action:

            case Keys.Back:
                // remove the selected text to enable deletion of the
                // character left to the selected block
                if ( this.SelectionLength < this.TextLength )
                {
                    int pos = this.SelectionStart;
                    this.guardChange = true;
                    this.Text = this.Text.Remove( this.SelectionStart, this.SelectionLength );
                    this.guardChange = false;
                    this.SelectionStart = pos;
                }
                // base class code deletes the rightmost character from the text
                break;

Very similar, in dropdown listbox mode 0, the Delete key does not make sense. The deleted text would also be restored by the OnTextChanged() listbox lookup code.

            case Keys.Delete:
                // prevent action in dropdown list mode
                if ( this.column.Mode == 1 )
                {
                    // remove selected text
                    int pos = this.SelectionStart;
                    this.guardChange = true;
                    this.Text = this.Text.Remove( this.SelectionStart, this.SelectionLength );
                    this.guardChange = false;
                    this.SelectionStart = pos;
                }
                // indicate processing done
                return true;
        }
        return base.ProcessCmdKey( ref msg, keydata );
    }
}

Listbox Dropdown

Next, we control the listbox dropdown with Alt-Down from the textbox. The code is very similar to the Button.OnClick() method, so it might be a good idea to centralize this code:

public class DatagridDDTextbox : System.Windows.Forms.TextBox
{
    protected override bool ProcessDialogKey( Keys keydata )
    {
        Helper.Trace( "ProccessDialogKey()", keydata );
        switch ( keydata )
        {
            case Keys.Down | Keys.Alt:
                // Listbox dropdown
                if ( this.column.Listbox.Visible ) this.column.EditAction = 2; // hide
                else this.column.EditAction = 1; // show
                // restart the cell edit procedure
                this.column.DataGridTableStyle.DataGrid.EndEdit( this.column, this.column.CurrentRow, false );
                this.column.DataGridTableStyle.DataGrid.BeginEdit( this.column, this.column.CurrentRow );
                return true;

            default:
                break;
        }
        return base.ProcessDialogKey( keydata );
    }
}

Cell Navigation Issues

The previously mentioned Tab key problem is solved in the ProcessKeyMessage() method. A return true; prevents passing the key message to the grid.

Left/Right keys control the caret, if not the entire text is selected. The return false; statment causes the textbox to perform its default action on these keys.

public class DatagridDDTextbox : System.Windows.Forms.TextBox
{
    protected override bool ProcessKeyMessage( ref System.Windows.Forms.Message msg )
    {
        // Controls processing of Tab and cursor keys
        Helper.Trace( "ProccessKeyMessage()", (Keys)(int)(msg.WParam) );
        switch ( (Keys)(int)(msg.WParam) )
        {
            case Keys.Tab:
                // prevent (shift) tab actions for the control
                return true;

            case Keys.Right:
            case Keys.Left:
                // if the textbox controls the caret, prevent the grid from
                // processing these keys
                if ( this.SelectionLength != this.Text.Length ) return false;
                break;

            default:
                break;
        }
        return base.ProcessKeyMessage( ref msg );
    }
}

Listbox Obligations

In this implementation the entire key handling has moved to the textbox, so there is nothing more to do for the listbox than handling a click event. A click simply copies the selected entry into the textbox and hides the listbox:

public class DatagridDDListbox : System.Windows.Forms.ListBox
{
    protected override void OnClick( System.EventArgs e )
    {
        base.OnClick( e );
        // copy the listbox entry to the textbox
        this.column.Textbox.Text = this.Text;
        // hide the listbox and restart the edit procedure
        this.column.EditAction = 2;
        this.column.DataGridTableStyle.DataGrid.EndEdit( this.column, this.column.CurrentRow, false );
        this.column.DataGridTableStyle.DataGrid.BeginEdit( this.column, this.column.CurrentRow );
    }
}

The code above is in DDListboxColumn.e.cs, and runs quite well, IMHO.

Conclusion

With a lot of glue code, I have managed to implement a usable combobox column in the end. Maybe there is a much simpler solution. If you found one, I'd be happy to hear about.

The current version of the combobox column is not databound. This will be the topic for the next article.

Feedback

If you have any question or would like to add a comment, please mail to ulrich.devcom@synesis.com.

History

2003-09-12 Initial publication