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();
}
}
}
|