Adding a custom control in Datagridview cell

Hi all,

I am a .NET developer and am facing a problem in Datagridview. To start with, I had a combobox as the column type in DataGridView. I was populating this combobox from a database table which had some aroudn 37,000 records.

With so many records getting populated in the combobox on the datagridview slowed the datagrid to an exruciating slow speed. To resolve this issue, I thought of writing a custom control which will have a text box and a drop down box next to it. The text box in the custom control will let the users type a search value for the description(display member) and the results will then populate in the dropdown next to it.

For e.g, if the users are searching for a company name (america), they will type america in the text box and the result will appear in the drop down next to the text box.

Finally when the users have selected one value from the drop-down box, the save button on the form will use the value selected in the drop-down box to save in the table in DB.

I did some research on adding a custom control, but there is very little documentaion for this and not many examples.

Please let me know if anyone has dealt with such a situation.

Thanks in advance !

Jax Developer

[1309 byte] By [jaxDeveloper] at [2007-12-24]
# 1

The datagridview faq and the white papers here are a good resource. I was thinking that maybe you can use a datagridview textbox column and set its autocomplete source. In the cell validating event check and see if user entered a valid entry

VB example



Imports System.Data
Imports System.Data.SqlClient
Public Class Form1
Dim scAutoComplete As New AutoCompleteStringCollection
Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim strConn As String
Dim da As SqlDataAdapter
Dim conn As SqlConnection
Dim ds As New DataSet
strConn = "Server = .;Database = NorthWind; Integrated Security = SSPI;"
conn = New SqlConnection(strConn)
da = New SqlDataAdapter("Select * from [Order Details]", conn)
da.Fill(ds, "Orders")
DataGridView1.DataSource = ds.Tables("Orders")
For x As Integer = 1 To 61000
scAutoComplete.Add(x.ToString)
Next
End Sub
Private Sub DataGridView1_CellValidating(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellValidatingEventArgs) Handles DataGridView1.CellValidating
If e.ColumnIndex = 1 Then
Debug.Print(e.FormattedValue.ToString)
If Not scAutoComplete.Contains(e.FormattedValue.ToString) Then
MessageBox.Show("Invalid Entry")
e.Cancel = True
End If
End If
End Sub
Private Sub DataGridView1_EditingControlShowing(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewEditingControlShowingEventArgs) Handles DataGridView1.EditingControlShowing
If DataGridView1.CurrentCell.ColumnIndex = 1 AndAlso TypeOf e.Control Is TextBox Then
With DirectCast(e.Control, TextBox)
.AutoCompleteMode = AutoCompleteMode.SuggestAppend
.AutoCompleteSource = AutoCompleteSource.CustomSource
.AutoCompleteCustomSource = scAutoComplete
End With
End If
End Sub
End Class

C# example



using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Data.SqlClient;
namespace CSAutoComplete
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
AutoCompleteStringCollection scAutoComplete = new AutoCompleteStringCollection();
private void Form1_Load(object sender, EventArgs e)
{
DataTable dt = new DataTable();
String strConn = "Server = .;Database = NorthWind; Integrated Security = SSPI;";
SqlConnection conn = new SqlConnection(strConn);
SqlDataAdapter da = new SqlDataAdapter("Select * from [Order Details]", conn);
da.Fill(dt);
dataGridView1.DataSource = dt;
for (int x = 1; x <= 61000; x++ )
{
scAutoComplete.Add(x.ToString());
}
}
private void dataGridView1_EditingControlShowing(object sender, DataGridViewEditingControlShowingEventArgs e)
{
if (dataGridView1.CurrentCellAddress.X == 1)
{
TextBox txt = e.Control as TextBox;
txt.AutoCompleteCustomSource = scAutoComplete;
txt.AutoCompleteMode = AutoCompleteMode.SuggestAppend;
txt.AutoCompleteSource = AutoCompleteSource.CustomSource;
}
}
private void dataGridView1_CellValidating(object sender, DataGridViewCellValidatingEventArgs e)
{
if(e.ColumnIndex==1)
{
if(!scAutoComplete.Contains(e.FormattedValue.ToString()))
{
MessageBox.Show("Invalid Data");
e.Cancel=true;
}
}
}
}
}

KenTucker at 2007-8-31 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...
# 2
Hi Ken

