Google
WWW Yariv Hammer's Code Site

Monday, November 28, 2005

DataGrid With Columns of Any Control You Like

Introduction
The DataGrid control in VS.NET 2003 only has two types of columns: TextBox and CheckBox.
It is often asked how can one put other controls inside the DataGrid (Like pictures, links, ComboBoxes, or even user controls). The answer is not very comforting. In order to achieve such an effect you will need to implement your own DataGridColumnStyle. This can be quite hard. In this article I will show how to implement a generic base class that can support a lot of controls. It will be demonstrated with a ComboBox column style. You will then be able to do modifications to the code to suit your needs.

Creating a DataGridGenericColumn
The DataGridGenericColumn will be the base class of all Column Styles.
---------------------------------------------------

public class DataGridGenericColumn:DataGridTextBoxColumn
{
private DataGridGenericColumn()
{}
public DataGridGenericColumn(Control control)
{
_control= control;
_control.Leave += new System.EventHandler(control_Leave);
_control.TextChanged += new EventHandler(control_TextChanged);
}
private Control _control;
private bool _isEditing = false;
protected Control Control
{
get { return _control; }
}
private void control_Leave(object sender, EventArgs e)
{
_control.Hide();
}
private void control_TextChanged(object sender, EventArgs e)
{
_isEditing = true;
base.ColumnStartedEditing((Control) sender);
}


protected override void Edit(CurrencyManager source, int rowNum, System.Drawing.Rectangle bounds, bool readOnly, string instantText, bool cellIsVisible)
{
base.Edit(source,rowNum,bounds,readOnly,instantText,cellIsVisible);
ShowControl();
}
protected virtual void ShowControl()
{
_control.Parent = this.TextBox.Parent;
_control.Location = this.TextBox.Location;
_control.Size = new Size(this.TextBox.Size.Width, this.TextBox.Size.Height);
_control.Text = this.TextBox.Text;
this.TextBox.Visible = false;
_control.Visible = true;
_control.BringToFront();
_control.Focus();
}
protected override bool Commit(CurrencyManager dataSource, int rowNum)
{
if (_isEditing)
{
_isEditing = false;
SetColumnValueAtRow(dataSource,rowNum,GetValue());
}
return true;
}
protected virtual object GetValue()
{
return _control.Text;
}
}
--------------------------------------------------
The base class is DataGridTextBoxColumn. It will be more convinient to inherit from this class than from DataGridColumnStyle, because a lot is already implemented for us. The base class has a TextBox property, which represents the DataGridTextBox that is shown in the cell when the user edit the cell.
In the constructor you pass a Control. Any Control will do. This Control is stored in a private member, and there is a protected member Control which may be accessed in the derived classes.
The Edit method is overrided in our class. The whole trick of showing our control instead of the TextBox is implemented in the ShowControl method. We set the location and size of our control to be the same as the TextBox, and then set the Visible property of our control to true, while setting the it to false form the TextBox.
Whenever the Text is changed inside the control, we remember this in _isEditing member. When the TextChanged event is raised, we know that we will need to store the new value into the DataSource. This is done in the Commit method, which we overrided from the base class. SetColumnValueAtRow method is used here in order to commit the changes into the DataSource. This method is implemented for us in the base class. The value is received from the virtual method GetValue. For our generic control we use the Text property, but this might not be the case for other controls.
Finally the Leave event of our control will be the trigger for making it invisible.
Note: You might want to make the control visible all the time (for example in case of pictures). It should be possible.

Creating a Column of ComboBoxes
Next, we need to derive from our new DataGridGenericColumn. We will implement a column of ComboBoxes.
-------------------------------------------------------

public class DataGridComboBoxColumn:DataGridGenericColumn
{
public DataGridComboBoxColumn():base(new DataGridComboBox())
{
}
public DataGridComboBox Combo
{
get { return (DataGridComboBox) Control; }
}
public class DataGridComboBox:ComboBox
{
private const int WM_KEYUP = 0x101;
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_KEYUP) return;
base.WndProc(ref m);
}
}
}
-------------------------------------------------------
The code is surprisingly very short - only a constructor that passes to the base class a new DataGridComboBox. Also there is a public property Combo to expose the control to the outside world. It is important as we will see in the demo soon.
The DataGridComboBox is implemented only because I wanted to fix a bug regarding the Tab key. IF you fill uncomfortable with it, then be my guest to use a normal ComboBox (It will work except for the small bug). I won't get into specific details here.

Usage of this control is as follows:
-------------------------------------------------------
DataGridComboBoxColumn cs4 = new DataGridComboBoxColumn();
cs4.MappingName = "ColumnName";
cs4.HeaderText = "Title";
cs4.Combo.Items.AddRange(new string[] {"a","b","c"});
-------------------------------------------------------
As long as the Items cllection has objects with ToString implemented for them, the combo will work great (for columns of type string of course).

