The problem:
My application requires a user to be able to select multiple entries in a datagrid via a column of checkboxes. The desired behavior is that when you click on a checkbox in the column, it behaves like a normal checkbox, but if you drag over it while the left mouse button is down, its selection state changes to the opposite of what is was before.
What I have tried so far:
I have tried subclassing CheckBox and handling OnMouseEnter, but the first checkbox that is clicked seems to capture the mouse so no other checkboxes fire the OnMouseEnter event.
I have tried implementing a drag-and-drop hack, where the user clicks to select a checkbox, and then drags that checkbox over the others so the others recieve a DragOver event and can switch states. This solution causes the cursor to display as a circle with a slash when not over another checkbox during the drag and drop, which is not acceptable for this application.
What I would like:
I would like a method to implement a checkbox that has the functionality I describe, ideally in a xaml style or subclass that I can reuse, as this functionality is needed in multiple places in my application.
Is there an elegant way to achieve this effect?
I did this in my application, very handy when you have to select, say, 30 checkBoxes.
To do this, i handled the preview mouse event myself : PreviewMouseLeftButtonDown, PreviewMouseMove, PreviewMouseLeftButtonUp.
In PreviewMouseLeftButtonDown : i get the mouse position relative to the control.
In PreviewMouseMove : i draw a rectangle from start to current position if i am far enough from firstPoint. then i iterate in CheckBoxes, see if they intersect with rectangle, and highlight them if so (so the user know whiwh chexboxes will swap)
In PreviewMouseLeftButtonUp : i do the swap for intersecting CheckBoxes.
if it can help you, here's the code i use. it is not MVVM (:-)) but works fine, it might give you ideas. it is an automatic translation from vb.net code.
To make it work, you need a Canvas on top of your CheckBoxes (=within the same grid cell for instance), with property IsHitTestVisible="False" .
Within this Canvas, put a Rectangle nammed "SelectionRectangle" with proper fill and stroke, but with 0.0 Opacity.
// '' <summary>
// '' When Left Mouse button is pressed, remember where the mouse move start
// '' </summary>
private void EditedItems_PreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e) {
StartPoint = Mouse.GetPosition(this);
}
// '' <summary>
// '' When mouse move, update the highlight of the selected items.
// '' </summary>
private void EditedItems_PreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e) {
if ((StartPoint == null)) {
return;
}
PointWhereMouseIs = Mouse.GetPosition(this);
Rect SelectedRect = new Rect(StartPoint, PointWhereMouseIs);
if (((SelectedRect.Width < 20)
&& (SelectedRect.Height < 20))) {
return;
}
// show the rectangle again
Canvas.SetLeft(SelectionRectangle, Math.Min(StartPoint.X, PointWhereMouseIs.X));
Canvas.SetTop(SelectionRectangle, Math.Min(StartPoint.Y, PointWhereMouseIs.Y));
SelectionRectangle.Width = Math.Abs((PointWhereMouseIs.X - StartPoint.X));
SelectionRectangle.Height = Math.Abs((PointWhereMouseIs.Y - StartPoint.Y));
foreach (CheckBox ThisChkBox in EditedItems.Children) {
object rectBounds = VisualTreeHelper.GetDescendantBounds(ThisChkBox);
Vector vector = VisualTreeHelper.GetOffset(ThisChkBox);
rectBounds.Offset(vector);
if (rectBounds.IntersectsWith(SelectedRect)) {
((TextBlock)(ThisChkBox.Content)).Background = Brushes.LightGreen;
}
else {
((TextBlock)(ThisChkBox.Content)).Background = Brushes.Transparent;
}
}
}
// '' <summary>
// '' When Left Mouse button is released, change all CheckBoxes values. (Or do nothing if it is a small move -->
// '' click will be handled in a standard way.)
// '' </summary>
private void EditedItems_PreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) {
PointWhereMouseIs = Mouse.GetPosition(this);
Rect SelectedRect = new Rect(StartPoint, PointWhereMouseIs);
StartPoint = null;
SelectionRectangle.Opacity = 0;
// hide the rectangle again
if (((SelectedRect.Width < 20)
&& (SelectedRect.Height < 20))) {
return;
}
foreach (CheckBox ThisEditedItem in EditedItems.Children) {
object rectBounds = VisualTreeHelper.GetDescendantBounds(ThisEditedItem);
Vector vector = VisualTreeHelper.GetOffset(ThisEditedItem);
rectBounds.Offset(vector);
if (rectBounds.IntersectsWith(SelectedRect)) {
ThisEditedItem.IsChecked = !ThisEditedItem.IsChecked;
}
((TextBlock)(ThisEditedItem.Content)).Background = Brushes.Transparent;
}
}
Edit : i used that code within a user control. This control takes a list of booleans and a list of strings (caption) as argument, and builds (with a WrapPanel) an array of CheckBoxes having the right caption. And so you can select/unselect with the rectangle, and there are also two buttons to check all/uncheck all. I tried also to keep good column/rows ratio to handle selection of 7 to 200 booleans with a good column/row balance.