Firstly thanks for replying for my question. What you have told is a decent idea. But I really wanted an user control (a combination of text box + combo) as part of the datagridview cell. The eg of DatePicker in MSDN is helpful but I am struggling a bit when I am having an UserControl.

My UserControl basically is going to be a textbox + drop-down, where the user types in a value and the combo then gets filled with the result values.

I have got the new column type working down now to where I can see my user control in the datagridview where in the user can type the value in the text box of the user control and see the matching values in the combo of the user control. But, what I am not getting is the value doesn't get set in the cell when I move to the next row or do something else on the grid. I lose the value which I have searched in the text box and selected the desired value from the drop down.

Pasted is below my code...for User control. The stored proc spGetCompany is just retrieving the descriptions of companies which matches the value entered in the text box.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using FNF.Data;

namespace WindowsApplication1
{
public partial class UserControl1 : UserControl
{

public UserControl1()
{
InitializeComponent();
}

private string textBoxValue = string.Empty;
public string ComboValue
{
get
{
if (this.comboBox1.SelectedValue != null)
{
return this.comboBox1.SelectedValue.ToString();
}
else
{
return string.Empty;
}
}
}

public string TextBoxValue
{
get
{
return this.textBox1.Text.ToString();
}
set
{
textBox1.Text = value;
}
}

public ComboBox ComboControl
{
get
{
return comboBox1;
}
}

public TextBox TextBoxControl
{
get
{
return textBox1;
}
}


private void textBox1_TextChanged(object sender, EventArgs e)
{
if (textBox1.Text.Length > 0)
{
DataSet ds = GetSelectedCompanies(textBox1.Text);
if (ds != null)
{
if (ds.Tables[0].Rows.Count < 100)
{

comboBox1.DisplayMember = "name";
comboBox1.ValueMember = "agentno";
comboBox1.DataSource = ds.Tables[0];
}
else
{
return;
}
}
}
}

private DataSet GetSelectedCompanies(string companyDescription)
{
return DBClient.RunStoredProcDatasetWithParms("spGetCompany", "companyDesc", companyDescription);
}


private void comboBox1_SelectedValueChanged(object sender, System.EventArgs e)
{
textBox1.Text = comboBox1.Text;

if (comboBox1.Text.Length > 0)
{
int found = comboBox1.Text.ToString().IndexOf("[", 1);
if (found != -1)
{
textBox1.Text = comboBox1.Text.ToString().Substring(0, found-1);
}
}
}

protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
}

}
}

And here is the actual implementation of the customcell which has the type of UserControl in the datagridview

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;

