Search code examples
c#winformsdatagridviewautocompletedatagridviewcolumn

Creating AutoComplete column in DataGridView


I am trying to create an autocomplete column using DataGridView in WinForms using C#. I have managed to get it working using EditingControlShowing event of DataGridView and is working fine.

However, as per normal AutoComplete textbox, the filtered list shows only data filtering based on "Starting With". To resolve this, I have used an AutoComplete TextBox from here that allows substring search using a custom Listbox.

Taking this custom control as base, I created a custom control inheriting DataGridViewColumn. The problem is the ListBox control doesn't show inline with the gridview cell. Here's the code -

public class DataGridViewAutoCompleteColumn : DataGridViewColumn
{
    public DataGridViewAutoCompleteColumn()
        : base(new DataGridViewAutoCompleteCell())
    {
    }

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


}

public class DataGridViewAutoCompleteCell : DataGridViewTextBoxCell
{

    public DataGridViewAutoCompleteCell()
        : base()
    {
        // Use the short date format.
        this.Style.Format = "d";
    }

    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);
        AutoCompleteEditingControl ctl = DataGridView.EditingControl as AutoCompleteEditingControl;
        ctl.AutoCompleteList = this.AutoCompleteList;
        // Use the default row value when Value property is null.
        if (this.Value == null)
        {
            ctl.Text = (string)this.DefaultNewRowValue;
        }
        else
        {
            ctl.Text = (string)this.Value;
        }
    }

    public override Type EditType
    {
        get
        {
            // Return the type of the editing control that DataGridViewAutoCompleteCell uses.
            return typeof(AutoCompleteEditingControl);
        }
    }

    public override Type ValueType
    {
        get
        {
            // Return the type of the value that DataGridViewAutoCompleteCell contains.

            return typeof(String);
        }
    }

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

    public List<String> AutoCompleteList { get; set; }
}

class AutoCompleteEditingControl : AutoCompleteTextbox, IDataGridViewEditingControl
{
    DataGridView dataGridView;
    private bool valueChanged = false;
    int rowIndex;

    public AutoCompleteEditingControl()
    {

    }

    // Implements the IDataGridViewEditingControl.EditingControlFormattedValue 
    // property.
    public object EditingControlFormattedValue
    {
        get
        {
            return this.Text;
        }
        set
        {
            if (value is String)
            {
                try
                {
                    // This will throw an exception of the string is 
                    // null, empty, or not in the format of a date.
                    this.Text = (String)value;
                }
                catch
                {
                    // In the case of an exception, just use the 
                    // default value so we're not left with a null
                    // value.
                    this.Text = String.Empty;
                }
            }
        }
    }

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

    // Implements the 
    // IDataGridViewEditingControl.ApplyCellStyleToEditingControl method.
    public void ApplyCellStyleToEditingControl(
        DataGridViewCellStyle dataGridViewCellStyle)
    {
        this.Font = dataGridViewCellStyle.Font;
        this.ForeColor = dataGridViewCellStyle.ForeColor;
        this.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 !dataGridViewWantsInputKey;
        }
    }

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

Please advise on what wrong am I doing here.


