I am trying to display data in a table format and allow users to pick cells. However, they should only be able to pick 1 cell per column, which is why MultiSelect is required.
Example of 1 cell selection per column:
Currently, I have an implementation for allowing only 1 selected cell per column, which works until I try to drag select.
Example of drag selection:
From what I've seen, there is no built-in way to disable drag selection if MultiSelect needs to be enabled.
Here is the current implementation:
private void TableViewer_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
{
// Skip headers
if (e.RowIndex \< 0 || e.ColumnIndex \< 0)
{
return;
}
// Disable multiple selections within the same column
var selectedCells = gridViewTableViewer.SelectedCells.Cast<DataGridViewCell>();
foreach (DataGridViewCell cell in selectedCells)
{
if (cell.ColumnIndex == e.ColumnIndex)
{
// Another cell in the same column has already been selected.
cell.Selected = false;
}
}
}
I've tried keeping track of the left mouse button (LMB) being held down/pressed through the CellMouseDown
and MouseUp
events + a TableViewer.ClearSelection()
inside the CellMouseEnter
event (if LMB is being held down):
(TableViewer is a DataGridView)
private void TableViewer_CellMouseEnter(object sender, DataGridViewCellEventArgs e)
{
if (isMouseDown)
{
TableViewer.ClearSelection();
}
}
private void TableViewer_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
{
isMouseDown = true;
...
}
private void TableViewer_MouseUp(object sender, MouseEventArgs e)
{
isMouseDown = false;
}
Any help would be appreciated - thank you so much!
Your post states:
they should only be able to pick 1 cell per column.
As your image shows, the problem is that dragging over multiple cells with MultiSelect
enabled is going to break that rule by selecting "multiple cells per column".
In other words, we need to be able to preview when the cell selection is changing, and have some criteria for whether to allow the change to occur. The DataGridView.SetSelectedCellCore
method is an ideal place to do this, and since this is protected and fires no event, it's necessary to make a lightweight extended class DataGridViewEx : DataGridView
.
The main feature of DataGridViewEx
is to be able to SingleSelectInColumn(int columnIndex, int rowIndex)
. Your code is correct that this needs to happen OnCellMouseDown
. But to fix your problem with the drag-select, we also need to call it OnCellMouseEnter
.
Meanwhile, the overridden SetSelectedCellCore
method inspects _allowedColumn
and if it's set, disallows attempted changes to any other column.
I used the minimal test code shown below to verify that this works as intended.
class DataGridViewEx : DataGridView
{
protected override void OnCellMouseDown(DataGridViewCellMouseEventArgs e)
{
var cellToggle =
e.ColumnIndex >= 0 && e.RowIndex >= 0 ?
this[e.ColumnIndex, e.RowIndex] : null;
if (ModifierKeys == Keys.Control && cellToggle?.Selected == true)
{
cellToggle.Selected = false;
}
else
{
BeginInvoke(() => SingleSelectInColumn(e.ColumnIndex, e.RowIndex));
}
// CRITICAL - Do 'not' call base class method!
}
private void SingleSelectInColumn(int columnIndex, int rowIndex)
{
try
{
_allowedColumn = columnIndex;
if (columnIndex >= 0 && rowIndex >= 0)
{
var cellsInColumn =
Rows
.OfType<DataGridViewRow>()
.Select(_ => _.Cells[columnIndex]);
foreach (
var cell in
cellsInColumn )
{
var sbSelected = cell.RowIndex == rowIndex;
if(!Equals(cell.Selected, sbSelected))
{
cell.Selected = sbSelected;
}
}
}
}
finally
{
_allowedColumn = null;
}
}
public int? _allowedColumn = null;
protected override void OnCellMouseEnter(DataGridViewCellEventArgs e)
{
base.OnCellMouseEnter(e);
if (MouseButtons == MouseButtons.Left)
{
BeginInvoke(()=> SingleSelectInColumn(e.ColumnIndex, e.RowIndex ));
}
}
protected override void OnMouseUp(MouseEventArgs e)
{
base.OnMouseUp(e);
// CRITICAL - We have to put this back the way we found it!
_allowedColumn = null;
}
protected override void OnCellMouseLeave(DataGridViewCellEventArgs e)
{
base.OnCellMouseLeave(e);
if (MouseButtons == MouseButtons.Left)
{
// CRITICAL - For example if the mouse is dragged outside the control.
_allowedColumn = int.MinValue;
}
}
protected override void SetSelectedCellCore(int columnIndex, int rowIndex, bool selected)
{
if (_allowedColumn is int validColumnIndex)
{
if(!Equals(columnIndex, validColumnIndex))
{
return;
}
}
base.SetSelectedCellCore(
columnIndex,
rowIndex,
selected);
}
}
You would then just manually edit your MainForm.Designer.cs file, substituting DataGridViewEx
for DataGridView
in two places.
This routine simply populates the DataGridView
with some rows to test. Then, in a continuous motion, the drag select is tested to ensure that the "one cell per column" rule is respected.
public partial class MainForm : Form
{
public MainForm() => InitializeComponent();
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
dataGridView.DataSource = Records;
dataGridView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
for (int i = 0; i< 10; i++)
{
Records.AddNew();
}
}
BindingList<Record> Records { get; } = new BindingList<Record>();
}
public class Record
{
static int _debugCount = 1;
public Record()
{
col1 = col2 = col3 = _debugCount ++;
}
public int col1 { get; set; }
public int col2 { get; set; }
public int col3 { get; set; }
}