From 818f791a747d2fcc72f41157b95c307e38b765e4 Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Tue, 28 Jul 2026 01:13:05 +0500 Subject: [PATCH 01/11] feat: allow drag selection to start from rows and empty space --- src/Extensions/ItemIndexRangeExtensions.cs | 51 +- .../TableViewCellSlotRangeExtensions.cs | 189 ++++++ src/Helpers/IndexRangeHelper.cs | 44 ++ src/Helpers/TableViewTrace.cs | 12 + src/TableView.Events.cs | 5 +- src/TableView.Properties.cs | 4 +- src/TableView.cs | 610 ++++++++++++++---- src/TableViewCell.cs | 159 +---- src/TableViewCellSlot.cs | 8 +- src/TableViewCellSlotRange.cs | 131 ++++ src/TableViewRow.cs | 38 -- tests/DragSelectionRectangleTests.cs | 36 ++ tests/TableViewSelectionUnitTests.cs | 19 +- 13 files changed, 955 insertions(+), 351 deletions(-) create mode 100644 src/Extensions/TableViewCellSlotRangeExtensions.cs create mode 100644 src/Helpers/IndexRangeHelper.cs create mode 100644 src/Helpers/TableViewTrace.cs create mode 100644 src/TableViewCellSlotRange.cs diff --git a/src/Extensions/ItemIndexRangeExtensions.cs b/src/Extensions/ItemIndexRangeExtensions.cs index 7afae48c..13a6db7c 100644 --- a/src/Extensions/ItemIndexRangeExtensions.cs +++ b/src/Extensions/ItemIndexRangeExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.UI.Xaml.Data; +using System.Runtime.CompilerServices; namespace WinUI.TableView.Extensions; @@ -21,11 +22,55 @@ public static bool IsInRange(this ItemIndexRange range, int index) /// /// Determines whether the given item index range is valid within the TableView. /// - /// The ItemIndexRange to check. + /// The ItemIndexRange to check. /// The TableView to check against. /// True if the item index range of TableView is valid; otherwise, false. - public static bool IsValid(this ItemIndexRange itemIndexRange, TableView tableView) + public static bool IsValid(this ItemIndexRange range, TableView tableView) + { + return range.FirstIndex >= 0 && range.LastIndex < tableView?.Items.Count; + } + + /// + /// Determines whether the specified range completely contains another range. + /// + /// The range to check. + /// The range to check against. + /// True if the range completely contains the other range; otherwise, false. + public static bool Contains(this ItemIndexRange range, ItemIndexRange other) { - return itemIndexRange.FirstIndex >= 0 && itemIndexRange.LastIndex < tableView?.Items.Count; + return other.FirstIndex >= range.FirstIndex && other.LastIndex <= range.LastIndex; + } + + /// + /// Subtracts another ItemIndexRange from the current range and returns the resulting range. + /// + /// The range to subtract from. + /// The range to subtract. + /// The resulting range after subtraction. + public static IEnumerable Subtract(this ItemIndexRange range, ItemIndexRange other) + { + var start = range.FirstIndex; + var end = start + (int)range.Length - 1; + + var otherStart = other.FirstIndex; + var otherEnd = otherStart + (int)other.Length - 1; + + // No overlap. + if (otherEnd < start || otherStart > end) + { + yield break; + } + + // Left remainder. + if (otherStart > start) + { + yield return new ItemIndexRange(start, (uint)(otherStart - start)); + } + + // Right remainder. + if (otherEnd < end) + { + yield return new ItemIndexRange(otherEnd + 1, (uint)(end - otherEnd)); + } } } diff --git a/src/Extensions/TableViewCellSlotRangeExtensions.cs b/src/Extensions/TableViewCellSlotRangeExtensions.cs new file mode 100644 index 00000000..01fbd49e --- /dev/null +++ b/src/Extensions/TableViewCellSlotRangeExtensions.cs @@ -0,0 +1,189 @@ +using Microsoft.UI.Xaml.Data; + +namespace WinUI.TableView.Extensions; + +/// +/// Provides extension methods for the TableViewCellSlotRange type. +/// +internal static class TableViewCellSlotRangeExtensions +{ + /// + /// Determines whether a specified cell slot is within the range. + /// + /// The TableViewCellSlotRange to check. + /// The cell slot to check. + /// True if the slot is within the range; otherwise, false. + public static bool IsInRange(this TableViewCellSlotRange? range, TableViewCellSlot slot) + { + if (range is null || range.Length <= 0) return false; + + var minRow = Math.Min(range.FirstRow, range.LastRow); + var maxRow = Math.Max(range.FirstRow, range.LastRow); + var minColumn = Math.Min(range.FirstColumn, range.LastColumn); + var maxColumn = Math.Max(range.FirstColumn, range.LastColumn); + + return slot.Row >= minRow && slot.Row <= maxRow + && slot.Column >= minColumn && slot.Column <= maxColumn; + } + + /// + /// Determines whether a specified row index is within the range. + /// + /// The TableViewCellSlotRange to check. + /// The row index to check. + /// True if the row index is within the range; otherwise, false. + public static bool IsRowInRange(this TableViewCellSlotRange? range, int row) + { + return range?.Length > 0 + && row >= Math.Min(range.FirstRow, range.LastRow) + && row <= Math.Max(range.FirstRow, range.LastRow); + } + + /// + /// Determines whether a specified column index is within the range. + /// + /// The TableViewCellSlotRange to check. + /// The column index to check. + /// True if the column index is within the range; otherwise, false. + public static bool IsColumnInRange(this TableViewCellSlotRange? range, int column) + { + return range?.Length > 0 + && column >= Math.Min(range.FirstColumn, range.LastColumn) + && column <= Math.Max(range.FirstColumn, range.LastColumn); + } + + /// + /// Determines whether the given cell slot range is valid within the TableView. + /// + /// The TableViewCellSlotRange to check. + /// The TableView to check against. + /// True if the cell slot range of TableView is valid; otherwise, false. + public static bool IsValid(this TableViewCellSlotRange range, TableView tableView) + { + return range.FirstSlot.IsValid(tableView) && range.LastSlot.IsValid(tableView); + } + + /// + /// Returns all cell slots contained within this range, enumerated row by row. + /// + public static IEnumerable GetSlots(this TableViewCellSlotRange range) + { + for (var row = range.FirstRow; row <= range.LastRow; row++) + { + for (var col = range.FirstColumn; col <= range.LastColumn; col++) + { + yield return new TableViewCellSlot(row, col); + } + } + } + + /// + /// Determines whether a specific cell slot falls within this range. + /// + public static bool Contains(this TableViewCellSlotRange range, int rowIndex, int columnIndex) + { + return rowIndex >= range.FirstRow && rowIndex <= range.LastRow && + columnIndex >= range.FirstColumn && columnIndex <= range.LastColumn; + } + + /// + /// Determines whether another TableViewCellSlotRange is completely contained within this range. + /// + public static bool Contains(this TableViewCellSlotRange? range, TableViewCellSlotRange? other) + { + if (range == null || other == null) return false; + + return range.Contains(other.FirstRow, other.FirstColumn) && + range.Contains(other.LastRow, other.LastColumn); + } + + /// + /// Determines whether another range intersects with this range. + /// + public static bool IntersectsWith(this TableViewCellSlotRange range, TableViewCellSlotRange other) + { + if (other == null) return false; + + return range.FirstRow <= other.LastRow && range.LastRow >= other.FirstRow && + range.FirstColumn <= other.LastColumn && range.LastColumn >= other.FirstColumn; + } + + /// + /// Subtracts another range from this range and returns the resulting ranges. + /// + /// The range to subtract from. + /// The range to subtract. + /// An enumerable of resulting ranges after subtraction. + public static IEnumerable Subtract(this TableViewCellSlotRange range, TableViewCellSlotRange other) + { + // No overlap. + if (!range.IntersectsWith(other)) + { + yield return range; + yield break; + } + + // Intersection rectangle. + var top = Math.Max(range.FirstRow, other.FirstRow); + var left = Math.Max(range.FirstColumn, other.FirstColumn); + var bottom = Math.Min(range.LastRow, other.LastRow); + var right = Math.Min(range.LastColumn, other.LastColumn); + + // Top strip. + if (range.FirstRow < top) + { + yield return new TableViewCellSlotRange( + range.FirstRow, + range.FirstColumn, + top - range.FirstRow, + range.Columns); + } + + // Bottom strip. + if (bottom < range.LastRow) + { + yield return new TableViewCellSlotRange( + bottom + 1, + range.FirstColumn, + range.LastRow - bottom, + range.Columns); + } + + // Left strip. + if (range.FirstColumn < left) + { + yield return new TableViewCellSlotRange( + top, + range.FirstColumn, + bottom - top + 1, + left - range.FirstColumn); + } + + // Right strip. + if (right < range.LastColumn) + { + yield return new TableViewCellSlotRange( + top, + right + 1, + bottom - top + 1, + range.LastColumn - right); + } + } + + /// + /// Merges two TableViewCellSlotRanges into a single range that encompasses both. + /// + public static TableViewCellSlotRange Merge(this TableViewCellSlotRange range, TableViewCellSlotRange other) + { + var firstRow = Math.Min(range.FirstRow, other.FirstRow); + var firstColumn = Math.Min(range.FirstColumn, other.FirstColumn); + var lastRow = Math.Max(range.LastRow, other.LastRow); + var lastColumn = Math.Max(range.LastColumn, other.LastColumn); + + return TableViewCellSlotRange.FromCoordinates( + firstRow, + firstColumn, + lastRow, + lastColumn); + } +} \ No newline at end of file diff --git a/src/Helpers/IndexRangeHelper.cs b/src/Helpers/IndexRangeHelper.cs new file mode 100644 index 00000000..59b73b39 --- /dev/null +++ b/src/Helpers/IndexRangeHelper.cs @@ -0,0 +1,44 @@ +using Microsoft.UI.Xaml.Data; + +namespace WinUI.TableView.Helpers; + +/// +/// Provides helper methods for working with index ranges in a TableView. +/// +internal static class IndexRangeHelper +{ + /// + /// Gets a list of contiguous index ranges from a collection of indexes. + /// + /// The collection of indexes to process. + /// A list of contiguous index ranges. + public static List GetRanges(IEnumerable indexes) + { + var sorted = indexes.Order().ToArray(); + + if (sorted.Length == 0) + return []; + + List ranges = []; + + var first = sorted[0]; + var previous = sorted[0]; + + for (var i = 1; i < sorted.Length; i++) + { + if (sorted[i] == previous + 1) + { + previous = sorted[i]; + continue; + } + + ranges.Add(new ItemIndexRange(first, (uint)(previous - first + 1))); + + first = previous = sorted[i]; + } + + ranges.Add(new ItemIndexRange(first, (uint)(previous - first + 1))); + + return ranges; + } +} diff --git a/src/Helpers/TableViewTrace.cs b/src/Helpers/TableViewTrace.cs new file mode 100644 index 00000000..2e1d9fa1 --- /dev/null +++ b/src/Helpers/TableViewTrace.cs @@ -0,0 +1,12 @@ +using System.Diagnostics; + +namespace WinUI.TableView.Helpers; + +internal static class TableViewTrace +{ + [Conditional("DEBUG")] + public static void Write(string message) + { + Debug.WriteLine($"[TableView] {message}"); + } +} \ No newline at end of file diff --git a/src/TableView.Events.cs b/src/TableView.Events.cs index 47868c32..5d313047 100644 --- a/src/TableView.Events.cs +++ b/src/TableView.Events.cs @@ -1,6 +1,6 @@ using Microsoft.UI.Xaml; -using System; -using System.ComponentModel; +using System.Diagnostics; +using WinUI.TableView.Helpers; namespace WinUI.TableView; @@ -186,6 +186,7 @@ protected internal virtual void OnClearSorting(TableViewClearSortingEventArgs ar /// protected virtual void OnCellSelectionChanged(TableViewCellSelectionChangedEventArgs args) { + TableViewTrace.Write($"TableViewCellSelectionChanged: Added={args.AddedCells.Count}, Removed={args.RemovedCells.Count}"); CellSelectionChanged?.Invoke(this, args); } diff --git a/src/TableView.Properties.cs b/src/TableView.Properties.cs index 80ac94b0..05f9697b 100644 --- a/src/TableView.Properties.cs +++ b/src/TableView.Properties.cs @@ -416,7 +416,7 @@ public bool ForceRowOrCellSelectionOnContextRequested /// /// Gets the selected cell ranges. /// - internal HashSet> SelectedCellRanges { get; } = []; + internal HashSet SelectedCellRanges { get; } = []; /// /// Gets or sets a value indicating whether the TableView is in editing mode. @@ -897,7 +897,7 @@ private static void OnSelectionModeChanged(DependencyObject d, DependencyPropert if (tableView.SelectionMode is ListViewSelectionMode.Single && tableView.CurrentCellSlot.HasValue) { - tableView.SelectedCellRanges.Add([tableView.CurrentCellSlot.Value]); + tableView.SelectedCellRanges.Add(TableViewCellSlotRange.FromSlots(tableView.CurrentCellSlot.Value)); } tableView.OnCellSelectionChanged(); diff --git a/src/TableView.cs b/src/TableView.cs index 698c482d..4557cf8a 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -3,18 +3,11 @@ using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Input; -using Microsoft.UI.Xaml.Media; -using System; using System.Collections; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Diagnostics; -using System.IO; -using System.Linq; using System.Reflection; using System.Text; -using System.Threading.Tasks; using Windows.ApplicationModel.DataTransfer; using Windows.Foundation; using Windows.Storage; @@ -22,6 +15,7 @@ using Windows.System; using WinUI.TableView.Extensions; using WinUI.TableView.Helpers; +using Pointer = Microsoft.UI.Xaml.Input.Pointer; namespace WinUI.TableView; @@ -42,7 +36,6 @@ public partial class TableView : ListView private readonly CollectionView _collectionView = []; private Border? _dragRectangle; private Point? _dragStartPoint; - private bool _cellSelectionDirty; private bool _suppressSelectionChangedCellClear; private Point? _lastDragCanvasPoint; private DispatcherTimer? _autoScrollTimer; @@ -50,6 +43,12 @@ public partial class TableView : ListView private double _autoScrollHorizontalDelta; private double _dragStartVerticalOffset; private double _dragStartHorizontalOffset; + private Pointer? _tableViewDragPointer; + private UIElement? _pointerCaptureElement; + private TableViewCellSlotRange? _lastDragSelectionCellRange; + private ItemIndexRange? _lastDragSelectionRowRange; + private bool _cellStateDispatchPending; + private readonly HashSet _pendingCellStateRows = []; /// /// Initializes a new instance of the TableView class. @@ -72,6 +71,8 @@ public TableView() Unloaded += OnUnloaded; SelectionChanged += TableView_SelectionChanged; _collectionView.ItemPropertyChanged += OnItemPropertyChanged; + AddHandler(PointerPressedEvent, new PointerEventHandler(OnAnyPointerPressed), handledEventsToo: true); + AddHandler(PointerReleasedEvent, new PointerEventHandler(OnAnyPointerReleased), handledEventsToo: true); } /// @@ -79,6 +80,8 @@ public TableView() /// private void TableView_SelectionChanged(object sender, SelectionChangedEventArgs e) { + TableViewTrace.Write($"TableViewSelectionChanged: AddedItems={e.AddedItems.Count}, RemovedItems={e.RemovedItems.Count}"); + if (_suppressSelectionChangedCellClear) { _suppressSelectionChangedCellClear = false; @@ -91,11 +94,13 @@ private void TableView_SelectionChanged(object sender, SelectionChangedEventArgs } else { - SelectedCellRanges.RemoveWhere(slots => + var addedIndexes = e.AddedItems.Select(item => Items.IndexOf(item)); + + foreach (var range in IndexRangeHelper.GetRanges(addedIndexes)) { - slots.RemoveWhere(slot => SelectedRanges.Any(range => range.IsInRange(slot.Row))); - return slots.Count == 0; - }); + var slotRange = TableViewCellSlotRange.FromCoordinates(range.FirstIndex, 0, range.LastIndex, Columns.VisibleColumns.Count - 1); + SubtractCellRangeFromSelection(slotRange); + } } CurrentCellSlot = null; @@ -108,6 +113,23 @@ private void TableView_SelectionChanged(object sender, SelectionChangedEventArgs } } + /// + /// Subtracts a specified cell range from the current selection. + /// + /// The cell range to subtract from the current selection. + private void SubtractCellRangeFromSelection(TableViewCellSlotRange slotRange) + { + while (SelectedCellRanges.FirstOrDefault(r => r.IntersectsWith(slotRange)) is { } intersectingRange) + { + foreach (var slicedRange in intersectingRange.Subtract(slotRange)) + { + SelectedCellRanges.Add(slicedRange); + } + + SelectedCellRanges.Remove(intersectingRange); + } + } + /// /// Handles the PropertyChanged event of an item in the TableView. /// @@ -185,6 +207,214 @@ protected override void OnKeyDown(KeyRoutedEventArgs e) HandleNavigations(e, shiftKey, ctrlKey); } + /// + /// Handles pointer-pressed for all cases, including when elements sets e.Handled = true. + /// + private void OnAnyPointerPressed(object sender, PointerRoutedEventArgs e) + { + var pointerPoint = e.GetCurrentPoint(this); + var position = pointerPoint.Position; + var canvasPoint = GetCanvasPoint(position); + var ctrlKey = KeyboardHelper.IsCtrlKeyDown(); + var isShiftkey = KeyboardHelper.IsShiftKeyDown(); + var orignalSoruce = e.OriginalSource as FrameworkElement; + + if (SelectionMode is ListViewSelectionMode.None // Skip selection when SelectionMode is None + || IsDragSelecting // Skip selection when a drag is already in progress + || orignalSoruce is ScrollBar // Skip selection when the pointer is over the ScrollBar + || orignalSoruce?.FindAscendant() is { } // Skip selection when the pointer is within a ScrollBar + || !pointerPoint.Properties.IsLeftButtonPressed // Skip selection when the left mouse button is not pressed + || canvasPoint is null // Skip selection when canvasPoint is null (e.g., pointer is outside the scroll canvas) + || canvasPoint.Value.Y < 0 // Skip selection when the pointer is in the column header area (above the scroll canvas) + || canvasPoint.Value.X < CellsHorizontalOffset // Skip selection when the pointer is in the row header area (to the left of the scroll canvas) + || isShiftkey) // Skip selection when the Shift key is held + { + return; + } + + _lastDragCanvasPoint = null; + CurrentCellSlot = null; + SelectionStartCellSlot = null; + SelectionStartRowIndex = null; + _lastDragSelectionRowRange = null; + _lastDragSelectionCellRange = null; + LastSelectionUnit = TableViewSelectionUnit.Row; + + if (!ctrlKey && SelectionMode is not ListViewSelectionMode.Multiple) + DeselectAll(); + + if (e.OriginalSource is UIElement element) + { + UIElement? clickedElement = element.FindAscendant(); // Check if the pointer is over a TableViewCell + clickedElement ??= element.FindAscendant(); // If not, check if the pointer is over a TableViewRow + clickedElement ??= this; // If not, default to the TableView itself + + SelectionStartCellSlot = (clickedElement as TableViewCell)?.Slot; + SelectionStartRowIndex = (clickedElement as TableViewRow)?.Index; + + LastSelectionUnit = SelectionUnit switch + { + TableViewSelectionUnit.Cell => TableViewSelectionUnit.Cell, + TableViewSelectionUnit.Row => TableViewSelectionUnit.Row, + _ => clickedElement is TableViewCell + ? TableViewSelectionUnit.Cell + : TableViewSelectionUnit.Row + }; + +#if WINDOWS + _pointerCaptureElement = clickedElement; +#else + _pointerCaptureElement = this; +#endif + + _pointerCaptureElement.CapturePointer(e.Pointer); + _tableViewDragPointer = e.Pointer; + StartDragSelection(canvasPoint.Value); + MakeSelectionInDragRect(); + } + } + + /// + protected override void OnPointerMoved(PointerRoutedEventArgs e) + { + base.OnPointerMoved(e); + + if (!IsDragSelecting) + { + return; + } + + var canvasPoint = GetCanvasPoint(e.GetCurrentPoint(this).Position); + if (canvasPoint is null) + { + return; + } + + // Drive the rect visual for all drag sources (cell-initiated drags bubble pointer events here). + UpdateDragRectangleVisual(canvasPoint.Value); + + // Selection-by-hit-test is only needed for TableView-initiated drags; cell-initiated + // drags perform selection in the cell's OnManipulationDelta via FindCell. + if (_tableViewDragPointer is not null) + { + MakeSelectionInDragRect(); + } + } + + /// + /// Makes selection based on the current drag rectangle, selecting either rows or cells depending on the last selection unit. + /// + private void MakeSelectionInDragRect() + { + if (_lastDragCanvasPoint is null) return; + + if (LastSelectionUnit is not TableViewSelectionUnit.Cell && GetRowIndexAtCanvasPoint(_lastDragCanvasPoint.Value) is int row) + { + SelectionStartRowIndex ??= row; + var minRow = Math.Min(SelectionStartRowIndex.Value, row); + var maxRow = Math.Max(SelectionStartRowIndex.Value, row); + var rows = new ItemIndexRange(minRow, (uint)(maxRow - minRow + 1)); + + SelectRowsInDragRect(rows); + } + else if (LastSelectionUnit is not TableViewSelectionUnit.Row && GetSlotAtCanvasPoint(_lastDragCanvasPoint.Value) is { } slot) + { + if (SelectionStartCellSlot is null) + { + var startColumn = slot.Column; + + if (_dragStartPoint is not null) + { + var horizontalScrollDelta = HorizontalOffset - _dragStartHorizontalOffset; + var startX = _dragStartPoint.Value.X - horizontalScrollDelta; + startColumn = GetColumnIndexAtCanvasX(startX) + ?? (startX < CellsHorizontalOffset - HorizontalOffset ? 0 : Columns.VisibleColumns.Count - 1); + } + + SelectionStartCellSlot = new(SelectionStartRowIndex ?? slot.Row, startColumn); + } + + var startRow = Math.Min(SelectionStartCellSlot.Value.Row, slot.Row); + var endRow = Math.Max(SelectionStartCellSlot.Value.Row, slot.Row); + var startCol = Math.Min(SelectionStartCellSlot.Value.Column, slot.Column); + var endCol = Math.Max(SelectionStartCellSlot.Value.Column, slot.Column); + var cells = TableViewCellSlotRange.FromCoordinates(startRow, startCol, endRow, endCol); + + SelectCellsInDragRect(cells); + } + else if (LastSelectionUnit is not TableViewSelectionUnit.Cell && _lastDragSelectionRowRange?.Length > 0) + { + DeselectRange(_lastDragSelectionRowRange); + + _lastDragSelectionRowRange = null; + SelectionStartRowIndex = null; + } + else if (LastSelectionUnit is not TableViewSelectionUnit.Row && _lastDragSelectionCellRange?.Length > 0) + { + DeselectCellRange(_lastDragSelectionCellRange); + + _lastDragSelectionCellRange = null; + SelectionStartCellSlot = null; + } + } + + /// + /// Selects rows that intersect with the current drag rectangle, updating the selection state accordingly. + /// + private void SelectRowsInDragRect(ItemIndexRange rows) + { + if (_lastDragSelectionRowRange?.FirstIndex == rows?.FirstIndex && _lastDragSelectionRowRange?.LastIndex == rows?.LastIndex) return; + + if (_lastDragSelectionRowRange is not null && rows is not null && _lastDragSelectionRowRange.Contains(rows)) + { + foreach (var slicedRange in _lastDragSelectionRowRange.Subtract(rows)) + { + DeselectRange(slicedRange); + } + } + else if (rows?.Length > 0) + { + SelectRange(rows); + } + + _lastDragSelectionRowRange = rows; + } + + /// + /// Selects cells that intersect with the current drag rectangle, updating the selection state accordingly. + /// + private void SelectCellsInDragRect(TableViewCellSlotRange cells) + { + if (_lastDragSelectionCellRange == cells) return; + + if (_lastDragSelectionCellRange is not null && cells is not null) + { + foreach (var range in _lastDragSelectionCellRange.Subtract(cells)) + { + SubtractCellRangeFromSelection(range); + } + } + + if (SelectedCellRanges.Any(r => r == cells)) + { + OnCellSelectionChanged(); + } + else if (cells?.Length > 0) + { + SelectCellRange(cells); + } + + _lastDragSelectionCellRange = cells; + } + + /// + /// Handles pointer-released for all cases, including when elements sets e.Handled = true. + /// + private void OnAnyPointerReleased(object sender, PointerRoutedEventArgs e) + { + EndDragSelection(); + } + /// /// Handles navigation keys. /// @@ -313,6 +543,9 @@ private int CalculateAvailablePageSize() return (int)Math.Floor(availableHeight / rowHeight); } + /// + /// Ends the editing of a cell, committing or canceling the edit based on the specified action. + /// internal bool EndCellEditing(TableViewEditAction editAction, TableViewCell cell) { var editingElement = cell.Content as FrameworkElement; @@ -573,8 +806,7 @@ internal void CopyToClipboardInternal(bool includeHeaders) { // Clipboard failures are normal on Windows (e.g., CLIPBRD_E_CANT_OPEN). // Swallow to avoid crashing the application. - Debug.WriteLine( - $"TableView: Clipboard.SetContent failed: {ex}"); + TableViewTrace.Write($"TableView: Clipboard.SetContent failed: {ex}"); } } @@ -1053,7 +1285,7 @@ private void SelectAllCells() if (Items.Count > 0 && Columns.VisibleColumns.Count > 0) { SelectedCellRanges.Clear(); - SelectedCellRanges.Add([new TableViewCellSlot(0, 0)]); + SelectedCellRanges.Add(TableViewCellSlotRange.FromSlots(new(0, 0))); } break; case ListViewSelectionMode.Multiple: @@ -1145,7 +1377,7 @@ internal void MakeSelection(TableViewCellSlot slot, bool shiftKey, bool ctrlKey else { if (SelectionUnit is TableViewSelectionUnit.CellWithRow) - { + { SelectRows(slot, shiftKey, ctrlKey); } else if (!ctrlKey) @@ -1246,17 +1478,15 @@ private void SelectCells(TableViewCellSlot slot, bool shiftKey, bool ctrlKey) } } - var selectionRange = (SelectionStartCellSlot is null ? null : SelectedCellRanges.LastOrDefault(x => SelectionStartCellSlot.HasValue && x.Contains(SelectionStartCellSlot.Value))) ?? []; + var selectionRange = (SelectionStartCellSlot is null ? null : SelectedCellRanges.LastOrDefault(x => SelectionStartCellSlot.HasValue && x.Contains(SelectionStartCellSlot.Value.Row, SelectionStartCellSlot.Value.Column))); if (ctrlKey && SelectionMode is ListViewSelectionMode.Multiple or ListViewSelectionMode.Extended) { - selectionRange = SelectedCellRanges.SelectMany(x => x).ToHashSet(); - SelectedCellRanges.Clear(); + // Keep existing ranges; the new slot/range will be added alongside them. } else { - SelectedCellRanges.Remove(selectionRange); - selectionRange.Clear(); + SelectedCellRanges.Remove(selectionRange!); } SelectionStartCellSlot ??= CurrentCellSlot; @@ -1264,36 +1494,14 @@ private void SelectCells(TableViewCellSlot slot, bool shiftKey, bool ctrlKey) if (shiftKey && SelectionMode is ListViewSelectionMode.Multiple or ListViewSelectionMode.Extended) { - var currentSlot = SelectionStartCellSlot.Value; - var startRow = Math.Min(slot.Row, currentSlot.Row); - var endRow = Math.Max(slot.Row, currentSlot.Row); - var startCol = Math.Min(slot.Column, currentSlot.Column); - var endCol = Math.Max(slot.Column, currentSlot.Column); - for (var row = startRow; row <= endRow; row++) - { - for (var column = startCol; column <= endCol; column++) - { - var nextSlot = new TableViewCellSlot(row, column); - selectionRange.Add(nextSlot); - if (SelectedCellRanges.LastOrDefault(x => x.Contains(nextSlot)) is { } range) - { - range.Remove(nextSlot); - } - } - } + var newRange = TableViewCellSlotRange.FromSlots(SelectionStartCellSlot.Value, slot); + SelectedCellRanges.Add(newRange); } else { SelectionStartCellSlot = slot; - selectionRange.Add(slot); - - if (SelectedCellRanges.LastOrDefault(x => x.Contains(slot)) is { } range) - { - range.Remove(slot); - } + SelectedCellRanges.Add(TableViewCellSlotRange.FromSlots(slot)); } - - SelectedCellRanges.Add(selectionRange); OnCellSelectionChanged(); CurrentCellSlot = slot; } @@ -1303,10 +1511,8 @@ private void SelectCells(TableViewCellSlot slot, bool shiftKey, bool ctrlKey) /// internal void DeselectCell(TableViewCellSlot slot) { - var selectionRange = SelectedCellRanges.LastOrDefault(x => x.Contains(slot)); - selectionRange?.Remove(slot); - - if (selectionRange?.Count == 0) + var selectionRange = SelectedCellRanges.LastOrDefault(x => x.Contains(slot.Row, slot.Column)); + if (selectionRange is not null) { SelectedCellRanges.Remove(selectionRange); } @@ -1315,6 +1521,49 @@ internal void DeselectCell(TableViewCellSlot slot) OnCellSelectionChanged(); } + /// + /// Selects all the cells within the specified range, raising the event only once. + /// + /// The range of cell slots to select. + public void SelectCellRange(TableViewCellSlotRange? range) + { + if (range is null || range.Length <= 0 + || !range.IsValid(this) + || SelectionMode is ListViewSelectionMode.None + || SelectionUnit is TableViewSelectionUnit.Row) + { + return; + } + + if (SelectedCellRanges.Any(x => x == range)) return; + + if (SelectionUnit is TableViewSelectionUnit.CellWithRow) + { + _suppressSelectionChangedCellClear = true; + var rowRange = new ItemIndexRange(range.FirstRow, (uint)range.Rows); + SelectRange(rowRange); + } + + SubtractCellRangeFromSelection(range); + SelectedCellRanges.Add(range); + OnCellSelectionChanged(); + } + + /// + /// Deselects all the cells within the specified range, raising the event only once. + /// + /// The range of cell slots to deselect. + public void DeselectCellRange(TableViewCellSlotRange? range) + { + if (range is null || range.Length <= 0 || SelectedCellRanges.Count is 0) + { + return; + } + + SubtractCellRangeFromSelection(range); + OnCellSelectionChanged(); + } + /// /// Handles changes to the current cell in the table view. /// @@ -1353,43 +1602,38 @@ private async Task OnCurrentCellChanged(TableViewCellSlot? oldSlot, TableViewCel /// private void OnCellSelectionChanged() { - if (_cellSelectionDirty) return; - _cellSelectionDirty = true; + var newSelection = SelectedCellRanges.SelectMany(x => x.GetSlots()).ToHashSet(); + var removedCells = SelectedCells.Where(s => !newSelection.Contains(s)).ToList(); + var addedCells = newSelection.Where(s => !SelectedCells.Contains(s)).ToList(); - if (!DispatcherQueue.TryEnqueue(() => - { - _cellSelectionDirty = false; + if (removedCells.Count is 0 && addedCells.Count is 0) return; - var oldSelection = SelectedCells; - SelectedCells = [.. SelectedCellRanges.SelectMany(x => x)]; + foreach (var slot in removedCells) SelectedCells.Remove(slot); + foreach (var slot in addedCells) SelectedCells.Add(slot); - var rowIndexes = oldSelection.Select(x => x.Row).Concat(SelectedCells.Select(x => x.Row)).Distinct(); + OnCellSelectionChanged(new TableViewCellSelectionChangedEventArgs(removedCells, addedCells)); - foreach (var rowIndex in rowIndexes) - { - var row = _rows.FirstOrDefault(x => x.Index == rowIndex); - row?.ApplyCellsSelectionState(); - } + foreach (var slot in removedCells.Concat(addedCells)) + _pendingCellStateRows.Add(slot.Row); - InvokeCellSelectionChangedEvent(oldSelection); - })) + if (!_cellStateDispatchPending) { - _cellSelectionDirty = false; + _cellStateDispatchPending = true; + DispatcherQueue.TryEnqueue(ApplyPendingCellStates); } } - /// - /// Invokes the event to notify subscribers of changes in the selected cells. - /// - private void InvokeCellSelectionChangedEvent(HashSet oldSelection) + private void ApplyPendingCellStates() { - var removedCells = oldSelection.Except(SelectedCells).ToList(); - var addedCells = SelectedCells.Except(oldSelection).ToList(); + _cellStateDispatchPending = false; + if (_pendingCellStateRows.Count is 0) return; - if (removedCells.Count > 0 || addedCells.Count > 0) + foreach (var row in _rows) { - OnCellSelectionChanged(new TableViewCellSelectionChangedEventArgs(removedCells, addedCells)); + if (_pendingCellStateRows.Contains(row.Index)) + row.ApplyCellsSelectionState(); } + _pendingCellStateRows.Clear(); } /// @@ -1414,10 +1658,7 @@ internal void StartDragSelection(Point startPoint) _dragStartVerticalOffset = _scrollViewer?.VerticalOffset ?? 0; _dragStartHorizontalOffset = HorizontalOffset; - if (_scrollViewer is not null) - { - _scrollViewer.ViewChanged += OnScrollViewerViewChangedDuringDrag; - } + _scrollViewer?.ViewChanged += OnScrollViewerViewChangedDuringDrag; // Show the drag rectangle visual if enabled and template parts are available if (ShowDragRectangle && DragRectangleCanvas is not null && _dragRectangle is not null) @@ -1455,6 +1696,26 @@ internal void UpdateDragRectangleVisual(Point currentPoint) UpdateAutoScroll(currentPoint); } + /// + /// Transforms a point relative to this into coordinates relative to the . + /// Returns null when the canvas is unavailable or the transform cannot be computed. + /// A negative Y value indicates the point is above the scroll area (column header territory). + /// + /// The position relative to this TableView. + /// The canvas-relative point, or null if unavailable. + private Point? GetCanvasPoint(Point position) + { + if (DragRectangleCanvas is null) return null; + try + { + return TransformToVisual(DragRectangleCanvas).TransformPoint(position); + } + catch (ArgumentException) + { + return null; + } + } + /// /// Positions the drag rectangle visual from the scroll-adjusted start point to the current point, /// so the rectangle follows the mouse and extends naturally when content scrolls. @@ -1597,8 +1858,6 @@ private void OnAutoScrollTimerTick(object? sender, object e) { PositionDragRectangle(_lastDragCanvasPoint.Value); } - - SelectCellAtDragPoint(); } } @@ -1627,54 +1886,6 @@ private void OnScrollViewerViewChangedDuringDrag(object? sender, ScrollViewerVie { PositionDragRectangle(_lastDragCanvasPoint.Value); } - - // Update selection for newly visible rows during auto-scroll - SelectCellAtDragPoint(); - } - - /// - /// Selects the cell at the last known drag pointer position. - /// Used during auto-scroll to select newly visible cells when the pointer isn't moving. - /// - private void SelectCellAtDragPoint() - { - if (_scrollViewer is null || _lastDragCanvasPoint is null || DragRectangleCanvas is null) - { - return; - } - - // Clamp to the cell area within the viewport. - // CellsHorizontalOffset accounts for row headers so we don't hit-test on header area. - var canvasPoint = _lastDragCanvasPoint.Value; - var minX = CellsHorizontalOffset + 1; - var clampedPoint = new Point( - Math.Clamp(canvasPoint.X, minX, Math.Max(minX, _scrollViewer.ViewportWidth - 1)), - Math.Clamp(canvasPoint.Y, 1, Math.Max(1, _scrollViewer.ViewportHeight - 1))); - - try - { - var screenPoint = DragRectangleCanvas.TransformToVisual(null).TransformPoint(clampedPoint); -#if WINDOWS - var cell = VisualTreeHelper.FindElementsInHostCoordinates(screenPoint, _scrollViewer) -#else - var cell = VisualTreeHelper.FindElementsInHostCoordinates(screenPoint, _scrollViewer, true) - .OfType() - .Where(x => x.Name is "Content") - .Select(x => x.FindAscendant() is { } c ? c : default) -#endif - .OfType() - .FirstOrDefault(); - - if (cell is not null && cell.Slot != CurrentCellSlot) - { - var ctrlKey = KeyboardHelper.IsCtrlKeyDown(); - MakeSelection(cell.Slot, true, ctrlKey); - } - } - catch (ArgumentException) - { - // Element not in visual tree during container recycling - } } /// @@ -1686,32 +1897,45 @@ internal async void EndDragSelection() StopAutoScroll(); - if (_scrollViewer is not null) + _pointerCaptureElement?.ReleasePointerCaptures(); + _tableViewDragPointer = null; + + _scrollViewer?.ViewChanged -= OnScrollViewerViewChangedDuringDrag; + _dragRectangle?.Visibility = Visibility.Collapsed; + + // Determine the corner of the selection nearest the pointer before clearing drag state. + TableViewCellSlot? endSlot = null; + if (_lastDragSelectionCellRange is { Length: > 0 } endRange && _dragStartPoint is not null && _lastDragCanvasPoint is not null) { - _scrollViewer.ViewChanged -= OnScrollViewerViewChangedDuringDrag; + var verticalScrollDelta = (_scrollViewer?.VerticalOffset ?? 0) - _dragStartVerticalOffset; + var horizontalScrollDelta = HorizontalOffset - _dragStartHorizontalOffset; + var rowsTopToBottom = _dragStartPoint.Value.Y - verticalScrollDelta <= _lastDragCanvasPoint.Value.Y; + var colsLeftToRight = _dragStartPoint.Value.X - horizontalScrollDelta <= _lastDragCanvasPoint.Value.X; + endSlot = new TableViewCellSlot( + rowsTopToBottom ? endRange.LastRow : endRange.FirstRow, + colsLeftToRight ? endRange.LastColumn : endRange.FirstColumn); } - if (_dragRectangle is not null) + // Clean up any pointer capture the TableView itself held (empty-space drag path). + if (_tableViewDragPointer is not null) { - _dragRectangle.Visibility = Visibility.Collapsed; + _tableViewDragPointer = null; + ReleasePointerCaptures(); } IsDragSelecting = false; _dragStartPoint = null; _lastDragCanvasPoint = null; + SelectionStartCellSlot = null; // Restore focus and scroll to the current cell now that dragging has ended - try + if (endSlot?.IsValid(this) == true) { - if (CurrentCellSlot.HasValue) - { - var cell = await ScrollCellIntoView(CurrentCellSlot.Value); - cell?.ApplyCurrentCellState(); - } + CurrentCellSlot = endSlot.Value; } - catch (Exception) + else if (_lastDragSelectionCellRange?.Length > 0 && _lastDragSelectionCellRange.LastSlot.IsValid(this)) { - // Focus restoration is best-effort after drag ends + CurrentCellSlot = _lastDragSelectionCellRange.LastSlot; } } @@ -1828,6 +2052,112 @@ void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e) return slot.IsValid(this) && ContainerFromIndex(slot.Row) is TableViewRow row ? row.Cells[slot.Column] : default; } + /// + /// Returns the index of the row that contains , or the nearest row + /// within the vertical span between the drag start point and when + /// the point falls in empty space. Returns null when no realized row falls in that span. + /// + private int? GetRowIndexAtCanvasPoint(Point canvasPoint) + { + if (DragRectangleCanvas is null) return null; + + // Compute the vertical span of the drag so we can snap to the nearest in-span row when the + // pointer is in empty space. If there is no drag start (called outside a drag), minY == maxY + // == canvasPoint.Y, which collapses back to the original exact hit-test behaviour. + var verticalScrollDelta = (_scrollViewer?.VerticalOffset ?? 0) - _dragStartVerticalOffset; + var adjustedStartY = _dragStartPoint is not null + ? _dragStartPoint.Value.Y - verticalScrollDelta + : canvasPoint.Y; + + var minY = Math.Min(adjustedStartY, canvasPoint.Y); + var maxY = Math.Max(adjustedStartY, canvasPoint.Y); + + TableViewRow? nearestRow = null; + var nearestDistance = double.MaxValue; + + foreach (var row in _rows) + { + Point rowOrigin; + try { rowOrigin = row.TransformToVisual(DragRectangleCanvas).TransformPoint(default); } + catch (ArgumentException) { continue; } + + var rowTop = rowOrigin.Y; + var rowBottom = rowTop + row.ActualHeight; + + if (rowBottom <= minY || rowTop >= maxY) continue; + + var distance = Math.Max(0d, Math.Max(rowTop - canvasPoint.Y, canvasPoint.Y - rowBottom)); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestRow = row; + } + } + + return nearestRow?.Index; + } + + /// + /// Returns the index of the visible column whose bounds contain the given canvas X coordinate. + /// Returns null when x falls outside the column area or there are no visible columns. + /// + private int? GetColumnIndexAtCanvasX(double x) + { + var columnLeft = CellsHorizontalOffset - HorizontalOffset; + for (var i = 0; i < Columns.VisibleColumns.Count; i++) + { + var columnRight = columnLeft + Columns.VisibleColumns[i].ActualWidth; + if (x >= columnLeft && x < columnRight) + return i; + columnLeft = columnRight; + } + return null; + } + + /// + /// Resolves the cell slot at , snapping to the nearest row and + /// column within the horizontal and vertical span of the current drag when the point falls in + /// empty space. Returns null when no realized row or visible column falls in that span. + /// + private TableViewCellSlot? GetSlotAtCanvasPoint(Point canvasPoint) + { + if (DragRectangleCanvas is null) return null; + + if (GetRowIndexAtCanvasPoint(canvasPoint) is not int rowIndex) return null; + + // Mirror the row snapping: find the nearest column within the horizontal drag span. + var horizontalScrollDelta = HorizontalOffset - _dragStartHorizontalOffset; + var adjustedStartX = _dragStartPoint is not null + ? _dragStartPoint.Value.X - horizontalScrollDelta + : canvasPoint.X; + + var minX = Math.Min(adjustedStartX, canvasPoint.X); + var maxX = Math.Max(adjustedStartX, canvasPoint.X); + + var nearestColIndex = -1; + var nearestDistance = double.MaxValue; + var columnLeft = CellsHorizontalOffset - HorizontalOffset; + + for (var i = 0; i < Columns.VisibleColumns.Count; i++) + { + var columnRight = columnLeft + Columns.VisibleColumns[i].ActualWidth; + + if (columnRight > minX && columnLeft < maxX) + { + var distance = Math.Max(0d, Math.Max(columnLeft - canvasPoint.X, canvasPoint.X - columnRight)); + if (distance < nearestDistance) + { + nearestDistance = distance; + nearestColIndex = i; + } + } + + columnLeft = columnRight; + } + + return nearestColIndex == -1 ? null : new TableViewCellSlot(rowIndex, nearestColIndex); + } + /// /// Gets the columns currently in view. /// diff --git a/src/TableViewCell.cs b/src/TableViewCell.cs index 523f8a46..d1955299 100644 --- a/src/TableViewCell.cs +++ b/src/TableViewCell.cs @@ -5,10 +5,6 @@ using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Shapes; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Windows.Foundation; using WinUI.TableView.Extensions; using WinUI.TableView.Helpers; @@ -29,7 +25,6 @@ namespace WinUI.TableView; #endif public partial class TableViewCell : ContentControl { - private ScrollViewer? _scrollViewer; private ContentPresenter? _contentPresenter; private Border? _selectionBorder; private Rectangle? _v_gridLine; @@ -230,12 +225,6 @@ protected override void OnTapped(TappedRoutedEventArgs e) e.Handled = true; return; } - - if (TableView?.CurrentCellSlot != Slot || TableView?.LastSelectionUnit is TableViewSelectionUnit.Row) - { - MakeSelection(); - e.Handled = true; - } } /// @@ -248,76 +237,15 @@ protected override void OnPointerPressed(PointerRoutedEventArgs e) e.Handled = true; return; } - - if (!KeyboardHelper.IsShiftKeyDown() && TableView is not null) - { - TableView.SelectionStartCellSlot = TableView.SelectionUnit is not TableViewSelectionUnit.Row || !IsReadOnly ? Slot : default; - TableView.SelectionStartRowIndex = Index; - CapturePointer(e.Pointer); - - // Start drag selection (auto-scroll + optional rectangle visual) - var point = e.GetCurrentPoint(this).Position; - var canvasPoint = TransformPointToCanvas(point); - if (canvasPoint.HasValue) - { - TableView.StartDragSelection(canvasPoint.Value); - } - } } - /// protected override void OnPointerReleased(PointerRoutedEventArgs e) { base.OnPointerReleased(e); - if (!KeyboardHelper.IsShiftKeyDown() && TableView is not null) - { - var cell = FindCell(e.GetCurrentPoint(this).Position); - TableView.SelectionStartCellSlot = TableView.SelectionUnit is not TableViewSelectionUnit.Row || !IsReadOnly ? cell?.Slot : default; - TableView.SelectionStartRowIndex = cell?.Slot.Row; - } - - TableView?.EndDragSelection(); - ReleasePointerCaptures(); - - e.Handled = true; - } - - /// - protected override void OnPointerCaptureLost(PointerRoutedEventArgs e) - { - base.OnPointerCaptureLost(e); - - TableView?.EndDragSelection(); - } - - /// - protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e) - { - base.OnManipulationDelta(e); - - if (PointerCaptures?.Any() is true) + if(TableView?.SelectionUnit is not TableViewSelectionUnit.Row) { - // Update drag rectangle visual and auto-scroll - if (TableView?.IsDragSelecting is true) - { - var canvasPoint = TransformPointToCanvas(e.Position); - if (canvasPoint.HasValue) - { - TableView.UpdateDragRectangleVisual(canvasPoint.Value); - } - } - - // Selection via FindCell — same proven path whether rectangle is on or off. - // When the pointer is outside the viewport, FindCell returns null and selection - // is updated by the ViewChanged handler on the next auto-scroll tick. - var cell = FindCell(e.Position); - - if (cell is not null && cell.Slot != TableView?.CurrentCellSlot) - { - var ctrlKey = KeyboardHelper.IsCtrlKeyDown(); - TableView?.MakeSelection(cell.Slot, true, ctrlKey); - } + e.Handled = true; } } @@ -350,45 +278,6 @@ private double GetHorizontalGridlineHeight() ? TableView.HorizontalGridLinesStrokeThickness : 0d; } - /// - /// Finds the cell at the specified position. - /// - private TableViewCell? FindCell(Point position) - { - _scrollViewer ??= TableView?.FindDescendant(); - if (_scrollViewer is null) return null; - - var transformedPoint = TransformToVisual(null).TransformPoint(position); -#if WINDOWS - return VisualTreeHelper.FindElementsInHostCoordinates(transformedPoint, _scrollViewer) -#else - return VisualTreeHelper.FindElementsInHostCoordinates(transformedPoint, _scrollViewer, true) - .OfType() - .Where(x => x.Name is "Content") - .Select(x => x.FindAscendant() is { } header ? header : default) -#endif - .OfType() - .FirstOrDefault(); - } - - /// - /// Transforms a point relative to this cell to coordinates relative to the drag rectangle canvas. - /// - private Point? TransformPointToCanvas(Point position) - { - if (TableView?.DragRectangleCanvas is null) return null; - - try - { - var transform = TransformToVisual(TableView.DragRectangleCanvas); - return transform.TransformPoint(position); - } - catch (ArgumentException) - { - return null; - } - } - /// protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e) { @@ -400,49 +289,7 @@ protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e) base.OnDoubleTapped(e); - if (!IsReadOnly && TableView is not null && !TableView.IsEditing && !Column?.UseSingleElement is true) - { - e.Handled = BeginCellEditing(e); - } - else - { - e.Handled = true; - } - } - - /// - /// Makes a selection based on the current cell. - /// - private void MakeSelection() - { - var shiftKey = KeyboardHelper.IsShiftKeyDown(); - var ctrlKey = KeyboardHelper.IsCtrlKeyDown(); - - if (TableView is null || Column is null) - { - return; - } - - if ((TableView.IsEditing || Column.UseSingleElement) && IsCurrent) - { - return; - } - - if (IsSelected && (ctrlKey || TableView.SelectionMode is ListViewSelectionMode.Multiple) && !shiftKey) - { - TableView.DeselectCell(Slot); - } - else - { - if (Column.UseSingleElement) - { - TableView.DeselectCell(Slot); - } - - TableView.MakeSelection(Slot, shiftKey, ctrlKey); - } - - TableView.SetIsEditing(false); + e.Handled = IsReadOnly || TableView is null || TableView.IsEditing || !Column?.UseSingleElement is not true || BeginCellEditing(e); } /// diff --git a/src/TableViewCellSlot.cs b/src/TableViewCellSlot.cs index 980ef25d..688604cd 100644 --- a/src/TableViewCellSlot.cs +++ b/src/TableViewCellSlot.cs @@ -3,4 +3,10 @@ /// /// Represents a slot of a TableView cell, identified by its row and column indices. /// -public readonly record struct TableViewCellSlot(int Row, int Column); +public readonly record struct TableViewCellSlot(int Row, int Column) +{ + public override string ToString() + { + return $"Row: {Row}, Col: {Column}"; + } +} diff --git a/src/TableViewCellSlotRange.cs b/src/TableViewCellSlotRange.cs new file mode 100644 index 00000000..f16fa8a1 --- /dev/null +++ b/src/TableViewCellSlotRange.cs @@ -0,0 +1,131 @@ +namespace WinUI.TableView; + +/// +/// Represents a coordinate-based range of cell slots in a TableView. +/// +public class TableViewCellSlotRange +{ + /// + /// Gets the starting row index of the range. + /// + public int FirstRow { get; } + + /// + /// Gets the starting column index of the range. + /// + public int FirstColumn { get; } + + /// + /// Gets the first cell slot in the range. + /// + public TableViewCellSlot FirstSlot => new(FirstRow, FirstColumn); + + /// + /// Gets the number of rows spanned by this range. + /// + public int Rows { get; } + + /// + /// Gets the number of columns spanned by this range. + /// + public int Columns { get; } + + /// + /// Gets the total number of cell slots in the range. + /// + public int Length => Rows * Columns; + + /// + /// Gets the last row index included in the range. + /// + public int LastRow => FirstRow + Rows - 1; + + /// + /// Gets the last column index included in the range. + /// + public int LastColumn => FirstColumn + Columns - 1; + + /// + /// Gets the last cell slot in the range. + /// + public TableViewCellSlot LastSlot => new(LastRow, LastColumn); + + /// + /// Initializes a new instance of the TableViewCellSlotRange class using starting indices and dimensions. + /// + public TableViewCellSlotRange(int firstRowIndex, int firstColumnIndex, int rowCount, int columnCount) + { + if (firstRowIndex < 0) + throw new ArgumentOutOfRangeException(nameof(firstRowIndex), "Index cannot be negative."); + + if (firstColumnIndex < 0) + throw new ArgumentOutOfRangeException(nameof(firstColumnIndex), "Index cannot be negative."); + + if (rowCount < 1) + throw new ArgumentOutOfRangeException(nameof(rowCount), "Count must be at least 1."); + if (columnCount < 1) + throw new ArgumentOutOfRangeException(nameof(columnCount), "Count must be at least 1."); + + FirstRow = firstRowIndex; + FirstColumn = firstColumnIndex; + Rows = rowCount; + Columns = columnCount; + } + + /// + /// Helper factory to initialize using start and end coordinates directly. + /// + public static TableViewCellSlotRange FromCoordinates(int startRow, int startCol, int endRow, int endCol) + { + var rowCount = Math.Abs(endRow - startRow) + 1; + var colCount = Math.Abs(endCol - startCol) + 1; + + return new TableViewCellSlotRange(startRow, startCol, rowCount, colCount); + } + + /// + /// Helper factory to initialize using two TableViewCellSlot instances. + /// + public static TableViewCellSlotRange FromSlots(TableViewCellSlot firstSlot, TableViewCellSlot? lastSlot = default) + { + lastSlot ??= firstSlot; + return FromCoordinates(firstSlot.Row, firstSlot.Column, lastSlot.Value.Row, lastSlot.Value.Column); + } + + /// + public override bool Equals(object? obj) + { + if (obj is TableViewCellSlotRange other) + { + return FirstRow == other.FirstRow && + FirstColumn == other.FirstColumn && + Rows == other.Rows && + Columns == other.Columns; + } + return false; + } + + /// + public static bool operator ==(TableViewCellSlotRange? left, TableViewCellSlotRange? right) + { + return Equals(left, right); + } + + /// + public static bool operator !=(TableViewCellSlotRange? left, TableViewCellSlotRange? right) + { + return !Equals(left, right); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(FirstRow, FirstColumn, Rows, Columns); + } + + /// + public override string ToString() + { + return $"[{FirstSlot}]..[{LastSlot}] ({Length})"; + } +} \ No newline at end of file diff --git a/src/TableViewRow.cs b/src/TableViewRow.cs index b2fe947d..8bc68aac 100644 --- a/src/TableViewRow.cs +++ b/src/TableViewRow.cs @@ -156,44 +156,6 @@ protected override void OnContentChanged(object oldContent, object newContent) TableView?.EnsureAlternateRowColors(); } - /// - protected override void OnPointerPressed(PointerRoutedEventArgs e) - { - if (TableView is { IsEditing: false }) - { - base.OnPointerPressed(e); - } - - if (!KeyboardHelper.IsShiftKeyDown() && TableView is not null) - { - TableView.SelectionStartRowIndex = Index; - } - } - - /// - protected override void OnPointerReleased(PointerRoutedEventArgs e) - { - base.OnPointerReleased(e); - - if (!KeyboardHelper.IsShiftKeyDown() && TableView is not null) - { - TableView.SelectionStartCellSlot = null; - TableView.SelectionStartRowIndex = Index; - } - } - - /// - protected override void OnTapped(TappedRoutedEventArgs e) - { - base.OnTapped(e); - - if (TableView?.SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow or TableViewSelectionUnit.CellWithRow) - { - TableView.CurrentRowIndex = Index; - TableView.LastSelectionUnit = TableViewSelectionUnit.Row; - } - } - /// protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e) { diff --git a/tests/DragSelectionRectangleTests.cs b/tests/DragSelectionRectangleTests.cs index a85a3ace..805fddca 100644 --- a/tests/DragSelectionRectangleTests.cs +++ b/tests/DragSelectionRectangleTests.cs @@ -192,4 +192,40 @@ public void UpdateDragRectangleVisual_DoesNotCrash_WhenNotDragging() Assert.IsFalse(tv.IsDragSelecting); } + + [UITestMethod] + public async Task SelectCellRange_SelectsTheProvidedRangeAndRaisesOneSelectionChangedEvent() + { + var tv = await CreateAndLoadTableView(); + var eventCount = 0; + tv.CellSelectionChanged += (_, _) => eventCount++; + + tv.SelectCellRange(TableViewCellSlotRange.FromCoordinates(0, 0, 1, 1)); + + Assert.AreEqual(4, tv.SelectedCells.Count); + Assert.IsTrue(tv.SelectedCells.Contains(new TableViewCellSlot(0, 0))); + Assert.IsTrue(tv.SelectedCells.Contains(new TableViewCellSlot(0, 1))); + Assert.IsTrue(tv.SelectedCells.Contains(new TableViewCellSlot(1, 0))); + Assert.IsTrue(tv.SelectedCells.Contains(new TableViewCellSlot(1, 1))); + Assert.AreEqual(1, eventCount); + + await UnitTestApp.Current.MainWindow.UnloadTestContentAsync(tv); + } + + [UITestMethod] + public async Task DeselectCellRange_DeselectsTheProvidedRangeAndRaisesOneSelectionChangedEvent() + { + var tv = await CreateAndLoadTableView(); + tv.SelectCellRange(TableViewCellSlotRange.FromCoordinates(0, 0, 1, 1)); + + var eventCount = 0; + tv.CellSelectionChanged += (_, _) => eventCount++; + + tv.DeselectCellRange(TableViewCellSlotRange.FromCoordinates(0, 0, 1, 1)); + + Assert.AreEqual(0, tv.SelectedCells.Count); + Assert.AreEqual(1, eventCount); + + await UnitTestApp.Current.MainWindow.UnloadTestContentAsync(tv); + } } diff --git a/tests/TableViewSelectionUnitTests.cs b/tests/TableViewSelectionUnitTests.cs index 2b1009c0..47bc062f 100644 --- a/tests/TableViewSelectionUnitTests.cs +++ b/tests/TableViewSelectionUnitTests.cs @@ -4,6 +4,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; using System.Threading.Tasks; +using Windows.Foundation; namespace WinUI.TableView.Tests; @@ -15,10 +16,10 @@ public async Task CellWithRow_CellClickSelectsCellAndOwningRow() { var tableView = await CreateTableViewAsync(TableViewSelectionUnit.CellWithRow); - tableView.MakeSelection(new TableViewCellSlot(1, 0), false, false); + tableView.SelectCellRange(TableViewCellSlotRange.FromSlots(new TableViewCellSlot(1, 0), new TableViewCellSlot(1, 0))); await Task.Yield(); // Allow selection to propagate - + Assert.IsTrue(tableView.SelectedCells.Contains(new TableViewCellSlot(1, 0))); Assert.AreEqual(1, tableView.SelectedItems.Count); Assert.AreSame(tableView.Items[1], tableView.SelectedItem); @@ -29,7 +30,7 @@ public async Task CellAndRow_RowHeaderClickSelectsOnlyRow() { var tableView = await CreateTableViewAsync(TableViewSelectionUnit.CellWithRow); - tableView.MakeSelection(new TableViewCellSlot(1, -1), false, false); + tableView.SelectRange(new ItemIndexRange(1, 1)); Assert.AreEqual(0, tableView.SelectedCells.Count); Assert.AreEqual(1, tableView.SelectedItems.Count); @@ -41,11 +42,11 @@ public async Task CellSelectionUnitStillSelectsOnlyCells() { var tableView = await CreateTableViewAsync(TableViewSelectionUnit.Cell); - tableView.MakeSelection(new TableViewCellSlot(0, 0), false, false); + tableView.SelectCellRange(TableViewCellSlotRange.FromSlots(new TableViewCellSlot(0, 0), new TableViewCellSlot(0, 0))); await Task.Yield(); // Allow selection to propagate - tableView.MakeSelection(new TableViewCellSlot(1, 1), false, true); + tableView.SelectCellRange(TableViewCellSlotRange.FromSlots(new TableViewCellSlot(1, 1), new TableViewCellSlot(1, 1))); await Task.Yield(); // Allow selection to propagate @@ -60,10 +61,10 @@ public async Task CellOrRowSelectionUnitStillUsesCellAndRowSemantics() { var tableView = await CreateTableViewAsync(TableViewSelectionUnit.CellOrRow); - tableView.MakeSelection(new TableViewCellSlot(1, 0), false, false); + tableView.SelectCellRange(TableViewCellSlotRange.FromSlots(new TableViewCellSlot(1, 0), new TableViewCellSlot(1, 0))); Assert.AreEqual(0, tableView.SelectedItems.Count); - tableView.MakeSelection(new TableViewCellSlot(0, -1), false, false); + tableView.SelectRange(new ItemIndexRange(0, 1)); Assert.AreEqual(1, tableView.SelectedItems.Count); Assert.AreEqual(0, tableView.SelectedCells.Count); } @@ -73,11 +74,11 @@ public async Task CellWithRow_MultiSelectionAddsCellAndRowSelections() { var tableView = await CreateTableViewAsync(TableViewSelectionUnit.CellWithRow, ListViewSelectionMode.Multiple); - tableView.MakeSelection(new TableViewCellSlot(0, 0), false, false); + tableView.SelectCellRange(TableViewCellSlotRange.FromSlots(new TableViewCellSlot(0, 0), new TableViewCellSlot(0, 0))); await Task.Yield(); // Allow selection to propagate - tableView.MakeSelection(new TableViewCellSlot(1, 1), false, true); + tableView.SelectCellRange(TableViewCellSlotRange.FromSlots(new TableViewCellSlot(1, 1), new TableViewCellSlot(1, 1))); await Task.Delay(200); // Allow selection to propagate From 0bc96a9d33ade4c03602282d65c456e85edfaf4d Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Thu, 30 Jul 2026 03:02:57 +0500 Subject: [PATCH 02/11] fix: add missing XML doc on TableViewCellSlot.ToString() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CS1591 build error — publicly visible override was missing an inheritdoc comment. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/TableViewCellSlot.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/TableViewCellSlot.cs b/src/TableViewCellSlot.cs index 688604cd..093ce5d2 100644 --- a/src/TableViewCellSlot.cs +++ b/src/TableViewCellSlot.cs @@ -5,6 +5,7 @@ /// public readonly record struct TableViewCellSlot(int Row, int Column) { + /// public override string ToString() { return $"Row: {Row}, Col: {Column}"; From 954a98e884cd51bc34a7f4ffbec746b6bd2928b0 Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Thu, 30 Jul 2026 04:39:38 +0500 Subject: [PATCH 03/11] Improve drag rectangle visibility and selection logic --- src/TableView.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/TableView.cs b/src/TableView.cs index 4557cf8a..ae39936b 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -247,6 +247,10 @@ private void OnAnyPointerPressed(object sender, PointerRoutedEventArgs e) { UIElement? clickedElement = element.FindAscendant(); // Check if the pointer is over a TableViewCell clickedElement ??= element.FindAscendant(); // If not, check if the pointer is over a TableViewRow + + // Skip selection when the pointer is not over a Cell or Row, and ShowDragRectangle is false. + if (clickedElement == null && !ShowDragRectangle) return; + clickedElement ??= this; // If not, default to the TableView itself SelectionStartCellSlot = (clickedElement as TableViewCell)?.Slot; @@ -1661,7 +1665,7 @@ internal void StartDragSelection(Point startPoint) _scrollViewer?.ViewChanged += OnScrollViewerViewChangedDuringDrag; // Show the drag rectangle visual if enabled and template parts are available - if (ShowDragRectangle && DragRectangleCanvas is not null && _dragRectangle is not null) + if (DragRectangleCanvas is not null && _dragRectangle is not null) { _dragStartPoint = startPoint; @@ -1670,7 +1674,7 @@ internal void StartDragSelection(Point startPoint) _dragRectangle.Width = 0; _dragRectangle.Height = 0; - _dragRectangle.Visibility = Visibility.Visible; + _dragRectangle.Visibility = ShowDragRectangle ? Visibility.Visible : Visibility.Collapsed; } } From cc69f22ab2f5afae8f23c9c4157757a9878b2ebd Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Thu, 30 Jul 2026 04:49:42 +0500 Subject: [PATCH 04/11] fix: address PR review feedback on range normalization and selection logic - Normalize FromCoordinates() to use Math.Min for firstRow/firstCol so ranges with start > end (e.g. reverse drag) compute correct bounds. - Guard TableView_SelectionChanged against zero visible columns before creating a full-row cell slot range (avoids ArgumentOutOfRangeException). - Fix SelectRowsInDragRect: remove invalid null-conditionals on the non-nullable ItemIndexRange parameter; use .Value on the nullable field. - Fix DeselectCell to subtract only the 1x1 cell range from each containing selection range instead of removing the whole range. - Fix EndDragSelection: clear _pointerCaptureElement after releasing captures and remove the dead _tableViewDragPointer cleanup block. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/TableView.cs | 30 ++++++++++++++++-------------- src/TableViewCellSlotRange.cs | 4 +++- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/TableView.cs b/src/TableView.cs index ae39936b..b14eaeb2 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -96,6 +96,8 @@ private void TableView_SelectionChanged(object sender, SelectionChangedEventArgs { var addedIndexes = e.AddedItems.Select(item => Items.IndexOf(item)); + if (Columns.VisibleColumns.Count == 0) return; + foreach (var range in IndexRangeHelper.GetRanges(addedIndexes)) { var slotRange = TableViewCellSlotRange.FromCoordinates(range.FirstIndex, 0, range.LastIndex, Columns.VisibleColumns.Count - 1); @@ -367,16 +369,16 @@ private void MakeSelectionInDragRect() /// private void SelectRowsInDragRect(ItemIndexRange rows) { - if (_lastDragSelectionRowRange?.FirstIndex == rows?.FirstIndex && _lastDragSelectionRowRange?.LastIndex == rows?.LastIndex) return; + if (_lastDragSelectionRowRange?.FirstIndex == rows.FirstIndex && _lastDragSelectionRowRange?.LastIndex == rows.LastIndex) return; - if (_lastDragSelectionRowRange is not null && rows is not null && _lastDragSelectionRowRange.Contains(rows)) + if (_lastDragSelectionRowRange is not null && _lastDragSelectionRowRange.Value.Contains(rows)) { - foreach (var slicedRange in _lastDragSelectionRowRange.Subtract(rows)) + foreach (var slicedRange in _lastDragSelectionRowRange.Value.Subtract(rows)) { DeselectRange(slicedRange); } } - else if (rows?.Length > 0) + else if (rows.Length > 0) { SelectRange(rows); } @@ -1515,10 +1517,16 @@ private void SelectCells(TableViewCellSlot slot, bool shiftKey, bool ctrlKey) /// internal void DeselectCell(TableViewCellSlot slot) { - var selectionRange = SelectedCellRanges.LastOrDefault(x => x.Contains(slot.Row, slot.Column)); - if (selectionRange is not null) + var singleCellRange = TableViewCellSlotRange.FromSlots(slot); + var containingRanges = SelectedCellRanges.Where(x => x.Contains(slot.Row, slot.Column)).ToList(); + + foreach (var range in containingRanges) { - SelectedCellRanges.Remove(selectionRange); + SelectedCellRanges.Remove(range); + foreach (var remaining in range.Subtract(singleCellRange)) + { + SelectedCellRanges.Add(remaining); + } } CurrentCellSlot = slot; @@ -1902,6 +1910,7 @@ internal async void EndDragSelection() StopAutoScroll(); _pointerCaptureElement?.ReleasePointerCaptures(); + _pointerCaptureElement = null; _tableViewDragPointer = null; _scrollViewer?.ViewChanged -= OnScrollViewerViewChangedDuringDrag; @@ -1920,13 +1929,6 @@ internal async void EndDragSelection() colsLeftToRight ? endRange.LastColumn : endRange.FirstColumn); } - // Clean up any pointer capture the TableView itself held (empty-space drag path). - if (_tableViewDragPointer is not null) - { - _tableViewDragPointer = null; - ReleasePointerCaptures(); - } - IsDragSelecting = false; _dragStartPoint = null; _lastDragCanvasPoint = null; diff --git a/src/TableViewCellSlotRange.cs b/src/TableViewCellSlotRange.cs index f16fa8a1..8a70a7c0 100644 --- a/src/TableViewCellSlotRange.cs +++ b/src/TableViewCellSlotRange.cs @@ -77,10 +77,12 @@ public TableViewCellSlotRange(int firstRowIndex, int firstColumnIndex, int rowCo /// public static TableViewCellSlotRange FromCoordinates(int startRow, int startCol, int endRow, int endCol) { + var firstRow = Math.Min(startRow, endRow); + var firstCol = Math.Min(startCol, endCol); var rowCount = Math.Abs(endRow - startRow) + 1; var colCount = Math.Abs(endCol - startCol) + 1; - return new TableViewCellSlotRange(startRow, startCol, rowCount, colCount); + return new TableViewCellSlotRange(firstRow, firstCol, rowCount, colCount); } /// From 7f9f685f3d8133221f4b1ae34b31d5eaa91015ba Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Thu, 30 Jul 2026 04:58:18 +0500 Subject: [PATCH 05/11] fix: remove incorrect .Value on ItemIndexRange reference type ItemIndexRange is a class, not a struct, so .Value does not exist on a nullable reference. Revert to calling Contains/Subtract directly on the already-null-checked _lastDragSelectionRowRange. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/TableView.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/TableView.cs b/src/TableView.cs index b14eaeb2..c01ad22b 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -371,9 +371,9 @@ private void SelectRowsInDragRect(ItemIndexRange rows) { if (_lastDragSelectionRowRange?.FirstIndex == rows.FirstIndex && _lastDragSelectionRowRange?.LastIndex == rows.LastIndex) return; - if (_lastDragSelectionRowRange is not null && _lastDragSelectionRowRange.Value.Contains(rows)) + if (_lastDragSelectionRowRange is not null && _lastDragSelectionRowRange.Contains(rows)) { - foreach (var slicedRange in _lastDragSelectionRowRange.Value.Subtract(rows)) + foreach (var slicedRange in _lastDragSelectionRowRange.Subtract(rows)) { DeselectRange(slicedRange); } From 1bfbf6edc1d3568268c587692b19c65ddcac5ea4 Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Thu, 30 Jul 2026 16:58:42 +0500 Subject: [PATCH 06/11] fix: release pointer capture when drag selection does not start, filter invalid item indexes - Release pointer captures immediately if StartDragSelection returns early (SelectionMode not Multiple/Extended), preventing a stuck capture. - Filter out Items.IndexOf() results of -1 in the Ctrl SelectionChanged path to avoid passing invalid row indexes to TableViewCellSlotRange. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/TableView.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/TableView.cs b/src/TableView.cs index c01ad22b..59c5271a 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -94,7 +94,9 @@ private void TableView_SelectionChanged(object sender, SelectionChangedEventArgs } else { - var addedIndexes = e.AddedItems.Select(item => Items.IndexOf(item)); + var addedIndexes = e.AddedItems + .Select(item => Items.IndexOf(item)) + .Where(i => i >= 0); if (Columns.VisibleColumns.Count == 0) return; @@ -276,6 +278,15 @@ private void OnAnyPointerPressed(object sender, PointerRoutedEventArgs e) _pointerCaptureElement.CapturePointer(e.Pointer); _tableViewDragPointer = e.Pointer; StartDragSelection(canvasPoint.Value); + + if (!IsDragSelecting) + { + _pointerCaptureElement?.ReleasePointerCaptures(); + _pointerCaptureElement = null; + _tableViewDragPointer = null; + return; + } + MakeSelectionInDragRect(); } } From 449df21a37a57e8c229ef6e83646c323088d0145 Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Fri, 31 Jul 2026 17:10:10 +0500 Subject: [PATCH 07/11] Improve TableView cell and drag selection logic --- src/TableView.cs | 122 +++++++++++++++++++++++++++----------------- src/TableViewRow.cs | 25 +++++++++ 2 files changed, 100 insertions(+), 47 deletions(-) diff --git a/src/TableView.cs b/src/TableView.cs index 59c5271a..64afe5d8 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -160,7 +160,14 @@ protected override void PrepareContainerForItemOverride(DependencyObject element row.TableView = this; row.EnsureCellsStyle(default, item); - row.ApplyCellsSelectionState(); + + _pendingCellStateRows.Add(row.Index); + if (!_cellStateDispatchPending) + { + _cellStateDispatchPending = true; + DispatcherQueue.TryEnqueue(ApplyPendingCellStates); + } + row.RowPresenter?.ApplyDetailsPaneState(item); if (CurrentCellSlot.HasValue) @@ -244,9 +251,6 @@ private void OnAnyPointerPressed(object sender, PointerRoutedEventArgs e) _lastDragSelectionCellRange = null; LastSelectionUnit = TableViewSelectionUnit.Row; - if (!ctrlKey && SelectionMode is not ListViewSelectionMode.Multiple) - DeselectAll(); - if (e.OriginalSource is UIElement element) { UIElement? clickedElement = element.FindAscendant(); // Check if the pointer is over a TableViewCell @@ -277,6 +281,10 @@ private void OnAnyPointerPressed(object sender, PointerRoutedEventArgs e) _pointerCaptureElement.CapturePointer(e.Pointer); _tableViewDragPointer = e.Pointer; + + if (!ctrlKey && SelectionMode is not ListViewSelectionMode.Multiple && LastSelectionUnit is not TableViewSelectionUnit.Cell) + DeselectAll(); + StartDragSelection(canvasPoint.Value); if (!IsDragSelecting) @@ -404,24 +412,34 @@ private void SelectCellsInDragRect(TableViewCellSlotRange cells) { if (_lastDragSelectionCellRange == cells) return; - if (_lastDragSelectionCellRange is not null && cells is not null) + DispatcherQueue.TryEnqueue(() => { - foreach (var range in _lastDragSelectionCellRange.Subtract(cells)) + if (_lastDragSelectionCellRange is null + && !KeyboardHelper.IsCtrlKeyDown() + && SelectionMode is not ListViewSelectionMode.Multiple) { - SubtractCellRangeFromSelection(range); + DeselectAllItems(); + SelectedCellRanges.Clear(); + } + else if (_lastDragSelectionCellRange is not null && cells is not null) + { + foreach (var range in _lastDragSelectionCellRange.Subtract(cells)) + { + SubtractCellRangeFromSelection(range); + } } - } - if (SelectedCellRanges.Any(r => r == cells)) - { - OnCellSelectionChanged(); - } - else if (cells?.Length > 0) - { - SelectCellRange(cells); - } + if (SelectedCellRanges.Any(r => r == cells)) + { + OnCellSelectionChanged(); + } + else if (cells?.Length > 0) + { + SelectCellRange(cells); + } - _lastDragSelectionCellRange = cells; + _lastDragSelectionCellRange = cells; + }); } /// @@ -620,6 +638,7 @@ protected async override void OnApplyTemplate() DragRectangleCanvas = GetTemplateChild("DragRectangleCanvas") as Canvas; _dragRectangle = GetTemplateChild("DragRectangle") as Border; _scrollViewer?.Loaded += OnScrollViewerLoaded; + _scrollViewer?.ViewChanged += OnScrollViewerViewChanged; if (IsLoaded) { @@ -631,6 +650,17 @@ protected async override void OnApplyTemplate() SetHeadersVisibility(); } + /// + /// Handles the ViewChanged event of the ScrollViewer control, updating the position of each row when the view changes. + /// + private void OnScrollViewerViewChanged(object? sender, ScrollViewerViewChangedEventArgs e) + { + foreach (var row in _rows) + { + row.UpdatePosition(); + } + } + /// /// Handles the Loaded event of the ScrollViewer control. /// @@ -1813,9 +1843,9 @@ private void UpdateAutoScroll(Point canvasPoint) { _autoScrollTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) }; _autoScrollTimer.Tick += OnAutoScrollTimerTick; + _autoScrollTimer.Start(); } - - _autoScrollTimer.Start(); + // else: timer already running — delta values above are picked up on the next tick } else { @@ -1872,15 +1902,21 @@ private void OnAutoScrollTimerTick(object? sender, object e) return; } - // Horizontal scroll via HorizontalOffset DP does not fire ViewChanged, - // so reposition rectangle and update selection here. - // Vertical scroll fires ViewChanged which handles it via OnScrollViewerViewChangedDuringDrag. - if (Math.Abs(_autoScrollHorizontalDelta) > 0.5 && _lastDragCanvasPoint is not null) + // Horizontal scroll does not fire ViewChanged, so reposition the rectangle here. + // Selection is updated for all scroll directions from the timer tick, not from ViewChanged, + // so that MakeSelectionInDragRect runs after ChangeView completes rather than inside the layout pass. + if (_lastDragCanvasPoint is not null) { - if (_dragStartPoint is not null && DragRectangleCanvas is not null && _dragRectangle is not null) + if (Math.Abs(_autoScrollHorizontalDelta) > 0.5 && + _dragStartPoint is not null && DragRectangleCanvas is not null && _dragRectangle is not null) { PositionDragRectangle(_lastDragCanvasPoint.Value); } + + if (_tableViewDragPointer is not null) + { + MakeSelectionInDragRect(); + } } } @@ -1909,6 +1945,13 @@ private void OnScrollViewerViewChangedDuringDrag(object? sender, ScrollViewerVie { PositionDragRectangle(_lastDragCanvasPoint.Value); } + + // During auto-scroll the timer tick owns selection updates to keep MakeSelectionInDragRect + // out of the scroll layout pass. Only update here for non-auto-scroll scrolls (e.g. scroll wheel). + if (_autoScrollTimer is null && _tableViewDragPointer is not null) + { + MakeSelectionInDragRect(); + } } /// @@ -2094,11 +2137,7 @@ void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e) foreach (var row in _rows) { - Point rowOrigin; - try { rowOrigin = row.TransformToVisual(DragRectangleCanvas).TransformPoint(default); } - catch (ArgumentException) { continue; } - - var rowTop = rowOrigin.Y; + var rowTop = row.Position; var rowBottom = rowTop + row.ActualHeight; if (rowBottom <= minY || rowTop >= maxY) continue; @@ -2108,6 +2147,7 @@ void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e) { nearestDistance = distance; nearestRow = row; + rowTop += row.ActualHeight; } } @@ -2144,35 +2184,23 @@ void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e) // Mirror the row snapping: find the nearest column within the horizontal drag span. var horizontalScrollDelta = HorizontalOffset - _dragStartHorizontalOffset; - var adjustedStartX = _dragStartPoint is not null - ? _dragStartPoint.Value.X - horizontalScrollDelta - : canvasPoint.X; + var adjustedPointerX = canvasPoint.X - horizontalScrollDelta; - var minX = Math.Min(adjustedStartX, canvasPoint.X); - var maxX = Math.Max(adjustedStartX, canvasPoint.X); - - var nearestColIndex = -1; - var nearestDistance = double.MaxValue; var columnLeft = CellsHorizontalOffset - HorizontalOffset; for (var i = 0; i < Columns.VisibleColumns.Count; i++) { var columnRight = columnLeft + Columns.VisibleColumns[i].ActualWidth; - if (columnRight > minX && columnLeft < maxX) - { - var distance = Math.Max(0d, Math.Max(columnLeft - canvasPoint.X, canvasPoint.X - columnRight)); - if (distance < nearestDistance) - { - nearestDistance = distance; - nearestColIndex = i; - } - } + if (adjustedPointerX <= columnRight) + return new TableViewCellSlot(rowIndex, i); columnLeft = columnRight; } - return nearestColIndex == -1 ? null : new TableViewCellSlot(rowIndex, nearestColIndex); + return Columns.VisibleColumns.Count > 0 + ? new TableViewCellSlot(rowIndex, Columns.VisibleColumns.Count - 1) + : null; } /// diff --git a/src/TableViewRow.cs b/src/TableViewRow.cs index 8bc68aac..d2a65c31 100644 --- a/src/TableViewRow.cs +++ b/src/TableViewRow.cs @@ -175,10 +175,35 @@ protected override Size ArrangeOverride(Size finalSize) var left = Math.Max(cornerRadius.TopLeft, cornerRadius.BottomLeft); _itemPresenter?.Arrange(new Rect(-left, 0, _itemPresenter.ActualWidth + left, _itemPresenter.ActualHeight)); + + UpdatePosition(); return finalSize; } + /// + /// Updates the position of the row relative to the TableView. + /// + internal void UpdatePosition() + { + DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Low, () => + { + if (TableView is not null) + { + try + { + Position = TransformToVisual(TableView.DragRectangleCanvas).TransformPoint(default).Y; + } + catch { } + } + }); + } + + /// + /// Gets or sets the position of the row relative to the TableView. + /// + internal double Position { get; set; } + /// /// Ensures cells are created for the row. /// From 504b8f81339558194d605df91cb946ff52d3ead9 Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Sat, 1 Aug 2026 01:34:29 +0500 Subject: [PATCH 08/11] fix: single selection mode behavior --- src/Helpers/TableViewTrace.cs | 5 +- src/TableView.cs | 103 ++++++++++++++++++++-------------- 2 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src/Helpers/TableViewTrace.cs b/src/Helpers/TableViewTrace.cs index 2e1d9fa1..6f31ca24 100644 --- a/src/Helpers/TableViewTrace.cs +++ b/src/Helpers/TableViewTrace.cs @@ -7,6 +7,9 @@ internal static class TableViewTrace [Conditional("DEBUG")] public static void Write(string message) { - Debug.WriteLine($"[TableView] {message}"); + if (Debugger.IsAttached) + { + Debug.WriteLine($"[TableView] {message}"); + } } } \ No newline at end of file diff --git a/src/TableView.cs b/src/TableView.cs index 64afe5d8..417adeaf 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -273,6 +273,16 @@ private void OnAnyPointerPressed(object sender, PointerRoutedEventArgs e) : TableViewSelectionUnit.Row }; + if (SelectionMode is ListViewSelectionMode.Single) + { + _lastDragCanvasPoint = canvasPoint; + MakeSelectionInDragRect(); + SetCurrentCellFromCanvasPoint(_lastDragCanvasPoint.Value); + + return; + } + + clickedElement.Focus(FocusState.Programmatic); #if WINDOWS _pointerCaptureElement = clickedElement; #else @@ -390,7 +400,11 @@ private void SelectRowsInDragRect(ItemIndexRange rows) { if (_lastDragSelectionRowRange?.FirstIndex == rows.FirstIndex && _lastDragSelectionRowRange?.LastIndex == rows.LastIndex) return; - if (_lastDragSelectionRowRange is not null && _lastDragSelectionRowRange.Contains(rows)) + if (SelectionMode is ListViewSelectionMode.Single && rows.Length is 1) + { + SelectedIndex = rows.FirstIndex; + } + else if (_lastDragSelectionRowRange is not null && _lastDragSelectionRowRange.Contains(rows)) { foreach (var slicedRange in _lastDragSelectionRowRange.Subtract(rows)) { @@ -1635,18 +1649,9 @@ private async Task OnCurrentCellChanged(TableViewCellSlot? oldSlot, TableViewCel if (newSlot.HasValue) { - // During drag selection, skip expensive scroll-into-view and focus operations. - // The drag rectangle handles visual feedback, and focus is restored when dragging ends. - if (IsDragSelecting) - { - var cell = GetCellFromSlot(newSlot.Value); - cell?.ApplyCurrentCellState(skipFocus: true); - } - else - { - var cell = await ScrollCellIntoView(newSlot.Value); - cell?.ApplyCurrentCellState(); - } + var cell = await ScrollCellIntoView(newSlot.Value); + cell?.ApplyCurrentCellState(); + cell?.Focus(FocusState.Programmatic); } } @@ -1959,7 +1964,7 @@ private void OnScrollViewerViewChangedDuringDrag(object? sender, ScrollViewerVie /// internal async void EndDragSelection() { - if (!IsDragSelecting) return; + if (!IsDragSelecting || _lastDragCanvasPoint is null) return; StopAutoScroll(); @@ -1970,32 +1975,24 @@ internal async void EndDragSelection() _scrollViewer?.ViewChanged -= OnScrollViewerViewChangedDuringDrag; _dragRectangle?.Visibility = Visibility.Collapsed; - // Determine the corner of the selection nearest the pointer before clearing drag state. - TableViewCellSlot? endSlot = null; - if (_lastDragSelectionCellRange is { Length: > 0 } endRange && _dragStartPoint is not null && _lastDragCanvasPoint is not null) - { - var verticalScrollDelta = (_scrollViewer?.VerticalOffset ?? 0) - _dragStartVerticalOffset; - var horizontalScrollDelta = HorizontalOffset - _dragStartHorizontalOffset; - var rowsTopToBottom = _dragStartPoint.Value.Y - verticalScrollDelta <= _lastDragCanvasPoint.Value.Y; - var colsLeftToRight = _dragStartPoint.Value.X - horizontalScrollDelta <= _lastDragCanvasPoint.Value.X; - endSlot = new TableViewCellSlot( - rowsTopToBottom ? endRange.LastRow : endRange.FirstRow, - colsLeftToRight ? endRange.LastColumn : endRange.FirstColumn); - } + SetCurrentCellFromCanvasPoint(_lastDragCanvasPoint.Value); IsDragSelecting = false; _dragStartPoint = null; _lastDragCanvasPoint = null; SelectionStartCellSlot = null; + } - // Restore focus and scroll to the current cell now that dragging has ended - if (endSlot?.IsValid(this) == true) - { - CurrentCellSlot = endSlot.Value; - } - else if (_lastDragSelectionCellRange?.Length > 0 && _lastDragSelectionCellRange.LastSlot.IsValid(this)) + private void SetCurrentCellFromCanvasPoint(Point canvasPoint) + { + if (GetSlotAtCanvasPoint(canvasPoint, exactMatch: true) is { } endSlot) { - CurrentCellSlot = _lastDragSelectionCellRange.LastSlot; + CurrentRowIndex = endSlot.Row; + + if (!(SelectionUnit is TableViewSelectionUnit.Row && IsReadOnly)) + { + CurrentCellSlot = endSlot; + } } } @@ -2117,10 +2114,26 @@ void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e) /// within the vertical span between the drag start point and when /// the point falls in empty space. Returns null when no realized row falls in that span. /// - private int? GetRowIndexAtCanvasPoint(Point canvasPoint) + private int? GetRowIndexAtCanvasPoint(Point canvasPoint, bool exactMatch = false) { if (DragRectangleCanvas is null) return null; + if (exactMatch) + { + foreach (var row in _rows) + { + var rowTop = row.Position; + var rowBottom = rowTop + row.ActualHeight; + + if (canvasPoint.Y >= rowTop && canvasPoint.Y < rowBottom) + { + return row.Index; + } + } + + return null; + } + // Compute the vertical span of the drag so we can snap to the nearest in-span row when the // pointer is in empty space. If there is no drag start (called outside a drag), minY == maxY // == canvasPoint.Y, which collapses back to the original exact hit-test behaviour. @@ -2140,14 +2153,14 @@ void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e) var rowTop = row.Position; var rowBottom = rowTop + row.ActualHeight; - if (rowBottom <= minY || rowTop >= maxY) continue; + if (rowBottom <= minY || rowTop >= maxY) + continue; var distance = Math.Max(0d, Math.Max(rowTop - canvasPoint.Y, canvasPoint.Y - rowBottom)); if (distance < nearestDistance) { nearestDistance = distance; nearestRow = row; - rowTop += row.ActualHeight; } } @@ -2176,11 +2189,11 @@ void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e) /// column within the horizontal and vertical span of the current drag when the point falls in /// empty space. Returns null when no realized row or visible column falls in that span. /// - private TableViewCellSlot? GetSlotAtCanvasPoint(Point canvasPoint) + private TableViewCellSlot? GetSlotAtCanvasPoint(Point canvasPoint, bool exactMatch = false) { if (DragRectangleCanvas is null) return null; - if (GetRowIndexAtCanvasPoint(canvasPoint) is not int rowIndex) return null; + if (GetRowIndexAtCanvasPoint(canvasPoint, exactMatch) is not int rowIndex) return null; // Mirror the row snapping: find the nearest column within the horizontal drag span. var horizontalScrollDelta = HorizontalOffset - _dragStartHorizontalOffset; @@ -2193,14 +2206,20 @@ void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e) var columnRight = columnLeft + Columns.VisibleColumns[i].ActualWidth; if (adjustedPointerX <= columnRight) - return new TableViewCellSlot(rowIndex, i); + { + // Require the pointer to actually be inside the cell. + if (exactMatch && adjustedPointerX < columnLeft) + return null; + + return new(rowIndex, i); + } columnLeft = columnRight; } - return Columns.VisibleColumns.Count > 0 - ? new TableViewCellSlot(rowIndex, Columns.VisibleColumns.Count - 1) - : null; + return exactMatch || Columns.VisibleColumns.Count == 0 + ? null + : new(rowIndex, Columns.VisibleColumns.Count - 1); } /// From 663dd76afa19cc25515c1229047af7aad3c292f4 Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Sat, 1 Aug 2026 01:36:55 +0500 Subject: [PATCH 09/11] Enable Frame Rate Counter while debugging --- samples/WinUI.TableView.SampleApp/App.xaml.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/samples/WinUI.TableView.SampleApp/App.xaml.cs b/samples/WinUI.TableView.SampleApp/App.xaml.cs index 0d7a1074..c602ae72 100644 --- a/samples/WinUI.TableView.SampleApp/App.xaml.cs +++ b/samples/WinUI.TableView.SampleApp/App.xaml.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Microsoft.UI.Xaml; +using System.Diagnostics; namespace WinUI.TableView.SampleApp; @@ -25,17 +26,17 @@ public App() #if DEBUG && WINDOWS private void DebugSettings_BindingFailed(object sender, BindingFailedEventArgs e) { - System.Diagnostics.Debug.WriteLine(e.Message); + Debug.WriteLine(e.Message); } private void DebugSettings_XamlResourceReferenceFailed(DebugSettings sender, XamlResourceReferenceFailedEventArgs args) { - System.Diagnostics.Debug.WriteLine(args.Message); + Debug.WriteLine(args.Message); } private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) { - if (System.Diagnostics.Debugger.IsAttached) System.Diagnostics.Debugger.Break(); + if (Debugger.IsAttached) Debugger.Break(); } #endif @@ -45,21 +46,27 @@ private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledEx /// Details about the launch request and process. protected override void OnLaunched(LaunchActivatedEventArgs args) { -#if DEBUG && !WINDOWS +#if DEBUG + if (Debugger.IsAttached) + { + DebugSettings.EnableFrameRateCounter = true; + } +#if !WINDOWS MainWindow.UseStudio(); MainWindow.SetWindowIcon(); +#endif #endif MainWindow.Activate(); } public static void InitializeLogging() { + #if DEBUG var factory = LoggerFactory.Create(builder => { #if __WASM__ builder.AddProvider(new global::Uno.Extensions.Logging.WebAssembly.WebAssemblyConsoleLoggerProvider()); - // Note: DebugSettings.EnableFrameRateCounter requires an Application instance #elif !WINDOWS builder.AddConsole(); #else From 2532f8bcb789b2c63ab696a80972e5e75257700a Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Sat, 1 Aug 2026 02:46:17 +0500 Subject: [PATCH 10/11] rename a veriable --- src/TableView.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/TableView.cs b/src/TableView.cs index 417adeaf..9b2ced35 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -71,6 +71,7 @@ public TableView() Unloaded += OnUnloaded; SelectionChanged += TableView_SelectionChanged; _collectionView.ItemPropertyChanged += OnItemPropertyChanged; + AddHandler(PointerPressedEvent, new PointerEventHandler(OnAnyPointerPressed), handledEventsToo: true); AddHandler(PointerReleasedEvent, new PointerEventHandler(OnAnyPointerReleased), handledEventsToo: true); } @@ -253,22 +254,22 @@ private void OnAnyPointerPressed(object sender, PointerRoutedEventArgs e) if (e.OriginalSource is UIElement element) { - UIElement? clickedElement = element.FindAscendant(); // Check if the pointer is over a TableViewCell - clickedElement ??= element.FindAscendant(); // If not, check if the pointer is over a TableViewRow + UIElement? pressedElement = element.FindAscendant(); // Check if the pointer is over a TableViewCell + pressedElement ??= element.FindAscendant(); // If not, check if the pointer is over a TableViewRow // Skip selection when the pointer is not over a Cell or Row, and ShowDragRectangle is false. - if (clickedElement == null && !ShowDragRectangle) return; + if (pressedElement == null && !ShowDragRectangle) return; - clickedElement ??= this; // If not, default to the TableView itself + pressedElement ??= this; // If not, default to the TableView itself - SelectionStartCellSlot = (clickedElement as TableViewCell)?.Slot; - SelectionStartRowIndex = (clickedElement as TableViewRow)?.Index; + SelectionStartCellSlot = (pressedElement as TableViewCell)?.Slot; + SelectionStartRowIndex = (pressedElement as TableViewRow)?.Index; LastSelectionUnit = SelectionUnit switch { TableViewSelectionUnit.Cell => TableViewSelectionUnit.Cell, TableViewSelectionUnit.Row => TableViewSelectionUnit.Row, - _ => clickedElement is TableViewCell + _ => pressedElement is TableViewCell ? TableViewSelectionUnit.Cell : TableViewSelectionUnit.Row }; @@ -282,9 +283,9 @@ private void OnAnyPointerPressed(object sender, PointerRoutedEventArgs e) return; } - clickedElement.Focus(FocusState.Programmatic); + pressedElement.Focus(FocusState.Programmatic); #if WINDOWS - _pointerCaptureElement = clickedElement; + _pointerCaptureElement = pressedElement; #else _pointerCaptureElement = this; #endif From 57158de28e2aa53102f8cc974b2f6198e36e58ab Mon Sep 17 00:00:00 2001 From: Waheed Ahmad Date: Sat, 1 Aug 2026 03:43:23 +0500 Subject: [PATCH 11/11] fixed cell and row focus states on Uno --- src/TableView.cs | 30 ++++++++++++++++++++++++++++++ src/Tableview.Uno.cs | 2 ++ 2 files changed, 32 insertions(+) diff --git a/src/TableView.cs b/src/TableView.cs index 9b2ced35..16af828f 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -257,6 +257,11 @@ private void OnAnyPointerPressed(object sender, PointerRoutedEventArgs e) UIElement? pressedElement = element.FindAscendant(); // Check if the pointer is over a TableViewCell pressedElement ??= element.FindAscendant(); // If not, check if the pointer is over a TableViewRow +#if !WINDOWS + _dragStartCell = pressedElement as TableViewCell; + _dragStartRow = element.FindAscendant(); +#endif + // Skip selection when the pointer is not over a Cell or Row, and ShowDragRectangle is false. if (pressedElement == null && !ShowDragRectangle) return; @@ -1993,7 +1998,32 @@ private void SetCurrentCellFromCanvasPoint(Point canvasPoint) if (!(SelectionUnit is TableViewSelectionUnit.Row && IsReadOnly)) { CurrentCellSlot = endSlot; +#if !WINDOWS + if (_dragStartCell is not null + && _dragStartPoint is not null + && endSlot != _dragStartCell.Slot) + { + VisualStates.GoToState(_dragStartCell, false, VisualStates.StateNormal); + + if (_dragStartCell.IsSelected) + { + VisualStates.GoToState(_dragStartCell, false, VisualStates.StateSelected); + } + } +#endif } + +#if !WINDOWS + if (_dragStartRow is not null && _dragStartRow.Index != endSlot.Row) + { + VisualStates.GoToState(_dragStartRow, false, VisualStates.StateNormal); + + if (_dragStartRow.IsSelected) + { + VisualStates.GoToState(_dragStartRow, false, VisualStates.StateSelected); + } + } +#endif } } diff --git a/src/Tableview.Uno.cs b/src/Tableview.Uno.cs index 91fa2fcf..b69e7061 100644 --- a/src/Tableview.Uno.cs +++ b/src/Tableview.Uno.cs @@ -17,6 +17,8 @@ partial class TableView private const BindingFlags BindingAttr = BindingFlags.NonPublic | BindingFlags.Instance; private PropertyInfo? _disableRaiseSelectionChangedPropertyInfo; private MethodInfo? _invokeSelectionChangedMethodInfo; + private TableViewRow? _dragStartRow; + private TableViewCell? _dragStartCell; private void SetDisableRaiseSelectionChanged(bool value) {