diff --git a/samples/WinUI.TableView.SampleApp/Pages/SelectionPage.xaml b/samples/WinUI.TableView.SampleApp/Pages/SelectionPage.xaml
index fcafdbb..676b2e6 100644
--- a/samples/WinUI.TableView.SampleApp/Pages/SelectionPage.xaml
+++ b/samples/WinUI.TableView.SampleApp/Pages/SelectionPage.xaml
@@ -36,6 +36,10 @@
OnContent="True"
OffContent="False"
IsOn="{Binding IsReadOnly, Mode=TwoWay, ElementName=tableView}" />
+
@@ -79,7 +83,8 @@
ItemsSource="{Binding Items}"
SelectionMode="$(SelectionMode)"
SelectionUnit="$(SelectionUnit)"
- IsReadOnly="$(IsReadOnly)" />
+ IsReadOnly="$(IsReadOnly)"
+ ShowDragRectangle="$(ShowDragRectangle)" />
@@ -89,6 +94,8 @@
Value="{x:Bind tableView.SelectionUnit, Mode=OneWay}" />
+
diff --git a/src/TableView.Properties.cs b/src/TableView.Properties.cs
index 5f6edb6..8b6fc07 100644
--- a/src/TableView.Properties.cs
+++ b/src/TableView.Properties.cs
@@ -300,6 +300,11 @@ public bool CanPaste
set => SetValue(CanPasteProperty, value);
}
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ShowDragRectangleProperty = DependencyProperty.Register(nameof(ShowDragRectangle), typeof(bool), typeof(TableView), new PropertyMetadata(true, OnShowDragRectangleChanged));
+
///
/// Gets or sets a value indicating whether opening the column filter over header right-click is enabled.
///
@@ -365,6 +370,15 @@ public bool ShowFilterItemsCount
set => SetValue(ShowFilterItemsCountProperty, value);
}
+ ///
+ /// Gets or sets a value indicating whether the drag selection rectangle is shown during cell drag selection.
+ ///
+ public bool ShowDragRectangle
+ {
+ get => (bool)GetValue(ShowDragRectangleProperty);
+ set => SetValue(ShowDragRectangleProperty, value);
+ }
+
///
/// Gets or sets a value that indicates whether the TableView should force select the Row or Cell depending on the SelectionUnit
///
@@ -404,6 +418,16 @@ public bool ForceRowOrCellSelectionOnContextRequested
///
internal bool IsEditing { get; private set; }
+ ///
+ /// Gets the canvas that hosts the drag selection rectangle.
+ ///
+ internal Canvas? DragRectangleCanvas { get; private set; }
+
+ ///
+ /// Gets a value indicating whether a drag selection is currently in progress.
+ ///
+ internal bool IsDragSelecting { get; private set; }
+
///
/// Gets the visibility states of details pane for each item.
///
@@ -928,6 +952,23 @@ private static void OnIsReadOnlyChanged(DependencyObject d, DependencyPropertyCh
}
}
+ ///
+ /// Handles changes to the ShowDragRectangle property.
+ ///
+ private static void OnShowDragRectangleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is TableView tableView && e.NewValue is false)
+ {
+ // Hide the rectangle visual but don't stop drag selection or auto-scroll
+ if (tableView._dragRectangle is not null)
+ {
+ tableView._dragRectangle.Visibility = Visibility.Collapsed;
+ }
+
+ tableView._dragStartPoint = null;
+ }
+ }
+
///
/// Handles changes to the CanFilterColumns property.
///
diff --git a/src/TableView.cs b/src/TableView.cs
index 28c83ff..e9ee5e5 100644
--- a/src/TableView.cs
+++ b/src/TableView.cs
@@ -3,6 +3,7 @@
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;
@@ -39,6 +40,15 @@ public partial class TableView : ListView
private bool _isItemsSourceSuspended;
private readonly List _rows = [];
private readonly CollectionView _collectionView = [];
+ private Border? _dragRectangle;
+ private Point? _dragStartPoint;
+ private bool _cellSelectionDirty;
+ private Point? _lastDragCanvasPoint;
+ private DispatcherTimer? _autoScrollTimer;
+ private double _autoScrollVerticalDelta;
+ private double _autoScrollHorizontalDelta;
+ private double _dragStartVerticalOffset;
+ private double _dragStartHorizontalOffset;
///
/// Initializes a new instance of the TableView class.
@@ -342,6 +352,8 @@ protected async override void OnApplyTemplate()
_headerRow = GetTemplateChild("HeaderRow") as TableViewHeaderRow;
_scrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer;
_headerRowDefinition = GetTemplateChild("HeaderRowDefinition") as RowDefinition;
+ DragRectangleCanvas = GetTemplateChild("DragRectangleCanvas") as Canvas;
+ _dragRectangle = GetTemplateChild("DragRectangle") as Border;
_scrollViewer?.Loaded += OnScrollViewerLoaded;
if (IsLoaded)
@@ -389,6 +401,9 @@ private void OnLoaded(object sender, RoutedEventArgs e)
///
private void OnUnloaded(object sender, RoutedEventArgs e)
{
+ EndDragSelection();
+ StopAutoScroll();
+
if (IsEditing && CurrentCellSlot.HasValue && GetCellFromSlot(CurrentCellSlot.Value) is { } currentCell)
{
currentCell.EndEditing(TableViewEditAction.Commit);
@@ -1270,8 +1285,18 @@ private async Task OnCurrentCellChanged(TableViewCellSlot? oldSlot, TableViewCel
if (newSlot.HasValue)
{
- var cell = await ScrollCellIntoView(newSlot.Value);
- cell?.ApplyCurrentCellState();
+ // 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();
+ }
}
}
@@ -1280,8 +1305,13 @@ private async Task OnCurrentCellChanged(TableViewCellSlot? oldSlot, TableViewCel
///
private void OnCellSelectionChanged()
{
- DispatcherQueue.TryEnqueue(() =>
+ if (_cellSelectionDirty) return;
+ _cellSelectionDirty = true;
+
+ if (!DispatcherQueue.TryEnqueue(() =>
{
+ _cellSelectionDirty = false;
+
var oldSelection = SelectedCells;
SelectedCells = [.. SelectedCellRanges.SelectMany(x => x)];
@@ -1294,7 +1324,10 @@ private void OnCellSelectionChanged()
}
InvokeCellSelectionChangedEvent(oldSelection);
- });
+ }))
+ {
+ _cellSelectionDirty = false;
+ }
}
///
@@ -1311,6 +1344,329 @@ private void InvokeCellSelectionChangedEvent(HashSet oldSelec
}
}
+ ///
+ /// Starts drag selection tracking, auto-scroll, and optionally the drag rectangle visual.
+ ///
+ /// The starting point relative to the drag rectangle canvas.
+ internal void StartDragSelection(Point startPoint)
+ {
+ if (SelectionMode is not (ListViewSelectionMode.Multiple or ListViewSelectionMode.Extended))
+ {
+ return;
+ }
+
+ // Guard against re-entry (e.g., multi-touch) to prevent double ViewChanged subscription
+ if (IsDragSelecting)
+ {
+ EndDragSelection();
+ }
+
+ IsDragSelecting = true;
+ _lastDragCanvasPoint = startPoint;
+ _dragStartVerticalOffset = _scrollViewer?.VerticalOffset ?? 0;
+ _dragStartHorizontalOffset = HorizontalOffset;
+
+ if (_scrollViewer is not null)
+ {
+ _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)
+ {
+ _dragStartPoint = startPoint;
+
+ Canvas.SetLeft(_dragRectangle, startPoint.X);
+ Canvas.SetTop(_dragRectangle, startPoint.Y);
+ _dragRectangle.Width = 0;
+ _dragRectangle.Height = 0;
+
+ _dragRectangle.Visibility = Visibility.Visible;
+ }
+ }
+
+ ///
+ /// Updates the drag visual and auto-scroll during drag selection.
+ ///
+ /// The current pointer position relative to the drag rectangle canvas.
+ internal void UpdateDragRectangleVisual(Point currentPoint)
+ {
+ if (!IsDragSelecting)
+ {
+ return;
+ }
+
+ _lastDragCanvasPoint = currentPoint;
+
+ // Update the rectangle visual if it's active
+ if (_dragStartPoint is not null && DragRectangleCanvas is not null && _dragRectangle is not null)
+ {
+ PositionDragRectangle(currentPoint);
+ }
+
+ UpdateAutoScroll(currentPoint);
+ }
+
+ ///
+ /// 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.
+ ///
+ private void PositionDragRectangle(Point currentPoint)
+ {
+ if (_dragStartPoint is null || DragRectangleCanvas is null || _dragRectangle is null) return;
+
+ // Adjust the start point by how much the view has scrolled since drag began.
+ // This makes the rectangle extend naturally as content scrolls.
+ var verticalScrollDelta = (_scrollViewer?.VerticalOffset ?? 0) - _dragStartVerticalOffset;
+ var horizontalScrollDelta = HorizontalOffset - _dragStartHorizontalOffset;
+ var adjustedStartY = _dragStartPoint.Value.Y - verticalScrollDelta;
+ var adjustedStartX = _dragStartPoint.Value.X - horizontalScrollDelta;
+
+ var canvasWidth = DragRectangleCanvas.ActualWidth;
+ var canvasHeight = DragRectangleCanvas.ActualHeight;
+
+ var left = Math.Max(0, Math.Min(adjustedStartX, currentPoint.X));
+ var top = Math.Max(0, Math.Min(adjustedStartY, currentPoint.Y));
+ var right = Math.Min(canvasWidth, Math.Max(adjustedStartX, currentPoint.X));
+ var bottom = Math.Min(canvasHeight, Math.Max(adjustedStartY, currentPoint.Y));
+
+ Canvas.SetLeft(_dragRectangle, left);
+ Canvas.SetTop(_dragRectangle, top);
+ _dragRectangle.Width = Math.Max(0, right - left);
+ _dragRectangle.Height = Math.Max(0, bottom - top);
+ }
+
+ ///
+ /// Manages auto-scroll behavior when the pointer is near the top or bottom edge during drag selection.
+ ///
+ private void UpdateAutoScroll(Point canvasPoint)
+ {
+ if (_scrollViewer is null) return;
+
+ const double edgeThreshold = 40;
+ const double maxScrollSpeed = 20;
+
+ var viewportHeight = _scrollViewer.ViewportHeight;
+ var viewportWidth = _scrollViewer.ViewportWidth;
+ double vDelta = 0;
+ double hDelta = 0;
+
+ if (canvasPoint.Y > viewportHeight - edgeThreshold)
+ {
+ var proximity = Math.Min(1.0, (canvasPoint.Y - (viewportHeight - edgeThreshold)) / edgeThreshold);
+ vDelta = proximity * maxScrollSpeed;
+ }
+ else if (canvasPoint.Y < edgeThreshold)
+ {
+ var proximity = Math.Min(1.0, (edgeThreshold - canvasPoint.Y) / edgeThreshold);
+ vDelta = -(proximity * maxScrollSpeed);
+ }
+
+ if (canvasPoint.X > viewportWidth - edgeThreshold)
+ {
+ var proximity = Math.Min(1.0, (canvasPoint.X - (viewportWidth - edgeThreshold)) / edgeThreshold);
+ hDelta = proximity * maxScrollSpeed;
+ }
+ else if (canvasPoint.X < edgeThreshold)
+ {
+ var proximity = Math.Min(1.0, (edgeThreshold - canvasPoint.X) / edgeThreshold);
+ hDelta = -(proximity * maxScrollSpeed);
+ }
+
+ if (Math.Abs(vDelta) > 0.5 || Math.Abs(hDelta) > 0.5)
+ {
+ _autoScrollVerticalDelta = vDelta;
+ _autoScrollHorizontalDelta = hDelta;
+ if (_autoScrollTimer is null)
+ {
+ _autoScrollTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(16) };
+ _autoScrollTimer.Tick += OnAutoScrollTimerTick;
+ }
+
+ _autoScrollTimer.Start();
+ }
+ else
+ {
+ StopAutoScroll();
+ }
+ }
+
+ ///
+ /// Handles the auto-scroll timer tick to scroll the view and update drag selection.
+ ///
+ private void OnAutoScrollTimerTick(object? sender, object e)
+ {
+ if (!IsDragSelecting || _scrollViewer is null)
+ {
+ StopAutoScroll();
+ return;
+ }
+
+ var scrolled = false;
+
+ // Vertical auto-scroll via ChangeView
+ if (Math.Abs(_autoScrollVerticalDelta) > 0.5)
+ {
+ var newOffset = Math.Clamp(
+ _scrollViewer.VerticalOffset + _autoScrollVerticalDelta,
+ 0,
+ _scrollViewer.ScrollableHeight);
+
+ if (Math.Abs(newOffset - _scrollViewer.VerticalOffset) >= 0.5)
+ {
+ _scrollViewer.ChangeView(null, newOffset, null, true);
+ scrolled = true;
+ }
+ }
+
+ // Horizontal auto-scroll via HorizontalOffset DP
+ if (Math.Abs(_autoScrollHorizontalDelta) > 0.5)
+ {
+ var newOffset = Math.Clamp(
+ HorizontalOffset + _autoScrollHorizontalDelta,
+ 0,
+ _scrollViewer.ScrollableWidth);
+
+ if (Math.Abs(newOffset - HorizontalOffset) >= 0.5)
+ {
+ SetValue(HorizontalOffsetProperty, newOffset);
+ scrolled = true;
+ }
+ }
+
+ if (!scrolled)
+ {
+ StopAutoScroll();
+ 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)
+ {
+ if (_dragStartPoint is not null && DragRectangleCanvas is not null && _dragRectangle is not null)
+ {
+ PositionDragRectangle(_lastDragCanvasPoint.Value);
+ }
+
+ SelectCellAtDragPoint();
+ }
+ }
+
+ ///
+ /// Stops the auto-scroll timer.
+ ///
+ private void StopAutoScroll()
+ {
+ if (_autoScrollTimer is not null)
+ {
+ _autoScrollTimer.Stop();
+ _autoScrollTimer.Tick -= OnAutoScrollTimerTick;
+ _autoScrollTimer = null;
+ }
+ }
+
+ ///
+ /// Handles ScrollViewer.ViewChanged during drag to re-evaluate selection when scroll position changes.
+ ///
+ private void OnScrollViewerViewChangedDuringDrag(object? sender, ScrollViewerViewChangedEventArgs e)
+ {
+ if (!IsDragSelecting || _lastDragCanvasPoint is null) return;
+
+ // Reposition the rectangle using scroll-adjusted start point (if rectangle is active)
+ if (_dragStartPoint is not null && DragRectangleCanvas is not null && _dragRectangle is not null)
+ {
+ 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
+ }
+ }
+
+ ///
+ /// Ends drag selection tracking, auto-scroll, and hides the drag rectangle if visible.
+ ///
+ internal async void EndDragSelection()
+ {
+ if (!IsDragSelecting) return;
+
+ StopAutoScroll();
+
+ if (_scrollViewer is not null)
+ {
+ _scrollViewer.ViewChanged -= OnScrollViewerViewChangedDuringDrag;
+ }
+
+ if (_dragRectangle is not null)
+ {
+ _dragRectangle.Visibility = Visibility.Collapsed;
+ }
+
+ IsDragSelecting = false;
+ _dragStartPoint = null;
+ _lastDragCanvasPoint = null;
+
+ // Restore focus and scroll to the current cell now that dragging has ended
+ try
+ {
+ if (CurrentCellSlot.HasValue)
+ {
+ var cell = await ScrollCellIntoView(CurrentCellSlot.Value);
+ cell?.ApplyCurrentCellState();
+ }
+ }
+ catch (Exception)
+ {
+ // Focus restoration is best-effort after drag ends
+ }
+ }
+
///
/// Scrolls the specified cell slot into view.
///
diff --git a/src/TableViewCell.cs b/src/TableViewCell.cs
index 21ac1ed..bed74cc 100644
--- a/src/TableViewCell.cs
+++ b/src/TableViewCell.cs
@@ -249,6 +249,14 @@ protected override void OnPointerPressed(PointerRoutedEventArgs e)
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);
+ }
}
}
@@ -264,11 +272,20 @@ protected override void OnPointerReleased(PointerRoutedEventArgs e)
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)
{
@@ -276,6 +293,19 @@ protected override void OnManipulationDelta(ManipulationDeltaRoutedEventArgs e)
if (PointerCaptures?.Any() is true)
{
+ // 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)
@@ -336,6 +366,24 @@ private double GetHorizontalGridlineHeight()
.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)
{
@@ -520,12 +568,12 @@ internal void ApplySelectionState()
///
/// Applies the current cell state to the cell.
///
- internal async void ApplyCurrentCellState()
+ internal async void ApplyCurrentCellState(bool skipFocus = false)
{
var stateName = IsCurrent ? VisualStates.StateCurrent : VisualStates.StateRegular;
VisualStates.GoToState(this, false, stateName);
- if (IsCurrent)
+ if (IsCurrent && !skipFocus)
{
Focus(FocusState.Pointer);
diff --git a/src/Themes/Resources.xaml b/src/Themes/Resources.xaml
index 31d47b4..0668511 100644
--- a/src/Themes/Resources.xaml
+++ b/src/Themes/Resources.xaml
@@ -74,6 +74,9 @@
+
+
+ 0
@@ -143,6 +146,9 @@
+
+
+ 0
@@ -212,6 +218,9 @@
+
+
+ 0
diff --git a/src/Themes/TableView.xaml b/src/Themes/TableView.xaml
index 8fcee42..a393ec3 100644
--- a/src/Themes/TableView.xaml
+++ b/src/Themes/TableView.xaml
@@ -126,6 +126,17 @@
Template="{StaticResource TableViewScrollViewerTemplate}">
+
+
diff --git a/tests/DragSelectionRectangleTests.cs b/tests/DragSelectionRectangleTests.cs
new file mode 100644
index 0000000..a85a3ac
--- /dev/null
+++ b/tests/DragSelectionRectangleTests.cs
@@ -0,0 +1,195 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
+using System.Linq;
+using System.Threading.Tasks;
+using Windows.Foundation;
+
+namespace WinUI.TableView.Tests;
+
+[TestClass]
+public class DragSelectionRectangleTests
+{
+ private static TableView CreateTableView(ListViewSelectionMode selectionMode = ListViewSelectionMode.Extended)
+ {
+ var tv = new TableView
+ {
+ ItemsSource = Enumerable.Range(0, 20).ToList(),
+ SelectionMode = selectionMode,
+ };
+ tv.Columns.Add(new TableViewTextColumn { Header = "Col1" });
+ tv.Columns.Add(new TableViewTextColumn { Header = "Col2" });
+ return tv;
+ }
+
+ private static async Task CreateAndLoadTableView(ListViewSelectionMode selectionMode = ListViewSelectionMode.Extended)
+ {
+ var tv = CreateTableView(selectionMode);
+ await UnitTestApp.Current.MainWindow.LoadTestContentAsync(tv);
+ return tv;
+ }
+
+ [UITestMethod]
+ public void ShowDragRectangle_DefaultsToTrue()
+ {
+ var tv = new TableView();
+ Assert.IsTrue(tv.ShowDragRectangle);
+ }
+
+ [UITestMethod]
+ public void ShowDragRectangle_CanBeSetToFalse()
+ {
+ var tv = new TableView();
+ tv.ShowDragRectangle = false;
+ Assert.IsFalse(tv.ShowDragRectangle);
+ }
+
+ [UITestMethod]
+ public async Task StartDragSelection_SetsIsDragging_WhenExtendedMode()
+ {
+ var tv = await CreateAndLoadTableView(ListViewSelectionMode.Extended);
+ Assert.IsFalse(tv.IsDragSelecting);
+
+ tv.StartDragSelection(new Point(10, 10));
+
+ Assert.IsTrue(tv.IsDragSelecting);
+
+ tv.EndDragSelection();
+ await UnitTestApp.Current.MainWindow.UnloadTestContentAsync(tv);
+ }
+
+ [UITestMethod]
+ public void StartDragSelection_DoesNotStart_WhenSingleMode()
+ {
+ var tv = CreateTableView(ListViewSelectionMode.Single);
+
+ tv.StartDragSelection(new Point(10, 10));
+
+ Assert.IsFalse(tv.IsDragSelecting);
+ }
+
+ [UITestMethod]
+ public void StartDragSelection_DoesNotStart_WhenNoneMode()
+ {
+ var tv = CreateTableView(ListViewSelectionMode.None);
+
+ tv.StartDragSelection(new Point(10, 10));
+
+ Assert.IsFalse(tv.IsDragSelecting);
+ }
+
+ [UITestMethod]
+ public async Task StartDragSelection_StartsButNoRectangle_WhenShowDragRectangleIsFalse()
+ {
+ var tv = await CreateAndLoadTableView(ListViewSelectionMode.Extended);
+ tv.ShowDragRectangle = false;
+
+ tv.StartDragSelection(new Point(10, 10));
+
+ // Drag selection and auto-scroll are active, but rectangle visual is not shown
+ Assert.IsTrue(tv.IsDragSelecting);
+ Assert.AreEqual(Visibility.Collapsed, tv.DragRectangleCanvas?.Children.OfType().FirstOrDefault()?.Visibility);
+
+ tv.EndDragSelection();
+ await UnitTestApp.Current.MainWindow.UnloadTestContentAsync(tv);
+ }
+
+ [UITestMethod]
+ public async Task StartDragSelection_Starts_InMultipleMode()
+ {
+ var tv = await CreateAndLoadTableView(ListViewSelectionMode.Multiple);
+
+ tv.StartDragSelection(new Point(10, 10));
+
+ Assert.IsTrue(tv.IsDragSelecting);
+
+ tv.EndDragSelection();
+ await UnitTestApp.Current.MainWindow.UnloadTestContentAsync(tv);
+ }
+
+ [UITestMethod]
+ public async Task EndDragSelection_ResetsIsDragging()
+ {
+ var tv = await CreateAndLoadTableView(ListViewSelectionMode.Extended);
+ tv.StartDragSelection(new Point(10, 10));
+ Assert.IsTrue(tv.IsDragSelecting);
+
+ tv.EndDragSelection();
+
+ Assert.IsFalse(tv.IsDragSelecting);
+
+ await UnitTestApp.Current.MainWindow.UnloadTestContentAsync(tv);
+ }
+
+ [UITestMethod]
+ public void EndDragSelection_IsIdempotent()
+ {
+ var tv = CreateTableView(ListViewSelectionMode.Extended);
+ tv.StartDragSelection(new Point(10, 10));
+
+ tv.EndDragSelection();
+ tv.EndDragSelection(); // second call should not throw
+
+ Assert.IsFalse(tv.IsDragSelecting);
+ }
+
+ [UITestMethod]
+ public async Task ShowDragRectangle_SetFalse_HidesRectangleButKeepsDragActive()
+ {
+ var tv = await CreateAndLoadTableView(ListViewSelectionMode.Extended);
+ tv.StartDragSelection(new Point(10, 10));
+ Assert.IsTrue(tv.IsDragSelecting);
+
+ tv.ShowDragRectangle = false;
+
+ // Drag selection and auto-scroll remain active, but rectangle visual is hidden
+ Assert.IsTrue(tv.IsDragSelecting);
+
+ tv.EndDragSelection();
+ await UnitTestApp.Current.MainWindow.UnloadTestContentAsync(tv);
+ }
+
+ [UITestMethod]
+ public void ShowDragRectangleProperty_IsRegistered()
+ {
+ var tv = new TableView();
+ var value = tv.GetValue(TableView.ShowDragRectangleProperty);
+ Assert.IsInstanceOfType(value, typeof(bool));
+ Assert.IsTrue((bool)value);
+ }
+
+ [UITestMethod]
+ public void ShowDragRectangleProperty_CanBeSetViaDP()
+ {
+ var tv = new TableView();
+ tv.SetValue(TableView.ShowDragRectangleProperty, false);
+ Assert.IsFalse(tv.ShowDragRectangle);
+ }
+
+ [UITestMethod]
+ public void StartDragSelection_StartsEvenWhenCanvasIsNull()
+ {
+ // Before OnApplyTemplate, DragRectangleCanvas is null
+ var tv = CreateTableView(ListViewSelectionMode.Extended);
+ Assert.IsNull(tv.DragRectangleCanvas);
+
+ tv.StartDragSelection(new Point(10, 10));
+
+ // Drag selection and auto-scroll start even without canvas (rectangle visual just won't show)
+ Assert.IsTrue(tv.IsDragSelecting);
+
+ tv.EndDragSelection();
+ }
+
+ [UITestMethod]
+ public void UpdateDragRectangleVisual_DoesNotCrash_WhenNotDragging()
+ {
+ var tv = CreateTableView();
+
+ // Should be a no-op, not throw
+ tv.UpdateDragRectangleVisual(new Point(50, 50));
+
+ Assert.IsFalse(tv.IsDragSelecting);
+ }
+}