namespace WindowsApplication1
{

public class CustomColumn : DataGridViewColumn
{
public CustomColumn(): base(new CustomControlCell())
{
}

public override DataGridViewCell CellTemplate
{
get
{
return base.CellTemplate;
}
set
{
// Ensure that the cell used for the template is a CalendarCell.
if (value != null &&
!value.GetType().IsAssignableFrom(typeof(CustomControlCell)))
{
throw new InvalidCastException("Must be a CustomControlCell");
}
base.CellTemplate = value;
}
}

private void InitializeComponent()
{


}
}

public class CustomControlCell : DataGridViewTextBoxCell
{

public CustomControlCell()
: base()
{

}

public override void InitializeEditingControl(int rowIndex, object
initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
{
// Set the value of the editing control to the current cell value.
base.InitializeEditingControl(rowIndex, initialFormattedValue,
dataGridViewCellStyle);
CustomEditingControl ctl = DataGridView.EditingControl as CustomEditingControl;

ctl.TextBoxControl.Text = this.Value.ToString();
//ctl.Value = (DateTime)this.Value;
}

public override Type EditType
{
get
{
// Return the type of the editing contol that CustomControlCell uses.
return typeof(CustomEditingControl);
}
}

public override Type ValueType
{
get
{
// Return the type of the value that CustomControlCell contains.
return typeof(String);
}
}

public override object DefaultNewRowValue
{
get
{
// Use the current date and time as the default value.
return string.Empty;
}
}
}

class CustomEditingControl : UserControl1, IDataGridViewEditingControl
{
DataGridView dataGridView;
private bool valueChanged = false;
int rowIndex;

public CustomEditingControl()
{

}

// Implements the IDataGridViewEditingControl.EditingControlFormattedValue
// property.
public object EditingControlFormattedValue
{
get
{
return this.TextBoxValue;
//return this.ComboValue;
//this.Value.ToShortDateString();
}
set
{
this.TextBoxValue = value.ToString();
//if (value is String)
//{
// this.Value = DateTime.Parse((String)value);
//}
}
}

// Implements the
// IDataGridViewEditingControl.GetEditingControlFormattedValue method.
public object GetEditingControlFormattedValue(
DataGridViewDataErrorContexts context)
{
return EditingControlFormattedValue;
}

// Implements the
// IDataGridViewEditingControl.ApplyCellStyleToEditingControl method.
public void ApplyCellStyleToEditingControl(
DataGridViewCellStyle dataGridViewCellStyle)
{
this.ComboControl.Font = dataGridViewCellStyle.Font;
this.ComboControl.ForeColor = dataGridViewCellStyle.ForeColor;
this.ComboControl.BackColor = dataGridViewCellStyle.BackColor;
}

// Implements the IDataGridViewEditingControl.EditingControlRowIndex
// property.
public int EditingControlRowIndex
{
get
{
return rowIndex;
}
set
{
rowIndex = value;
}
}

// Implements the IDataGridViewEditingControl.EditingControlWantsInputKey
// method.
public bool EditingControlWantsInputKey(Keys key, bool dataGridViewWantsInputKey)
{
// Let the DateTimePicker handle the keys listed.
switch (key & Keys.KeyCode)
{
case Keys.Left:
case Keys.Up:
case Keys.Down:
case Keys.Right:
case Keys.Home:
case Keys.End:
case Keys.PageDown:
case Keys.PageUp:
return true;
default:
return false;
}
}

// Implements the IDataGridViewEditingControl.PrepareEditingControlForEdit
// method.
public void PrepareEditingControlForEdit(bool selectAll)
{
// No preparation needs to be done.
}

// Implements the IDataGridViewEditingControl
// .RepositionEditingControlOnValueChange property.
public bool RepositionEditingControlOnValueChange
{
get
{
return false;
}
}

// Implements the IDataGridViewEditingControl
// .EditingControlDataGridView property.
public DataGridView EditingControlDataGridView
{
get
{
return dataGridView;
}
set
{
dataGridView = value;
}
}

// Implements the IDataGridViewEditingControl
// .EditingControlValueChanged property.
public bool EditingControlValueChanged
{
get
{
return valueChanged;
}
set
{
valueChanged = value;
}
}

// Implements the IDataGridViewEditingControl
// .EditingPanelCursor property.
public Cursor EditingPanelCursor
{
get
{
return base.Cursor;
}
}

protected override void OnTextChanged(EventArgs eventargs)
{
// notify the datagridview that the contents of the cell
// have changed.
valueChanged = true;
this.EditingControlDataGridView.NotifyCurrentCellDirty(true);
base.OnTextChanged(eventargs);
// base.OnValueChanged(eventargs);
}
}

public partial class Form2 : Form
{
private DataGridView dataGridView1 = new DataGridView();
public Form2()
{
InitializeComponent();
this.dataGridView1.Dock = DockStyle.Fill;
this.Controls.Add(this.dataGridView1);
this.Load += new EventHandler(Form2_Load);
this.Text = "DataGridView calendar column demo";
}

private void Form2_Load(object sender, System.EventArgs e)
{
//CalendarColumn col = new CalendarColumn();
CustomColumn col = new CustomColumn();
this.dataGridView1.Columns.Add(col);
this.dataGridView1.RowCount = 5;
foreach (DataGridViewRow row in this.dataGridView1.Rows)
{
row.Cells[0].Value = "Beavo, LLC";
}
}


}
}

jaxDeveloper at 2007-8-31 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...
# 3

In the user control try changing the name of the textboxvalue property to text.



public override string Text
{
get
{
return textBox1.Text ;
}
set
{
textBox1.Text = value;
}
}

KenTucker at 2007-8-31 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...
# 4
Hi Ken,

Thanks for your prompt reply again. I tried renaming the property value, it did not work. This has been really frustrating for me as there are not many examples in MSDN, which you could use.

I think the problem lies in this code with the grid not getting notified about the value being changed. This piece of code never gets the break-point for me.

class CustomEditingControl : UserControl1, IDataGridViewEditingControl
{
protected override void OnTextChanged(EventArgs eventargs)
{
// notify the datagridview that the contents of the cell
// have changed.
valueChanged = true;
this.EditingControlDataGridView.NotifyCurrentCellDirty(true);
base.OnTextChanged(eventargs);
}

}

In the date picker example which I am using from the MSDN, this piece of code is the OnValueChanged event of the datepicker being override. For my eg, I thought about exposing the OnTextChanged event and then letting the CustomEditingControl overriding that OnTextChangedEvent.

But I am not sure how do write an OnTextChanged event for my UserControl. Right now, I have the following code in my UserControl code

protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
}

