Overview
One of the most missing features in the .NET / Windows Form
programming is, that you cannot put a databound Combobox in a column of a Datagrid.
And that too if you’re trying to get it programmed in a Windows Forms world its
a lot of work to do. You got to write a whole lot of code for a simple extensibility
of the existing features of datagrid.
To use a databound combobox, you have to add overrides for
SetColumnValueAtRow and GetColumnValueAtRow to switch the
DisplayMember and ValueMember as you get and set the data from the
underlying table. Also, you cannot have the ComboBox bound with the same
BindingContext to the same datasource as the datagrid.
Example
To use databound comboboxes in the datagrid the sample derives
a new datagrid column style from the
DataGridTextBoxColumn class and adds overrides for
the SetColumnValueAtRow, GetColumnValueAtRow, Edit
and Commit methods. This is performed in the DataGridComboBoxColumn class.
The Edit event is raised when the user sets the focus to
the cell containing the combobox. In this event the
dimensions of the combobox are set and an event handler is
assigned to handle scrolling of the combobox. When a value
is changed in the combobox and you leave the grid cell the
new value must update the corresponding value in the bound data source. This requires tracking when the user starts to edit the
combobox value and then commiting the changes to the data
source. Hence the override to the Edit and Commit
events.
A second NoKeyUpCombo class derives a new ComboBox whose
WndProc method is overridden. This is required to
address issues when TAB-bing through the grid so as not to
leave focus on the combobox. It is this derived combobox
that is added to the datagrid.
DataGridTextBoxColumn Class
The Class derives a new datagrid column style from the
DataGridTextBoxColumn class that:
-
Adds a ComboBox member.
-
Tracks when the combobox has focus in the Enter and Leave
events.
-
Overrides Edit event so the ComboBox replaces the
TextBox
-
Overrides Commit event to save the changed data
-
Overridest
the SetColumnValueAtRow and
GetColumnValueAtRow method
to switch the DisplayMember and
ValueMember as you get and set the data from the underlying table.
SetColumnValueAtRow
The SetColumnValueAtRow
method updates the bound DataTable "Titles" with the
ValueMember
for a given DisplayMember = myComboBox.Text from the Combobox.
GetColumnValueAtRow
The GetColumnValueAtRow method updates the bound
Combobox with the DisplayMember
for a given Row Number = rowNum from the DataTable "Titles".
Sample Code
myComboBox.cs
// The NoKeyUpCombo class derives a new ComboBox whose
WndProc
// method is overridden. This is required to address issues when
// It is this derived combobox that is added to the datagrid.
// When you TAB trough the DataGrid and you reach the DropDown Combobox
// the Focus is immediately moved to the next Column.
using System;
namespace Akadia.DataGridBoundCombo
{
public class myComboBox: System.Windows.Forms.ComboBox
{
private const int WM_KEYUP = 0x101;
// The WndProc
method corresponds exactly to the Windows WindowProc function.
// For more information about processing
Windows messages, see the WindowProc
// function documentation in the Windows
Platform SDK reference located in
// the MSDN Library.
protected override void WndProc(ref
System.Windows.Forms.Message theMessage)
{
// Ignore KeyUp
event to avoid problem with tabbing the dropdown.
if
(theMessage.Msg == WM_KEYUP)
{
return;
}
else
{
base.WndProc(ref theMessage);
}
}
}
}
DataGridComboBoxColumn.cs
using System;
using System.Data;
using System.Drawing;
using System.Windows.Forms;
namespace Akadia.DataGridBoundCombo
{
public class DataGridComboBoxColumn :
DataGridTextBoxColumn
{
public myComboBox myComboBox;
private
System.Windows.Forms.CurrencyManager _currencyManager;
private int _rowNum;
private bool _Editing;
// Constructor,
create our own customized Combobox
public DataGridComboBoxColumn()
{
_currencyManager
= null;
_Editing =
false;
// Create our own customized Combobox, which is used in the DataGrid
// DropDownList:
The user cannot directly edit the text portion.
//
The user must click the arrow button to display the
//
list portion.
// DropDown: The text portion is editable. The user must
click
//
the arrow button to display the list portion.
// Simple: The text portion is editable. The list
portion is
//
always visible.
myComboBox = new
myComboBox();
myComboBox.DropDownStyle = ComboBoxStyle.DropDownList;
// My own Combobox subscribes to the Leave Event. It occurs when the
// input focus
leaves the ComboBox.
this.myComboBox.Leave +=
new System.EventHandler(LeaveComboBox);
// My own Combobox subscribes to the SelectionChangeCommitted Event.
// It occurs when
the selected item has changed and that change
// is committed
(save the changed data to the DataGrid TextBox).
this.myComboBox.SelectionChangeCommitted +=
new System.EventHandler(SelectionChangeCommit);
}
// Make current
Combobox invisible when user scrolls
// the DataGrid control using the
ScrollBar.
private void HandleScroll(Object
sender, EventArgs e)
{
if
(myComboBox.Visible)
{
myComboBox.Hide();
}
}
// The
ColumnStartedEditing method allows the DataGrid
// to show a pencil in the row header
indicating the row
// is being edited. (base is the parent
DataGridTextBoxColumn)
private void
SelectionChangeCommit(Object sender, EventArgs e)
{
_Editing =
true;
base.ColumnStartedEditing((System.Windows.Forms.Control) sender);
}
// Handle Combobox
Behaviour when Focus leaves the Combobox.
private void LeaveComboBox(Object
sender, EventArgs e)
{
if (_Editing)
{
// Set the Combobox ValueMember to the current RowColumn
// when the Focus leaves the Combobox.
SetColumnValueAtRow(_currencyManager, _rowNum, myComboBox.Text);
_Editing = false;
// Redraws the column
Invalidate();
}
// Hide the current Combobox when Focus on Combobox is
loosen
myComboBox.Hide();
// Let current Combobox visible when user scrolls
// the DataGrid
control using the ScrollBar.
this.DataGridTableStyle.DataGrid.Scroll += new System.EventHandler(HandleScroll);
}
// The
SetColumnValueAtRow method updates the bound
// DataTable "Titles" with the
ValueMember
// for a given DisplayMember =
myComboBox.Text from the Combobox.
protected override void
SetColumnValueAtRow
(CurrencyManager source, int rowNum, Object value)
{
Object tbDisplay
= value;
DataView dv =
(DataView)this.myComboBox.DataSource;
int rowCount =
dv.Count;
int i =
0;
Object
cbDisplay;
Object
cbValue;
// Loop through the Combobox DisplayMember values
// until you find
the selected value, then read the
// ValueMember
from the Combobox and update it in the
// DataTable
"Titles"
while (i <
rowCount)
{
cbDisplay = dv[i][this.myComboBox.DisplayMember];
if ((cbDisplay != DBNull.Value) &&
(tbDisplay.Equals(cbDisplay)))
{
break;
}
i += 1;
}
if (i <
rowCount)
{
cbValue = dv[i][this.myComboBox.ValueMember];
}
else
{
cbValue = DBNull.Value;
}
base.SetColumnValueAtRow(source, rowNum, cbValue);
}
// The
GetColumnValueAtRow method updates the bound
// Combobox with the DisplayMember
// for a given Row Number = rowNum from
the DataTable "Titles".
protected override Object
GetColumnValueAtRow
(CurrencyManager source, int rowNum)
{
// Get the
ValueMember from the DataTable "Titles"
Object tbValue =
base.GetColumnValueAtRow(source, rowNum);
// Associate a DataView to the Combox, so we can search
// the
DisplayMember in the Combox corresponding to the
// ValueMember
from the DataTable "Titles"
DataView dv =
(DataView)this.myComboBox.DataSource;
int rowCount =
dv.Count;
int i = 0;
Object
cbValue;
// Loop through the Combox Entries and search the
DisplayMember
while (i <
rowCount)
{
cbValue = dv[i][this.myComboBox.ValueMember];
if ((cbValue != DBNull.Value) &&
(tbValue != DBNull.Value) &&
(tbValue.Equals(cbValue)))
{
break; // We found the DisplayMember - exit the Loop
}
i += 1;
}
// If we are within the Combox Entries, return now the
DisplayMember
// for the found
ValueMember above. If we are at the End of the Combox
// Entries,
return NULL
if (i <
rowCount)
{
return dv[i][this.myComboBox.DisplayMember];
}
else
{
return DBNull.Value;
}
}
// The Edit event
is raised when the user sets the focus to the cell
// containing the combobox. In this event
the dimensions of the combobox
// are set and an event handler is
assigned to handle scrolling of the combobox.
protected override void Edit(
CurrencyManager
source,
int rowNum,
Rectangle
bounds,
bool
readOnly,
string
instantText,
bool
cellIsVisible)
{
base.Edit(source,
rowNum, bounds, readOnly, instantText, cellIsVisible);
// Set current Rownum and Postion Manager
_rowNum =
rowNum;
_currencyManager
= source;
// Calculate Location of the Combox relative to the TextBox
// of the
DataGrid which have the Focus
Point NewLoc;
NewLoc =
this.TextBox.Location;
NewLoc.X -=
3;
NewLoc.Y -=
3;
myComboBox.Location = NewLoc;
// Attach the Combobox to the same Parent Control
// as the TextBox
of the DataGrid
myComboBox.Parent
= this.TextBox.Parent;
// Position the Combox at the same Location as the TextBox
myComboBox.Size =
new Size(this.TextBox.Size.Width + 3, myComboBox.Size.Height);
// Select the Entry in the Combobox corresponding to the Text in
// in the
TextBox.
myComboBox.SelectedIndex = myComboBox.FindStringExact(this.TextBox.Text);
//
myComboBox.Text = this.TextBox.Text;
// Make the TextBox invisible and then show the Combobox
this.TextBox.Visible = false;
myComboBox.Visible = true;
myComboBox.BringToFront();
myComboBox.Focus();
// Make Combobox invisible id User scrolls uo or down the
DataGrid
this.DataGridTableStyle.DataGrid.Scroll += new System.EventHandler(HandleScroll);
}
// The Commit
method can be used to put the Combomox ValueMember
// into the TextBox ValueMember. This can
be handled in the
// LeaveComboBox EventHandler as
well.
protected override bool
Commit(
System.Windows.Forms.CurrencyManager dataSource,int rowNum)
{
if (_Editing)
{
_Editing = false;
SetColumnValueAtRow(dataSource, rowNum, myComboBox.Text);
}
return true;
}
}
}
Testapplication
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.Data.SqlClient;
namespace Akadia.DataGridBoundCombo
{
public class DataGridBoundCombo :
System.Windows.Forms.Form
{
private DataSet _DataSet;
private SqlConnection _Conn;
private SqlDataAdapter
_DataAdapterTitles;
private SqlDataAdapter
_DataAdapterPublishers;
private System.Windows.Forms.DataGrid
_DataGrid;
private CurrencyManager
_currencyManager;
private System.Windows.Forms.Button
btnUpdate;
private System.Windows.Forms.StatusBar
statusBar1;
private System.Windows.Forms.Button
btnDelete;
private System.Windows.Forms.Button
btnNew;
private System.Windows.Forms.Button
btnCancel;
private System.ComponentModel.Container
components = null;
public DataGridBoundCombo()
{
InitializeComponent();
}
protected override void Dispose( bool
disposing )
{
if( disposing
)
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose(
disposing );
}
.....
.....
[STAThread]
static void Main()
{
Application.Run(new DataGridBoundCombo());
}
//
Initializing
private void
DataGridBoundCombo_Load(object sender, System.EventArgs e)
{
_DataGrid.AllowSorting = false;
GetData();
}
// Load Data from
the Database
private void GetData()
{
string strConn =
"data source=xeon;uid=sa;password=manager;database=pubs";
try
{
_Conn = new SqlConnection(strConn);
// Fill DataSet
string strSQL = "SELECT title, title_id, pub_id, price FROM Titles ORDER BY
title";
_DataSet = new DataSet();
_DataAdapterTitles = new SqlDataAdapter(strSQL, _Conn);
_DataAdapterTitles.Fill(_DataSet, "Titles");
// Create an explicit UPDATE command for the Titles
Table
_DataAdapterTitles.UpdateCommand = _Conn.CreateCommand();
_DataAdapterTitles.UpdateCommand.CommandText =
"UPDATE Titles SET "
+ "title = @u1, "
+ "pub_id = @u2, "
+ "price = @u3 "
+ "WHERE (title_id = @u4)";
// Add the parameters for UPDATE
SqlParameter u1 = new SqlParameter("@u1",SqlDbType.VarChar,80,"title");
u1.SourceVersion = DataRowVersion.Current;
_DataAdapterTitles.UpdateCommand.Parameters.Add(u1);
SqlParameter u2 = new SqlParameter("@u2",SqlDbType.Char,4,"pub_id");
u2.SourceVersion = DataRowVersion.Current;
_DataAdapterTitles.UpdateCommand.Parameters.Add(u2);
SqlParameter u3 = new SqlParameter("@u3",SqlDbType.Money,0,"price");
u3.SourceVersion = DataRowVersion.Current;
_DataAdapterTitles.UpdateCommand.Parameters.Add(u3);
SqlParameter u4 = new SqlParameter("@u4",SqlDbType.Char,6,"title_id");
u4.SourceVersion = DataRowVersion.Original;
_DataAdapterTitles.UpdateCommand.Parameters.Add(u4);
// Create an explicit INSERT command for the Titles
Table
_DataAdapterTitles.InsertCommand = _Conn.CreateCommand();
_DataAdapterTitles.InsertCommand.CommandText =
"INSERT INTO titles (title, title_id, pub_id, price) " +
"VALUES(@i1, @i2, @i3, @i4)";
// Add the parameters for INSERT
SqlParameter i1 = new SqlParameter("@i1",SqlDbType.VarChar,80,"title");
i1.SourceVersion = DataRowVersion.Current;
_DataAdapterTitles.InsertCommand.Parameters.Add(i1);
SqlParameter i2 = new SqlParameter("@i2",SqlDbType.Char,6,"title_id");
i2.SourceVersion = DataRowVersion.Current;
_DataAdapterTitles.InsertCommand.Parameters.Add(i2);
SqlParameter i3 = new SqlParameter("@i3",SqlDbType.Char,4,"pub_id");
i3.SourceVersion = DataRowVersion.Current;
_DataAdapterTitles.InsertCommand.Parameters.Add(i3);
SqlParameter i4 = new SqlParameter("@i4",SqlDbType.Money,0,"price");
i4.SourceVersion = DataRowVersion.Current;
_DataAdapterTitles.InsertCommand.Parameters.Add(i4);
// Create an explicit DELETE command for the Titles
Table
_DataAdapterTitles.DeleteCommand = _Conn.CreateCommand();
_DataAdapterTitles.DeleteCommand.CommandText =
"DELETE FROM titles " +
"WHERE title_id = @d1";
// Bind parameters to appropriate columns for DELETE
command
SqlParameter d1 = new SqlParameter("@d1",SqlDbType.Char,6,"title_id");
d1.SourceVersion = DataRowVersion.Original;
_DataAdapterTitles.DeleteCommand.Parameters.Add(d1);
// Fill a DataTable for the bound ComboBox
(publishers).
//
// NOTE: the ComboBox and grid are NOT bound to the same
// table. If bound to the same table, you must
use
// different binding contexts.
strSQL = "SELECT pub_name, pub_id FROM publishers ORDER BY pub_name";
_DataAdapterPublishers = new SqlDataAdapter(strSQL, _Conn);
_DataAdapterPublishers.Fill(_DataSet, "Publishers");
}
catch (Exception
ex)
{
string msg = ex.Message.ToString();
MessageBox.Show(msg, "Unable to retrieve data.",
MessageBoxButtons.OK, MessageBoxIcon.Error);
this.Close();
return;
}
// Create a TableStyle to format the datagrid's columns.
DataGridTableStyle tableStyle = new DataGridTableStyle();
DataTable
dataTable = _DataSet.Tables["Titles"];
tableStyle.MappingName = "Titles";
tableStyle.RowHeadersVisible = true;
tableStyle.RowHeaderWidth = 20;
// Customize Columns using own ColumnStyle
for (int i = 0; i
< dataTable.Columns.Count; i++)
{
switch (i)
{
case 0: // title - must correspond with "SELECT
title ...
DataGridTextBoxColumn column_0 = new DataGridTextBoxColumn();
column_0.MappingName = "title";
column_0.HeaderText = "Title";
column_0.Width = 200;
tableStyle.GridColumnStyles.Add(column_0);
break;
case 1: // title_id - must correspond with "SELECT
title ...
DataGridTextBoxColumn column_1 = new DataGridTextBoxColumn();
column_1.MappingName = "title_id";
column_1.HeaderText = "ID";
column_1.Width = 100;
tableStyle.GridColumnStyles.Add(column_1);
break;
case 2: // pub_id - must correspond with "SELECT title
...
// Use a combobox for the publisher.
DataGridComboBoxColumn column_2 = new DataGridComboBoxColumn();
column_2.MappingName = "pub_id";
column_2.HeaderText = "Publisher";
column_2.Width = 100;
column_2.myComboBox.DataSource = _DataSet.Tables["Publishers"].DefaultView;
column_2.myComboBox.DisplayMember = "pub_name";
column_2.myComboBox.ValueMember = "pub_id";
tableStyle.PreferredRowHeight = column_2.myComboBox.Height;
tableStyle.GridColumnStyles.Add(column_2);
break;
case 3: // price - must correspond with "SELECT title
...
// Use a PropertyDescriptor to format column Price.
PropertyDescriptorCollection pcol = this.BindingContext
[_DataSet,"Titles"].GetItemProperties();
DataGridTextBoxColumn column_3 =
new DataGridTextBoxColumn(pcol["price"], "c", true);
column_3.MappingName = "price";
column_3.HeaderText = "Price";
column_3.Width = 100;
tableStyle.GridColumnStyles.Add(column_3);
break;
default:
DataGridTextBoxColumn TextCol = new DataGridTextBoxColumn();
TextCol.MappingName = dataTable.Columns[i].ColumnName;
TextCol.HeaderText = TextCol.MappingName;
TextCol.Width = 100;
tableStyle.GridColumnStyles.Add(TextCol);
break;
}
}
// Add the custom Tablestyle to the Datagrid and bind the
// title Table
data to the datagrid.
_DataGrid.TableStyles.Clear();
_DataGrid.TableStyles.Add(tableStyle);
_DataGrid.DataSource = dataTable;
// No adding of new rows through Dataview
_currencyManager
= (CurrencyManager)this.BindingContext
[_DataGrid.DataSource, _DataGrid.DataMember];
//
((DataView)_currencyManager.List).AllowNew = false;
statusBar1.Text =
"Data loaded.";
}
// Save Data to the
Database - no Constrint Checking is done
// to keep code simple for thie
demo.
private void btnUpdate_Click(object
sender, System.EventArgs e)
{
// DEBUG: Print out "Titles" Table in the DataSet
// DataTable
aTable = _DataSet.Tables["Titles"];
//
foreach(DataRow aRow in aTable.Rows)
// {
//
Console.WriteLine(aRow["title_id"].ToString()
// + ": " +
oRow["pub_id"].ToString());
// }
// Pending Changes ?
if
(_DataSet.HasChanges())
{
// Update the database
try
{
_DataAdapterTitles.Update(_DataSet,"Titles");
_DataSet.AcceptChanges();
Application.DoEvents();
statusBar1.Text = "Transaction(s) successfully completed.";
}
catch (SqlException ex)
{
_DataSet.RejectChanges();
MessageBox.Show(ex.Message);
}
}
else
{
statusBar1.Text = "No pending Data to save.";
}
}
// Reset StatusBar
Message
private void
_DataGrid_CurrentCellChanged(object sender, System.EventArgs e)
{
statusBar1.Text =
"";
}
// Return the
selected Rows in an ArrayList
public ArrayList
GetSelectedRows(DataGrid dg)
{
ArrayList al =
new ArrayList();
DataView dv =
(DataView)_currencyManager.List;
for(int i = 0; i
< dv.Count; ++i)
{
if (dg.IsSelected(i))
{
al.Add(i);
}
}
return al;
}
// Delete selected
Rows from the Database, show simple Message.
// In a real Application the User shoul
be asked before the
// Delete Action is performed.
private void btnDelete_Click(object
sender, System.EventArgs e)
{
DataTable aTable
= _DataSet.Tables["Titles"];
string s =
"Deleted Rows: ";
if
(GetSelectedRows(_DataGrid).Count > 0)
{
foreach(object o in GetSelectedRows(_DataGrid))
{
DataRow aRow = aTable.Rows[(int)o];
aRow.Delete();
s += " " + o.ToString();
}
// Update the database
try
{
_DataAdapterTitles.Update(_DataSet,"Titles");
_DataSet.AcceptChanges();
Application.DoEvents();
statusBar1.Text = s;
}
catch (SqlException ex)
{
_DataSet.RejectChanges();
MessageBox.Show(ex.Message);
}
}
else
{
statusBar1.Text = "No Rows selected for Deletion.";
}
}
// Jump to the last + 1
Row and offer a new created Row
// to the User to enter a new
Title.
private void btnNew_Click(object
sender, System.EventArgs e)
{
//
_currencyManager.AddNew();
_DataGrid.CurrentCell = new DataGridCell(_currencyManager.Count,0);
statusBar1.Text =
"Please enter new Title.";
}
// Reload the
Data
private void btnCancel_Click(object
sender, System.EventArgs e)
{
if (this._Conn !=
null)
{
_Conn.Close();
}
this.GetData();
}
}
}
|