Skip to content

Commit 254ed49

Browse files
authored
Merge pull request w-ahmad#388 from w-ahmad/feature/cell-with-row-selection
feat: Add `CellWithRow` selection support
2 parents 10297af + 3e9e2e1 commit 254ed49

5 files changed

Lines changed: 184 additions & 20 deletions

File tree

src/TableView.Properties.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1037,7 +1037,7 @@ private static void OnSelectionUnitChanged(DependencyObject d, DependencyPropert
10371037
{
10381038
if (d is TableView tableView)
10391039
{
1040-
if (tableView.SelectionUnit is TableViewSelectionUnit.Row)
1040+
if (tableView.SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellWithRow)
10411041
{
10421042
tableView.SelectedCellRanges.Clear();
10431043
tableView.OnCellSelectionChanged();

src/TableView.cs

Lines changed: 52 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public partial class TableView : ListView
4343
private Border? _dragRectangle;
4444
private Point? _dragStartPoint;
4545
private bool _cellSelectionDirty;
46+
private bool _suppressSelectionChangedCellClear;
4647
private Point? _lastDragCanvasPoint;
4748
private DispatcherTimer? _autoScrollTimer;
4849
private double _autoScrollVerticalDelta;
@@ -78,21 +79,28 @@ public TableView()
7879
/// </summary>
7980
private void TableView_SelectionChanged(object sender, SelectionChangedEventArgs e)
8081
{
81-
if (!KeyboardHelper.IsCtrlKeyDown())
82+
if (_suppressSelectionChangedCellClear)
8283
{
83-
SelectedCellRanges.Clear();
84+
_suppressSelectionChangedCellClear = false;
8485
}
8586
else
8687
{
87-
SelectedCellRanges.RemoveWhere(slots =>
88+
if (!KeyboardHelper.IsCtrlKeyDown())
8889
{
89-
slots.RemoveWhere(slot => SelectedRanges.Any(range => range.IsInRange(slot.Row)));
90-
return slots.Count == 0;
91-
});
92-
}
90+
SelectedCellRanges.Clear();
91+
}
92+
else
93+
{
94+
SelectedCellRanges.RemoveWhere(slots =>
95+
{
96+
slots.RemoveWhere(slot => SelectedRanges.Any(range => range.IsInRange(slot.Row)));
97+
return slots.Count == 0;
98+
});
99+
}
93100

94-
CurrentCellSlot = null;
95-
OnCellSelectionChanged();
101+
CurrentCellSlot = null;
102+
OnCellSelectionChanged();
103+
}
96104

97105
if (SelectedItems?.Count == 1)
98106
{
@@ -1114,9 +1122,13 @@ internal void MakeSelection(TableViewCellSlot slot, bool shiftKey, bool ctrlKey
11141122
{
11151123
ctrlKey = ctrlKey || SelectionMode is ListViewSelectionMode.Multiple;
11161124

1117-
if (SelectionUnit is TableViewSelectionUnit.Row
1118-
|| (LastSelectionUnit is TableViewSelectionUnit.Row && slot.IsValidRow(this) && !slot.IsValidColumn(this))
1119-
|| (SelectionUnit is TableViewSelectionUnit.CellOrRow && slot.IsValidRow(this) && !slot.IsValidColumn(this)))
1125+
_suppressSelectionChangedCellClear = SelectionUnit is TableViewSelectionUnit.CellWithRow;
1126+
var shouldSelectRows = SelectionUnit is TableViewSelectionUnit.Row
1127+
|| (SelectionUnit is TableViewSelectionUnit.CellWithRow && !slot.IsValidColumn(this))
1128+
|| (LastSelectionUnit is TableViewSelectionUnit.Row && slot.IsValidRow(this) && !slot.IsValidColumn(this))
1129+
|| (SelectionUnit is TableViewSelectionUnit.CellOrRow && slot.IsValidRow(this) && !slot.IsValidColumn(this));
1130+
1131+
if (shouldSelectRows)
11201132
{
11211133
if (!ctrlKey)
11221134
DeselectAllCells();
@@ -1125,8 +1137,15 @@ internal void MakeSelection(TableViewCellSlot slot, bool shiftKey, bool ctrlKey
11251137
}
11261138
else
11271139
{
1128-
if (!ctrlKey)
1140+
if (SelectionUnit is TableViewSelectionUnit.CellWithRow)
1141+
{
1142+
SelectRows(slot, shiftKey, ctrlKey);
1143+
}
1144+
else if (!ctrlKey)
1145+
{
11291146
DeselectAllItems();
1147+
}
1148+
11301149
SelectCells(slot, shiftKey, ctrlKey);
11311150
LastSelectionUnit = TableViewSelectionUnit.Cell;
11321151
}
@@ -1210,12 +1229,29 @@ private void SelectCells(TableViewCellSlot slot, bool shiftKey, bool ctrlKey)
12101229

12111230
if (!ctrlKey || !(SelectionMode is ListViewSelectionMode.Multiple or ListViewSelectionMode.Extended))
12121231
{
1213-
DeselectAll();
1232+
if (SelectionUnit is TableViewSelectionUnit.CellWithRow)
1233+
{
1234+
DeselectAllCells();
1235+
}
1236+
else
1237+
{
1238+
DeselectAll();
1239+
}
12141240
}
12151241

12161242
var selectionRange = (SelectionStartCellSlot is null ? null : SelectedCellRanges.LastOrDefault(x => SelectionStartCellSlot.HasValue && x.Contains(SelectionStartCellSlot.Value))) ?? [];
1217-
SelectedCellRanges.Remove(selectionRange);
1218-
selectionRange.Clear();
1243+
1244+
if (ctrlKey && SelectionMode is ListViewSelectionMode.Multiple or ListViewSelectionMode.Extended)
1245+
{
1246+
selectionRange = SelectedCellRanges.SelectMany(x => x).ToHashSet();
1247+
SelectedCellRanges.Clear();
1248+
}
1249+
else
1250+
{
1251+
SelectedCellRanges.Remove(selectionRange);
1252+
selectionRange.Clear();
1253+
}
1254+
12191255
SelectionStartCellSlot ??= CurrentCellSlot;
12201256
SelectionStartCellSlot ??= slot;
12211257

@@ -1824,7 +1860,6 @@ void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e)
18241860
private void UpdateBaseSelectionMode()
18251861
{
18261862
_shouldThrowSelectionModeChangedException = true;
1827-
18281863
base.SelectionMode = SelectionUnit is TableViewSelectionUnit.Cell ? ListViewSelectionMode.None : SelectionMode;
18291864

18301865
UpdateHorizontalScrollBarMargin();

src/TableViewRow.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ protected override void OnTapped(TappedRoutedEventArgs e)
186186
{
187187
base.OnTapped(e);
188188

189-
if (TableView?.SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow)
189+
if (TableView?.SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow or TableViewSelectionUnit.CellWithRow)
190190
{
191191
TableView.CurrentRowIndex = Index;
192192
TableView.LastSelectionUnit = TableViewSelectionUnit.Row;

src/TableViewSelectionUnit.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
public enum TableViewSelectionUnit
77
{
88
/// <summary>
9-
/// Selection can be either a cell or a row.
9+
/// Selection can be either a cell or a row, but selecting a cell does not select the owning row.
1010
/// </summary>
1111
CellOrRow,
1212

13+
/// <summary>
14+
/// Selection can be either a cell or a row, but selecting a cell also selects the owning row.
15+
/// </summary>
16+
CellWithRow,
17+
1318
/// <summary>
1419
/// Selection is limited to cells.
1520
/// </summary>
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using Microsoft.UI.Xaml;
2+
using Microsoft.UI.Xaml.Controls;
3+
using Microsoft.UI.Xaml.Data;
4+
using Microsoft.VisualStudio.TestTools.UnitTesting;
5+
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
6+
using System.Threading.Tasks;
7+
8+
namespace WinUI.TableView.Tests;
9+
10+
[TestClass]
11+
public class TableViewSelectionUnitTests
12+
{
13+
[UITestMethod]
14+
public async Task CellWithRow_CellClickSelectsCellAndOwningRow()
15+
{
16+
var tableView = await CreateTableViewAsync(TableViewSelectionUnit.CellWithRow);
17+
18+
tableView.MakeSelection(new TableViewCellSlot(1, 0), false, false);
19+
20+
await Task.Yield(); // Allow selection to propagate
21+
22+
Assert.IsTrue(tableView.SelectedCells.Contains(new TableViewCellSlot(1, 0)));
23+
Assert.AreEqual(1, tableView.SelectedItems.Count);
24+
Assert.AreSame(tableView.Items[1], tableView.SelectedItem);
25+
}
26+
27+
[UITestMethod]
28+
public async Task CellAndRow_RowHeaderClickSelectsOnlyRow()
29+
{
30+
var tableView = await CreateTableViewAsync(TableViewSelectionUnit.CellWithRow);
31+
32+
tableView.MakeSelection(new TableViewCellSlot(1, -1), false, false);
33+
34+
Assert.AreEqual(0, tableView.SelectedCells.Count);
35+
Assert.AreEqual(1, tableView.SelectedItems.Count);
36+
Assert.AreSame(tableView.Items[1], tableView.SelectedItem);
37+
}
38+
39+
[UITestMethod]
40+
public async Task CellSelectionUnitStillSelectsOnlyCells()
41+
{
42+
var tableView = await CreateTableViewAsync(TableViewSelectionUnit.Cell);
43+
44+
tableView.MakeSelection(new TableViewCellSlot(0, 0), false, false);
45+
46+
await Task.Yield(); // Allow selection to propagate
47+
48+
tableView.MakeSelection(new TableViewCellSlot(1, 1), false, true);
49+
50+
await Task.Yield(); // Allow selection to propagate
51+
52+
Assert.AreEqual(0, tableView.SelectedItems.Count);
53+
Assert.AreEqual(2, tableView.SelectedCells.Count);
54+
Assert.IsTrue(tableView.SelectedCells.Contains(new TableViewCellSlot(0, 0)));
55+
Assert.IsTrue(tableView.SelectedCells.Contains(new TableViewCellSlot(1, 1)));
56+
}
57+
58+
[UITestMethod]
59+
public async Task CellOrRowSelectionUnitStillUsesCellAndRowSemantics()
60+
{
61+
var tableView = await CreateTableViewAsync(TableViewSelectionUnit.CellOrRow);
62+
63+
tableView.MakeSelection(new TableViewCellSlot(1, 0), false, false);
64+
Assert.AreEqual(0, tableView.SelectedItems.Count);
65+
66+
tableView.MakeSelection(new TableViewCellSlot(0, -1), false, false);
67+
Assert.AreEqual(1, tableView.SelectedItems.Count);
68+
Assert.AreEqual(0, tableView.SelectedCells.Count);
69+
}
70+
71+
[UITestMethod]
72+
public async Task CellWithRow_MultiSelectionAddsCellAndRowSelections()
73+
{
74+
var tableView = await CreateTableViewAsync(TableViewSelectionUnit.CellWithRow, ListViewSelectionMode.Multiple);
75+
76+
tableView.MakeSelection(new TableViewCellSlot(0, 0), false, false);
77+
78+
await Task.Yield(); // Allow selection to propagate
79+
80+
tableView.MakeSelection(new TableViewCellSlot(1, 1), false, true);
81+
82+
await Task.Delay(200); // Allow selection to propagate
83+
84+
Assert.AreEqual(2, tableView.SelectedItems.Count);
85+
Assert.AreEqual(2, tableView.SelectedCells.Count);
86+
Assert.IsTrue(tableView.SelectedCells.Contains(new TableViewCellSlot(0, 0)));
87+
Assert.IsTrue(tableView.SelectedCells.Contains(new TableViewCellSlot(1, 1)));
88+
}
89+
90+
private static async Task<TableView> CreateTableViewAsync(TableViewSelectionUnit selectionUnit, ListViewSelectionMode selectionMode = ListViewSelectionMode.Extended)
91+
{
92+
var tableView = new TableView
93+
{
94+
SelectionMode = selectionMode,
95+
SelectionUnit = selectionUnit
96+
};
97+
98+
tableView.Columns.Add(new TableViewTextColumn
99+
{
100+
Header = "Name",
101+
Binding = new Binding { Path = new PropertyPath(nameof(SelectionItem.Name)) }
102+
});
103+
tableView.Columns.Add(new TableViewTextColumn
104+
{
105+
Header = "Value",
106+
Binding = new Binding { Path = new PropertyPath(nameof(SelectionItem.Value)) }
107+
});
108+
tableView.ItemsSource = new[]
109+
{
110+
new SelectionItem { Name = "A", Value = 1 },
111+
new SelectionItem { Name = "B", Value = 2 }
112+
};
113+
114+
await UnitTestApp.Current.MainWindow.LoadTestContentAsync(tableView);
115+
116+
return tableView;
117+
}
118+
119+
private sealed class SelectionItem
120+
{
121+
public string Name { get; set; } = string.Empty;
122+
public int Value { get; set; }
123+
}
124+
}

0 commit comments

Comments
 (0)