Not sure if this is right. I honestly don't know much how to raise events in a custom user control.

Let me know if you can shed more light on this.

Anyway, I was also going back to your earlier advised solution of using the auto-complete. I used your example and it works good. There are couple of things I have observed though

1> When you type something in the cell and then select a desired value from the auto-complete source in the text box and move on to doing other things like moving to a different cell or not doing anything itself then you cannot edit the value(s) which you have selected earlier. The row just gets locked, none of the other columns you can edit. If this is resolved, then I think I am going to use this approach and convince my user and supervisor.

2> Also, my descriptions is going to contain duplicate values as it is not the primary key value. There can be similar descriptions of Companies for different Company IDs. The auto source do not show the duplicate values in that situation, I get to see only one value. For e.g if I type Microsoft, I will see only one Microsoft, though there are two companies named Microsoft.
In this situation, then I will have to show the Desription + ID in the autosource list. But then when I save the form with the grid with multiple rows, how do I retrieve the Id values to save them in the Database?

Please help and thanks for your great help !.

jaxDeveloper at 2007-8-31 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...
# 5

It did not seam like the textchanged event was ever firing. I made some changes to the control.



public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
private string textBoxValue = string.Empty;
public string ComboValue
{
get
{
if (this.comboBox1.SelectedValue != null)
{
return this.comboBox1.SelectedValue.ToString();
}
else
{
return string.Empty;
}
}
}
public override string Text
{
get
{
return textBox1.Text;
}
set
{
textBox1.Text = value;
}
}

public ComboBox ComboControl
{
get
{
return comboBox1;
}
}
public TextBox TextBoxControl
{
get
{
return textBox1;
}
}

public delegate void TextChangedEventHandler(Object sender, EventArgs e);
public virtual event TextChangedEventHandler TextChanged;
protected override void OnTextChanged(EventArgs e)
{
if (TextChanged != null)
{
TextChanged(this, e);
}
}
private void textBox1_TextChanged(object sender, EventArgs e)
{
OnTextChanged(e);
if (textBox1.Text.Length > 0)
{
DataSet ds = GetSelectedCompanies(textBox1.Text);
if (ds != null)
{
if (ds.Tables[0].Rows.Count < 100)
{
comboBox1.DisplayMember = "name";
comboBox1.ValueMember = "agentno";
//comboBox1.DataSource = ds.Tables[0];
}
else
{
return;
}
}
}
}
private DataSet GetSelectedCompanies(string companyDescription)
{
return null;// DBClient.RunStoredProcDatasetWithParms("spGetCompany", "companyDesc", companyDescription);
}
private void comboBox1_SelectedValueChanged(object sender, System.EventArgs e)
{
textBox1.Text = comboBox1.Text;
if (comboBox1.Text.Length > 0)
{
int found = comboBox1.Text.ToString().IndexOf("[", 1);
if (found != -1)
{
textBox1.Text = comboBox1.Text.ToString().Substring(0, found - 1);
}
}
}
}


KenTucker at 2007-8-31 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...
# 6
Hi Ken

Again thanks for providing such an valuable support in the forum. I will try this and see if it works. For now am using the AutoComplete functionality. This functionality is better than using the user control.

I want to ask another quick thing. My datagridview is an unbound datagridview, am adding the values to the cells by manually looping through a result set. I want to know how can I save the values changed in the cells by the user only if they have been changed meaning updated or added.

I don't want to loop through all the rows in the unbound datagridview and then save all the values in the DB. I would ideally just save only the changed rows and new rows in the database. And how would I identify the values which have been added fresh to the datagrid.

Please let me know if this is possible.

Thanks,
JaxDeveloper.

jaxDeveloper at 2007-8-31 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...
# 7
There is not anyway to get the changed rows from the datagridviews row collection. I guess you could check the cells value with the formattedvalue in the cells validating event. If they are different the value has been changed and store the row number is a list or something.
KenTucker at 2007-8-31 > top of Msdn Tech,Windows Forms,Windows Forms Data Controls and Databinding...