Implementing a Data Bound ComboBox Column
Next, I will derive from our new DataGridComboBoxColumn and create a Data Bound ComboBox column. This means that you will be able to set a DataSource, Display and Value members for the ComboBox, and allow you to show a list from other tables (for example in a case of foreign key).
-------------------------------------------------------

public class DataGridDataBoundComboBoxColumn:DataGridComboBoxColumn
{
private DataGridDataBoundComboBoxColumn()
{
}
public DataGridDataBoundComboBoxColumn(object dataSource,string displayMember, string valueMember):base()
{
Combo.DataSource = dataSource;
Combo.ValueMember = valueMember;
Combo.DisplayMember = displayMember;
}
protected override object GetValue()
{
if (Combo.SelectedValue == null)
return DBNull.Value;
return Combo.SelectedValue;
}
protected override void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, Brush backBrush, Brush foreBrush, bool alignToRight)
{
string text = GetText(GetColumnValueAtRow(source, rowNum));
PaintText(g, bounds, text, backBrush, foreBrush, alignToRight);
}
protected virtual string GetText(object value)
{
if (value == DBNull.Value)
return NullText;
if (value != null)
{
DataRow[] dr = ((DataView)Combo.DataSource).Table.Select(Combo.ValueMember + " = " + value.ToString());
return dr[0][Combo.DisplayMember].ToString();
}
else
return "";
}
}
-------------------------------------------------------
The constructor receives a DataSource (DataView, DataTable, or any other legitimate data source), a DisplayMember (name of the column you want to display, like Name), and a ValueMember (name of the columns you want as value, it can be the same, or it can be the Id column).
The GetValue method we have implemented before was overriden, because we need the SelectedValue property (Text will not be enough).
We override the Paint method, because we want to show in the cell the Display text instead of the value. The GetText method is important to show the correct text in the cell when it is not edited. It will work if the data source of the ComboBox is a DataView or a DataTable (You may modify otherwise).

Demo of The Custom ColumnStyles
Start a new Project. Place the above code in the project (or reference to it).
Drag two DataGrids to the form (dataGrid1 above dataGrid2). Double click on it, and place the following code inside the form load event handler:
---------------------------------------------

private void Form1_Load(object sender, System.EventArgs e)
{
DataTable dtData = new DataTable("Data");
dtData.Columns.Add(new DataColumn("Id",typeof(long)));
dtData.Columns.Add(new DataColumn("Name",typeof(string)));
dtData.Columns.Add(new DataColumn("Foreign",typeof(long)));
dtData.Columns.Add(new DataColumn("List",typeof(string)));

DataTable dtConst = new DataTable("Constants");
dtConst.Columns.Add(new DataColumn("Value",typeof(long)));
dtConst.Columns.Add(new DataColumn("Text",typeof(string)));
dataGrid2.DataSource = new DataView(dtConst);

dataGrid1.DataSource = new DataView(dtData);
DataGridTableStyle ts = new DataGridTableStyle();
ts.MappingName = "Data";
DataGridTextBoxColumn cs1 = new DataGridTextBoxColumn();
cs1.MappingName = "Id";
cs1.HeaderText = "Id";

DataGridTextBoxColumn cs2 = new DataGridTextBoxColumn();
cs2.MappingName = "Name";
cs2.HeaderText = "Name";

DataGridDataBoundComboBoxColumn cs3 = new DataGridDataBoundComboBoxColumn(new DataView(dtConst),"Text","Value");
cs3.MappingName = "Foreign";
cs3.HeaderText = "Foreign";

DataGridComboBoxColumn cs4 = new DataGridComboBoxColumn();
cs4.MappingName = "List";
cs4.HeaderText = "List";
cs4.Combo.Items.AddRange(new string[] {"a","b","c"});

ts.GridColumnStyles.AddRange(new DataGridColumnStyle[] {cs1,cs2,cs3,cs4});
dataGrid1.TableStyles.Add(ts);
}
---------------------------------------------
Run the program. As you can see if you add data in the bottom DataGrid (rows with integer values and text fields), you will be able to select those when you click in cells in the Foreign column in the top grid. In the List column you can select one of the constant values.

1 Comments:

At 6:53 AM, February 06, 2006, Anonymous Anonymous said...

Hey, great work with this class. It's exactly what I needed.
One problem though, I am getting an error:

An unhandled exception of type 'System.StackOverflowException' occurred in system.windows.forms.dll

This happens when I run the form and I click on the DataGrids new line.
I hope you can help me!
Thanks

 

Post a Comment

<< Home

Feel free to use everything here. Add links to my site if you wish.

Do not copy anything to other sites without adding link to here.

All the contents of the site belong to Yariv Hammer.