Solution

  • I've done a few of these now and while they're really powerful, it's a bit more complicated than it really should be.

    First up you're not passing the AutoCompleteList in anywhere at column level. This would mean it'd needs setting on each cell, which might be useful, but it isn't how datagrids usually function. So it needs to be a property of the column class, as that's where you can set it.

    Additionally if the Column class has any custom properties it needs to override the Clone() method to maintain these properties. Something inside the implementation means they just don't work without this. You might want to expose properties for CaseSensitive and MinTypedCharacters.

    public class DataGridViewAutoCompleteColumn : DataGridViewColumn
    {
        public DataGridViewAutoCompleteColumn()
            : base(new DataGridViewAutoCompleteCell())
        {
        }
    
        public override DataGridViewCell CellTemplate
        {
            get
            {
                return base.CellTemplate;
            }
            set
            {
                // Ensure that the cell used for the template is a DataGridViewAutoCompleteCell.
                if (value != null &&
                    !value.GetType().IsAssignableFrom(typeof(DataGridViewAutoCompleteCell)))
                {
                    throw new InvalidCastException("Must be a DataGridViewAutoCompleteCell");
                }
                base.CellTemplate = value;
            }
        }
    
        [Browsable(true)]
        public List<string> AutoCompleteList
        {
            get; set;
        }
    
        [Browsable(true)]
        public int MinTypedCharacters { get; set; }
        [Browsable(true)]
        public bool CaseSensitive { get; set; }
    
        public override object Clone()
        {
            DataGridViewAutoCompleteColumn clone = (DataGridViewAutoCompleteColumn)base.Clone();
            clone.AutoCompleteList = this.AutoCompleteList;
            clone.MinTypedCharacters = this.MinTypedCharacters;
            clone.CaseSensitive = this.CaseSensitive;
            return clone;
        }
    }
    

    On the cell class we can still use a cell level list as an override by modifying it's AutoComplete list property.

    private List<string> _autoCompleteList;
    public List<String> AutoCompleteList
    {
        get
        {
            if (_autoCompleteList == null)
                return ((DataGridViewAutoCompleteColumn)this.OwningColumn).AutoCompleteList;
            else
                return
                    _autoCompleteList;
        }
        set
        {
            _autoCompleteList = value;
        }
    }
    

    You then need to pass those settings on in InitializeEditingControl. You can access the column object like this:

    DataGridViewAutoCompleteColumn col = (DataGridViewAutoCompleteColumn)this.OwningColumn;
    

    Next there's a bug in the DataGridViewCell.Value get accessor that means you can't safely use it in the InitializeEditingControl() method. It will sometimes try to use an invalid rowIndex. You should use GetValue(rowIndex) instead.

    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);
        AutoCompleteEditingControl ctl = DataGridView.EditingControl as AutoCompleteEditingControl;
        ctl.AutoCompleteList = this.AutoCompleteList;
        // Use the default row value when Value property is null.
        if (this.Value == null)
        {
            ctl.Text = (string)this.DefaultNewRowValue;
        }
        else
        {
            ctl.Text = (string)this.GetValue(rowIndex);  // this line can't use this.Value
        }
    }
    

    The other problems, including what you first asked are in the AutoCompleteTextBox class (you haven't posted code for this)

    It's ParentForm method creates null references when the controls being created.
    But more fundamental than this the listview is shown on the form, it doesn't work properly when the editing control is within a container, or in this case a datagridview.

    this.Location for the editing control will be ~0,0 when its in a cell. You need to translate that to form coordinates.

    https://stackoverflow.com/a/1478105/4605432

    // in the AutoCompleteTextBox itself
        private Form ParentForm
        {
            get
            {
                if (this.Parent != null)
                    return this.Parent.FindForm();
                else
                    return null;
            }
        }
    
        private void UpdateListBoxItems()
        {
            // if there is a ParentForm
            if ((ParentForm != null))
            {
        // this will get the position relative to the form, use instead of this.Location
                Point formposition = this.ParentForm.PointToClient(this.Parent.PointToScreen(this.Location));
                // get its width
                panel.Width = this.Width;
                // calculate the remeining height beneath the TextBox
                panel.Height = this.ParentForm.ClientSize.Height - this.Height - formposition.Y;
                // and the Location to use
                panel.Location = formposition + new Size(0, this.Height);
                // Panel and ListBox have to be added to ParentForm.Controls before calling BingingContext
                if (!this.ParentForm.Controls.Contains(panel))
                {
                    // add the Panel and ListBox to the PartenForm
                    this.ParentForm.Controls.Add(panel);
                }
                ((CurrencyManager)listBox.BindingContext[CurrentAutoCompleteList]).Refresh();
            }
        }
    

    But there are other problems beyond the scope of this question, like an ArgumentOutOfRange I sometimes get in UpdateListBoxItems() when editing a value. The listbox doesn't always hide itself if the controls deactivated and thus can get detached. Honestly the control seems to be a bit of a mess.