From 0b99d33dfe4489b162bfa02d50d5f4857ed37161 Mon Sep 17 00:00:00 2001 From: rashmithakur Date: Mon, 6 Apr 2026 14:08:44 +0530 Subject: [PATCH 1/8] row selection on cell editing poc --- src/TableView.cs | 27 ++++++++++++++++++++- src/TableViewCell.cs | 6 +++-- src/TableViewRow.cs | 57 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/TableView.cs b/src/TableView.cs index d9347c1d..bd307209 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -47,6 +47,7 @@ private sealed class GroupHeaderRowItem private bool _shouldThrowSelectionModeChangedException; private bool _isUpdatingBaseItemsSource; private bool _ensureColumns = true; + private TableViewRow? _editingHighlightRow; private readonly List _rows = []; private readonly CollectionView _collectionView = []; private readonly ObservableCollection _displayItems = []; @@ -276,7 +277,7 @@ private async Task HandleNavigations(KeyRoutedEventArgs e, bool shiftKey, bool c do { - newSlot = GetNextSlot(newSlot, shiftKey, e.Key is VirtualKey.Enter); + newSlot = GetNextSlot(newSlot, shiftKey, e.Key is VirtualKey.Enter || SelectionUnit is TableViewSelectionUnit.Row); } while (isEditing && Columns[newSlot.Column].IsReadOnly); @@ -288,6 +289,16 @@ private async Task HandleNavigations(KeyRoutedEventArgs e, bool shiftKey, bool c { SetIsEditing(false); } + else if (SelectionUnit is TableViewSelectionUnit.Row && newSlot.Row != currentCell.Slot.Row) + { + // Editing moved to a different row — move the highlight + _editingHighlightRow?.ApplyEditingHighlight(false); + if (ContainerFromIndex(newSlot.Row) is TableViewRow newRow) + { + _editingHighlightRow = newRow; + newRow.ApplyEditingHighlight(true); + } + } } MakeSelection(newSlot, false); @@ -2253,6 +2264,20 @@ internal void SetIsEditing(bool value) IsEditing = value; UpdateCornerButtonState(); + + if (value && SelectionUnit is TableViewSelectionUnit.Row) + { + if (CurrentCellSlot.HasValue && ContainerFromIndex(CurrentCellSlot.Value.Row) is TableViewRow row) + { + _editingHighlightRow = row; + row.ApplyEditingHighlight(true); + } + } + else if (!value) + { + _editingHighlightRow?.ApplyEditingHighlight(false); + _editingHighlightRow = null; + } } /// diff --git a/src/TableViewCell.cs b/src/TableViewCell.cs index c33dcb86..b05423a9 100644 --- a/src/TableViewCell.cs +++ b/src/TableViewCell.cs @@ -327,7 +327,8 @@ protected override void OnPointerEntered(PointerRoutedEventArgs e) if ((TableView?.SelectionMode is not ListViewSelectionMode.None && TableView?.SelectionUnit is not TableViewSelectionUnit.Row) - || !TableView.IsReadOnly) + || !TableView.IsReadOnly + || (TableView?.SelectionUnit is TableViewSelectionUnit.Row && !IsReadOnly)) { VisualStates.GoToState(this, false, VisualStates.StatePointerOver); } @@ -340,7 +341,8 @@ protected override void OnPointerExited(PointerRoutedEventArgs e) if ((TableView?.SelectionMode is not ListViewSelectionMode.None && TableView?.SelectionUnit is not TableViewSelectionUnit.Row) - || !TableView.IsReadOnly) + || !TableView.IsReadOnly + || (TableView?.SelectionUnit is TableViewSelectionUnit.Row && !IsReadOnly)) { VisualStates.GoToState(this, false, VisualStates.StateNormal); } diff --git a/src/TableViewRow.cs b/src/TableViewRow.cs index aff28b6c..75821c41 100644 --- a/src/TableViewRow.cs +++ b/src/TableViewRow.cs @@ -37,6 +37,7 @@ public partial class TableViewRow : ListViewItem private ListViewItemPresenter? _itemPresenter; private Border? _selectionBackground; private bool _ensureCells = true; + private bool _isEditing; private Brush? _cellPresenterBackground; private Brush? _cellPresenterForeground; @@ -226,7 +227,7 @@ protected override void OnTapped(TappedRoutedEventArgs e) } /// - protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e) + protected override async void OnDoubleTapped(DoubleTappedRoutedEventArgs e) { if (TableView?.IsGroupHeaderItem(Content) is true) { @@ -238,6 +239,25 @@ protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e) TableView?.OnRowDoubleTapped(eventArgs); e.Handled = eventArgs.Handled; + if (e.Handled) + { + return; + } + + // When SelectionUnit is Row, the cell's OnDoubleTapped never fires because + // ListViewItem consumes the pointer events for row selection. Forward the + // double-tap to the target cell so editing can still be initiated. + if (TableView?.SelectionUnit is TableViewSelectionUnit.Row + && e.OriginalSource is DependencyObject source + && source.FindAscendant() is { IsReadOnly: false } cell + && !TableView.IsEditing + && cell.Column?.UseSingleElement is not true) + { + TableView.MakeSelection(cell.Slot, false); + e.Handled = await cell.BeginCellEditing(e); + return; + } + base.OnDoubleTapped(e); } @@ -655,7 +675,7 @@ private async void EnsureSelectionIndicatorPosition(double detailsHeight, Border /// internal void EnsureAlternateColors() { - if (TableView is null || RowPresenter is null) return; + if (TableView is null || RowPresenter is null || _isEditing) return; RowPresenter.Background = Index % 2 == 1 && TableView.AlternateRowBackground is not null ? TableView.AlternateRowBackground : _cellPresenterBackground; @@ -674,6 +694,39 @@ internal void UpdateSelectCheckMarkOpacity() } } + /// + /// Highlights or unhighlights the row to indicate that a cell is being edited. + /// + internal void ApplyEditingHighlight(bool isEditing) + { + _isEditing = isEditing; + + if (isEditing) + { +#if WINDOWS + if (RowPresenter is not null) + { + RowPresenter.Background = _itemPresenter?.PointerOverBackground; + } +#else + if (_selectionBackground is not null) + { + _selectionBackground.Opacity = 1; + } +#endif + } + else + { +#if !WINDOWS + if (_selectionBackground is not null) + { + _selectionBackground.Opacity = IsSelected ? 1 : 0; + } +#endif + EnsureAlternateColors(); + } + } + /// /// Gets the height of the horizontal gridlines. /// From 1d83594b9b7142cc8fba0327784b054e524f8321 Mon Sep 17 00:00:00 2001 From: rashmithakur Date: Mon, 6 Apr 2026 16:46:12 +0530 Subject: [PATCH 2/8] add single-tap forwarding --- src/TableViewRow.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/TableViewRow.cs b/src/TableViewRow.cs index 75821c41..28d19566 100644 --- a/src/TableViewRow.cs +++ b/src/TableViewRow.cs @@ -209,7 +209,7 @@ protected override void OnPointerReleased(PointerRoutedEventArgs e) } /// - protected override void OnTapped(TappedRoutedEventArgs e) + protected override async void OnTapped(TappedRoutedEventArgs e) { if (TableView?.IsGroupHeaderItem(Content) is true) { @@ -224,6 +224,20 @@ protected override void OnTapped(TappedRoutedEventArgs e) TableView.CurrentRowIndex = Index; TableView.LastSelectionUnit = TableViewSelectionUnit.Row; } + + // When SelectionUnit is Row and the row is already selected, forward the + // tap to the target cell so editing can be initiated with a second tap + // (like File Explorer's tap-pause-tap to rename). + if (TableView?.SelectionUnit is TableViewSelectionUnit.Row + && IsSelected + && e.OriginalSource is DependencyObject source + && source.FindAscendant() is { IsReadOnly: false } cell + && !TableView.IsEditing + && cell.Column?.UseSingleElement is not true) + { + TableView.MakeSelection(cell.Slot, false); + e.Handled = await cell.BeginCellEditing(e); + } } /// From 168fa7c84e3be9467e55f6ef8d7d4a77282ceb73 Mon Sep 17 00:00:00 2001 From: rashmithakur Date: Mon, 6 Apr 2026 17:06:15 +0530 Subject: [PATCH 3/8] add tests --- tests/TableViewRowEditingTests.cs | 46 +++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/TableViewRowEditingTests.cs diff --git a/tests/TableViewRowEditingTests.cs b/tests/TableViewRowEditingTests.cs new file mode 100644 index 00000000..1fe88b2c --- /dev/null +++ b/tests/TableViewRowEditingTests.cs @@ -0,0 +1,46 @@ +using Microsoft.UI.Xaml.Controls; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; + +namespace WinUI.TableView.Tests; + +[TestClass] +public class TableViewRowEditingTests +{ + [UITestMethod] + public void SetIsEditing_TogglesInRowMode() + { + var tableView = new TableView + { + AutoGenerateColumns = false, + SelectionMode = ListViewSelectionMode.Single, + SelectionUnit = TableViewSelectionUnit.Row, + }; + + tableView.Columns.Add(new TableViewTextColumn { Header = "Name" }); + + Assert.IsFalse(tableView.IsEditing); + + tableView.SetIsEditing(true); + Assert.IsTrue(tableView.IsEditing); + + // Calling again with same value should be a no-op + tableView.SetIsEditing(true); + Assert.IsTrue(tableView.IsEditing); + + tableView.SetIsEditing(false); + Assert.IsFalse(tableView.IsEditing); + } + + [UITestMethod] + public void ApplyEditingHighlight_Toggles() + { + var row = new TableViewRow(); + + // Apply and clear highlight multiple times — should not crash + row.ApplyEditingHighlight(true); + row.ApplyEditingHighlight(false); + row.ApplyEditingHighlight(true); + row.ApplyEditingHighlight(false); + } +} From 5c0c9212e52946e784bb1088373d695845835ab3 Mon Sep 17 00:00:00 2001 From: rashmithakur Date: Tue, 7 Apr 2026 15:43:40 +0530 Subject: [PATCH 4/8] bug fixes --- src/TableView.cs | 43 ++++++++++++++++++--- src/TableViewCell.cs | 14 ++++++- src/TableViewRow.cs | 24 +++++++++--- tests/TableViewRowEditingTests.cs | 64 +++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 14 deletions(-) diff --git a/src/TableView.cs b/src/TableView.cs index bd307209..faff6d2e 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -48,6 +48,7 @@ private sealed class GroupHeaderRowItem private bool _isUpdatingBaseItemsSource; private bool _ensureColumns = true; private TableViewRow? _editingHighlightRow; + private int _editingHighlightRowIndex = -1; private readonly List _rows = []; private readonly CollectionView _collectionView = []; private readonly ObservableCollection _displayItems = []; @@ -191,6 +192,18 @@ protected override void PrepareContainerForItemOverride(DependencyObject element { base.PrepareContainerForItemOverride(element, item); + // Reset editing highlight state on recycled containers to prevent + // stale _hasEditingHighlight from blocking EnsureAlternateColors. + if (element is TableViewRow { } recycledRow) + { + recycledRow.ApplyEditingHighlight(false); + + if (_editingHighlightRow == recycledRow) + { + _editingHighlightRow = null; + } + } + DispatcherQueue.TryEnqueue(() => { if (element is TableViewRow row) @@ -202,6 +215,14 @@ protected override void PrepareContainerForItemOverride(DependencyObject element { row.ApplyCurrentCellState(CurrentCellSlot.Value); } + + // Apply editing highlight when the editing row scrolls into view + var rowIndex = Items.IndexOf(item); + if (_editingHighlightRowIndex >= 0 && rowIndex == _editingHighlightRowIndex) + { + _editingHighlightRow = row; + row.ApplyEditingHighlight(true); + } } }); } @@ -277,7 +298,7 @@ private async Task HandleNavigations(KeyRoutedEventArgs e, bool shiftKey, bool c do { - newSlot = GetNextSlot(newSlot, shiftKey, e.Key is VirtualKey.Enter || SelectionUnit is TableViewSelectionUnit.Row); + newSlot = GetNextSlot(newSlot, shiftKey, e.Key is VirtualKey.Enter || (e.Key is VirtualKey.Tab && SelectionUnit is TableViewSelectionUnit.Row)); } while (isEditing && Columns[newSlot.Column].IsReadOnly); @@ -289,15 +310,20 @@ private async Task HandleNavigations(KeyRoutedEventArgs e, bool shiftKey, bool c { SetIsEditing(false); } - else if (SelectionUnit is TableViewSelectionUnit.Row && newSlot.Row != currentCell.Slot.Row) + else if (SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow && newSlot.Row != currentCell.Slot.Row) { // Editing moved to a different row — move the highlight _editingHighlightRow?.ApplyEditingHighlight(false); + _editingHighlightRowIndex = newSlot.Row; if (ContainerFromIndex(newSlot.Row) is TableViewRow newRow) { _editingHighlightRow = newRow; newRow.ApplyEditingHighlight(true); } + else + { + _editingHighlightRow = null; + } } } @@ -2265,18 +2291,23 @@ internal void SetIsEditing(bool value) IsEditing = value; UpdateCornerButtonState(); - if (value && SelectionUnit is TableViewSelectionUnit.Row) + if (value && SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow) { - if (CurrentCellSlot.HasValue && ContainerFromIndex(CurrentCellSlot.Value.Row) is TableViewRow row) + if (CurrentCellSlot.HasValue) { - _editingHighlightRow = row; - row.ApplyEditingHighlight(true); + _editingHighlightRowIndex = CurrentCellSlot.Value.Row; + if (ContainerFromIndex(CurrentCellSlot.Value.Row) is TableViewRow row) + { + _editingHighlightRow = row; + row.ApplyEditingHighlight(true); + } } } else if (!value) { _editingHighlightRow?.ApplyEditingHighlight(false); _editingHighlightRow = null; + _editingHighlightRowIndex = -1; } } diff --git a/src/TableViewCell.cs b/src/TableViewCell.cs index b05423a9..620bdec0 100644 --- a/src/TableViewCell.cs +++ b/src/TableViewCell.cs @@ -328,7 +328,7 @@ protected override void OnPointerEntered(PointerRoutedEventArgs e) if ((TableView?.SelectionMode is not ListViewSelectionMode.None && TableView?.SelectionUnit is not TableViewSelectionUnit.Row) || !TableView.IsReadOnly - || (TableView?.SelectionUnit is TableViewSelectionUnit.Row && !IsReadOnly)) + || (TableView?.SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow && !IsReadOnly)) { VisualStates.GoToState(this, false, VisualStates.StatePointerOver); } @@ -342,7 +342,7 @@ protected override void OnPointerExited(PointerRoutedEventArgs e) if ((TableView?.SelectionMode is not ListViewSelectionMode.None && TableView?.SelectionUnit is not TableViewSelectionUnit.Row) || !TableView.IsReadOnly - || (TableView?.SelectionUnit is TableViewSelectionUnit.Row && !IsReadOnly)) + || (TableView?.SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow && !IsReadOnly)) { VisualStates.GoToState(this, false, VisualStates.StateNormal); } @@ -374,6 +374,16 @@ protected override async void OnTapped(TappedRoutedEventArgs e) MakeSelection(); e.Handled = true; } + else if (TableView?.SelectionUnit is TableViewSelectionUnit.CellOrRow + && !IsReadOnly + && TableView is not null + && !TableView.IsEditing + && Column?.UseSingleElement is not true) + { + // Second tap on an already-selected cell in CellOrRow mode — start editing + // (like File Explorer's tap-pause-tap to rename). + e.Handled = await BeginCellEditing(e); + } } /// diff --git a/src/TableViewRow.cs b/src/TableViewRow.cs index 28d19566..e4a07c58 100644 --- a/src/TableViewRow.cs +++ b/src/TableViewRow.cs @@ -37,7 +37,8 @@ public partial class TableViewRow : ListViewItem private ListViewItemPresenter? _itemPresenter; private Border? _selectionBackground; private bool _ensureCells = true; - private bool _isEditing; + private bool _hasEditingHighlight; + private bool _isBeginningEdit; private Brush? _cellPresenterBackground; private Brush? _cellPresenterForeground; @@ -233,10 +234,13 @@ protected override async void OnTapped(TappedRoutedEventArgs e) && e.OriginalSource is DependencyObject source && source.FindAscendant() is { IsReadOnly: false } cell && !TableView.IsEditing + && !_isBeginningEdit && cell.Column?.UseSingleElement is not true) { + _isBeginningEdit = true; TableView.MakeSelection(cell.Slot, false); e.Handled = await cell.BeginCellEditing(e); + _isBeginningEdit = false; } } @@ -265,10 +269,13 @@ protected override async void OnDoubleTapped(DoubleTappedRoutedEventArgs e) && e.OriginalSource is DependencyObject source && source.FindAscendant() is { IsReadOnly: false } cell && !TableView.IsEditing + && !_isBeginningEdit && cell.Column?.UseSingleElement is not true) { + _isBeginningEdit = true; TableView.MakeSelection(cell.Slot, false); e.Handled = await cell.BeginCellEditing(e); + _isBeginningEdit = false; return; } @@ -689,7 +696,7 @@ private async void EnsureSelectionIndicatorPosition(double detailsHeight, Border /// internal void EnsureAlternateColors() { - if (TableView is null || RowPresenter is null || _isEditing) return; + if (TableView is null || RowPresenter is null || _hasEditingHighlight) return; RowPresenter.Background = Index % 2 == 1 && TableView.AlternateRowBackground is not null ? TableView.AlternateRowBackground : _cellPresenterBackground; @@ -713,14 +720,14 @@ internal void UpdateSelectCheckMarkOpacity() /// internal void ApplyEditingHighlight(bool isEditing) { - _isEditing = isEditing; + _hasEditingHighlight = isEditing; if (isEditing) { #if WINDOWS - if (RowPresenter is not null) + if (RowPresenter is not null && _itemPresenter?.PointerOverBackground is { } pointerOverBrush) { - RowPresenter.Background = _itemPresenter?.PointerOverBackground; + RowPresenter.Background = pointerOverBrush; } #else if (_selectionBackground is not null) @@ -731,7 +738,12 @@ internal void ApplyEditingHighlight(bool isEditing) } else { -#if !WINDOWS +#if WINDOWS + if (RowPresenter is not null) + { + RowPresenter.Background = _cellPresenterBackground; + } +#else if (_selectionBackground is not null) { _selectionBackground.Opacity = IsSelected ? 1 : 0; diff --git a/tests/TableViewRowEditingTests.cs b/tests/TableViewRowEditingTests.cs index 1fe88b2c..476c73d5 100644 --- a/tests/TableViewRowEditingTests.cs +++ b/tests/TableViewRowEditingTests.cs @@ -32,6 +32,70 @@ public void SetIsEditing_TogglesInRowMode() Assert.IsFalse(tableView.IsEditing); } + [UITestMethod] + public void SetIsEditing_DoesNotHighlightInCellMode() + { + var tableView = new TableView + { + AutoGenerateColumns = false, + SelectionMode = ListViewSelectionMode.Single, + SelectionUnit = TableViewSelectionUnit.Cell, + }; + + tableView.Columns.Add(new TableViewTextColumn { Header = "Name" }); + + // In Cell mode, SetIsEditing should not attempt to apply highlight + tableView.SetIsEditing(true); + Assert.IsTrue(tableView.IsEditing); + + tableView.SetIsEditing(false); + Assert.IsFalse(tableView.IsEditing); + } + + [UITestMethod] + public void SetIsEditing_TracksRowIndexForHighlight() + { + var tableView = new TableView + { + AutoGenerateColumns = false, + SelectionMode = ListViewSelectionMode.Single, + SelectionUnit = TableViewSelectionUnit.Row, + }; + + tableView.Columns.Add(new TableViewTextColumn { Header = "Name" }); + + // Set CurrentCellSlot so SetIsEditing can track the row index + tableView.CurrentCellSlot = new TableViewCellSlot(2, 0); + + tableView.SetIsEditing(true); + Assert.IsTrue(tableView.IsEditing); + + // ContainerFromIndex returns null without a visual tree, + // but the row index should still be tracked internally + tableView.SetIsEditing(false); + Assert.IsFalse(tableView.IsEditing); + } + + [UITestMethod] + public void SetIsEditing_HighlightsInCellOrRowMode() + { + var tableView = new TableView + { + AutoGenerateColumns = false, + SelectionMode = ListViewSelectionMode.Single, + SelectionUnit = TableViewSelectionUnit.CellOrRow, + }; + + tableView.Columns.Add(new TableViewTextColumn { Header = "Name" }); + + // CellOrRow mode should also attempt highlight (same as Row mode) + tableView.SetIsEditing(true); + Assert.IsTrue(tableView.IsEditing); + + tableView.SetIsEditing(false); + Assert.IsFalse(tableView.IsEditing); + } + [UITestMethod] public void ApplyEditingHighlight_Toggles() { From 5dc6c0c00794f157cada5b58b8c442ec41d77eaa Mon Sep 17 00:00:00 2001 From: rashmithakur Date: Tue, 7 Apr 2026 17:12:25 +0530 Subject: [PATCH 5/8] fix filtering issues after cell enters editing mode --- src/ColumnFilterHandler.cs | 1 + src/TableView.cs | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/ColumnFilterHandler.cs b/src/ColumnFilterHandler.cs index 9c6e94cd..9b5d5046 100644 --- a/src/ColumnFilterHandler.cs +++ b/src/ColumnFilterHandler.cs @@ -112,6 +112,7 @@ public virtual void ApplyFilter(TableViewColumn column) if (column is { TableView.CollectionView: CollectionView { } collectionView }) { using var defer = collectionView.DeferRefresh(); + column.TableView.CancelEditing(); column.TableView.DeselectAll(); if (!column.IsFiltered) diff --git a/src/TableView.cs b/src/TableView.cs index faff6d2e..87b97003 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -211,6 +211,13 @@ protected override void PrepareContainerForItemOverride(DependencyObject element row.EnsureCellsStyle(default, item); row.ApplyCellsSelectionState(); + // Reset current cell border on all cells in recycled containers + // to clear stale "Current" visual state from previous use. + foreach (var cell in row.Cells) + { + cell.ApplyCurrentCellState(); + } + if (CurrentCellSlot.HasValue) { row.ApplyCurrentCellState(CurrentCellSlot.Value); @@ -1576,6 +1583,7 @@ public void RefreshView() /// public void RefreshSorting() { + CancelEditing(); DeselectAll(); _collectionView.RefreshSorting(); } @@ -1585,6 +1593,7 @@ public void RefreshSorting() /// public void ClearAllSorting() { + CancelEditing(); DeselectAll(); SortDescriptions.Clear(); @@ -1626,6 +1635,7 @@ public void ClearAllFilters() /// public void RefreshFilter() { + CancelEditing(); DeselectAll(); _collectionView.RefreshFilter(); } @@ -1726,11 +1736,12 @@ private void DeselectAllItems() /// private void DeselectAllCells() { + CurrentCellSlot = null; + if (SelectedCellRanges.Count is 0) return; SelectedCellRanges.Clear(); OnCellSelectionChanged(); - CurrentCellSlot = null; } /// @@ -2311,6 +2322,26 @@ internal void SetIsEditing(bool value) } } + /// + /// Cancels any active cell editing, restores the cell to display mode, + /// and clears the editing highlight. + /// + internal void CancelEditing() + { + if (!IsEditing) return; + + if (CurrentCellSlot.HasValue) + { + var currentCell = GetCellFromSlot(CurrentCellSlot.Value); + if (currentCell is not null) + { + EndCellEditing(TableViewEditAction.Cancel, currentCell); + } + } + + SetIsEditing(false); + } + /// /// Sets the visibility of the headers. /// From 17d70e1add603ee231261665cd8fb5b0b173ae06 Mon Sep 17 00:00:00 2001 From: rashmithakur Date: Wed, 8 Apr 2026 16:28:30 +0530 Subject: [PATCH 6/8] Revert "fix filtering issues after cell enters editing mode" This reverts commit 5dc6c0c00794f157cada5b58b8c442ec41d77eaa. --- src/ColumnFilterHandler.cs | 1 - src/TableView.cs | 33 +-------------------------------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/ColumnFilterHandler.cs b/src/ColumnFilterHandler.cs index 9b5d5046..9c6e94cd 100644 --- a/src/ColumnFilterHandler.cs +++ b/src/ColumnFilterHandler.cs @@ -112,7 +112,6 @@ public virtual void ApplyFilter(TableViewColumn column) if (column is { TableView.CollectionView: CollectionView { } collectionView }) { using var defer = collectionView.DeferRefresh(); - column.TableView.CancelEditing(); column.TableView.DeselectAll(); if (!column.IsFiltered) diff --git a/src/TableView.cs b/src/TableView.cs index a52b5637..1785d6ee 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -217,13 +217,6 @@ protected override void PrepareContainerForItemOverride(DependencyObject element row.EnsureCellsStyle(default, item); row.ApplyCellsSelectionState(); - // Reset current cell border on all cells in recycled containers - // to clear stale "Current" visual state from previous use. - foreach (var cell in row.Cells) - { - cell.ApplyCurrentCellState(); - } - if (CurrentCellSlot.HasValue) { row.ApplyCurrentCellState(CurrentCellSlot.Value); @@ -1611,7 +1604,6 @@ public void RefreshView() /// public void RefreshSorting() { - CancelEditing(); DeselectAll(); _collectionView.RefreshSorting(); } @@ -1621,7 +1613,6 @@ public void RefreshSorting() /// public void ClearAllSorting() { - CancelEditing(); DeselectAll(); SortDescriptions.Clear(); @@ -1663,7 +1654,6 @@ public void ClearAllFilters() /// public void RefreshFilter() { - CancelEditing(); DeselectAll(); _collectionView.RefreshFilter(); } @@ -1764,12 +1754,11 @@ private void DeselectAllItems() /// private void DeselectAllCells() { - CurrentCellSlot = null; - if (SelectedCellRanges.Count is 0) return; SelectedCellRanges.Clear(); OnCellSelectionChanged(); + CurrentCellSlot = null; } /// @@ -2514,26 +2503,6 @@ internal void SetIsEditing(bool value) } } - /// - /// Cancels any active cell editing, restores the cell to display mode, - /// and clears the editing highlight. - /// - internal void CancelEditing() - { - if (!IsEditing) return; - - if (CurrentCellSlot.HasValue) - { - var currentCell = GetCellFromSlot(CurrentCellSlot.Value); - if (currentCell is not null) - { - EndCellEditing(TableViewEditAction.Cancel, currentCell); - } - } - - SetIsEditing(false); - } - /// /// Sets the visibility of the headers. /// From e013c6c3c633282c5f9bb01571d61302d8b5343c Mon Sep 17 00:00:00 2001 From: rashmithakur Date: Wed, 8 Apr 2026 16:32:32 +0530 Subject: [PATCH 7/8] restore virtualization fix --- src/TableView.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/TableView.cs b/src/TableView.cs index 1785d6ee..ee782fc7 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -217,6 +217,13 @@ protected override void PrepareContainerForItemOverride(DependencyObject element row.EnsureCellsStyle(default, item); row.ApplyCellsSelectionState(); + // Reset current cell border on all cells in recycled containers + // to clear stale "Current" visual state from previous use. + foreach (var cell in row.Cells) + { + cell.ApplyCurrentCellState(); + } + if (CurrentCellSlot.HasValue) { row.ApplyCurrentCellState(CurrentCellSlot.Value); From a79c13b85fb70a4f0629ad4b28394a3451b4aea1 Mon Sep 17 00:00:00 2001 From: rashmithakur Date: Wed, 8 Apr 2026 17:11:32 +0530 Subject: [PATCH 8/8] add more comprehensive tests --- tests/TableViewRowEditingTests.cs | 265 +++++++++++++++++++++++------- 1 file changed, 206 insertions(+), 59 deletions(-) diff --git a/tests/TableViewRowEditingTests.cs b/tests/TableViewRowEditingTests.cs index 476c73d5..0ad526c3 100644 --- a/tests/TableViewRowEditingTests.cs +++ b/tests/TableViewRowEditingTests.cs @@ -1,110 +1,257 @@ using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer; +using System.Collections.ObjectModel; +using System.Threading.Tasks; namespace WinUI.TableView.Tests; +/// +/// Tests for the row-editing feature: tap-to-edit in Row and CellOrRow modes, +/// editing highlight, pointer hover, and virtualization. +/// [TestClass] public class TableViewRowEditingTests { + // Scenario 1 & 2: Tap-to-edit in Row mode — editing highlight applied to row [UITestMethod] - public void SetIsEditing_TogglesInRowMode() + public async Task RowMode_EditingHighlight_AppliedToRow() { - var tableView = new TableView - { - AutoGenerateColumns = false, - SelectionMode = ListViewSelectionMode.Single, - SelectionUnit = TableViewSelectionUnit.Row, - }; + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.Row, items); - tableView.Columns.Add(new TableViewTextColumn { Header = "Name" }); + await LoadAsync(tableView); + try + { + tableView.CurrentCellSlot = new TableViewCellSlot(1, 0); + tableView.SetIsEditing(true); - Assert.IsFalse(tableView.IsEditing); + var row = tableView.ContainerFromIndex(1) as TableViewRow; + Assert.IsNotNull(row); + // Editing highlight should block EnsureAlternateColors + row.EnsureAlternateColors(); - tableView.SetIsEditing(true); - Assert.IsTrue(tableView.IsEditing); + tableView.SetIsEditing(false); + // After stopping, alternate colors should apply normally + row.EnsureAlternateColors(); + } + finally + { + await UnloadAsync(tableView); + } + } - // Calling again with same value should be a no-op - tableView.SetIsEditing(true); - Assert.IsTrue(tableView.IsEditing); + // Scenario 5: Pointer hover effects — CellOrRow mode prerequisites + [UITestMethod] + public async Task CellOrRowMode_PointerHover_Prerequisites() + { + var items = CreateTestItems(3); + var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items); - tableView.SetIsEditing(false); - Assert.IsFalse(tableView.IsEditing); + await LoadAsync(tableView); + try + { + // OnPointerEntered checks: SelectionUnit is Row or CellOrRow && !IsReadOnly + Assert.AreEqual(TableViewSelectionUnit.CellOrRow, tableView.SelectionUnit); + Assert.IsFalse(tableView.IsReadOnly); + } + finally + { + await UnloadAsync(tableView); + } } + // Scenario 6: Tap-to-edit in CellOrRow — highlight + prerequisites [UITestMethod] - public void SetIsEditing_DoesNotHighlightInCellMode() + public async Task CellOrRowMode_EditingHighlight_AppliedToRow() { - var tableView = new TableView + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items); + + await LoadAsync(tableView); + try { - AutoGenerateColumns = false, - SelectionMode = ListViewSelectionMode.Single, - SelectionUnit = TableViewSelectionUnit.Cell, - }; + tableView.CurrentCellSlot = new TableViewCellSlot(2, 0); + tableView.SetIsEditing(true); - tableView.Columns.Add(new TableViewTextColumn { Header = "Name" }); + var row = tableView.ContainerFromIndex(2) as TableViewRow; + Assert.IsNotNull(row); + row.EnsureAlternateColors(); // should not override highlight + + tableView.SetIsEditing(false); + row.EnsureAlternateColors(); // should apply normally now + } + finally + { + await UnloadAsync(tableView); + } + } + + [UITestMethod] + public async Task CellOrRowMode_TapToEdit_Prerequisites() + { + var items = CreateTestItems(3); + var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items); - // In Cell mode, SetIsEditing should not attempt to apply highlight - tableView.SetIsEditing(true); - Assert.IsTrue(tableView.IsEditing); + await LoadAsync(tableView); + try + { + // TableViewCell.OnTapped checks these conditions for second-tap editing + Assert.AreEqual(TableViewSelectionUnit.CellOrRow, tableView.SelectionUnit); + Assert.IsFalse(tableView.IsReadOnly); + Assert.IsFalse(tableView.IsEditing); - tableView.SetIsEditing(false); - Assert.IsFalse(tableView.IsEditing); + foreach (var column in tableView.Columns) + { + Assert.IsFalse(column.UseSingleElement); + } + } + finally + { + await UnloadAsync(tableView); + } } + // Scenario 7: Virtualization — editing highlight cleared on recycled row [UITestMethod] - public void SetIsEditing_TracksRowIndexForHighlight() + public async Task Virtualization_EditingHighlight_ClearedOnRecycle() { - var tableView = new TableView + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.Row, items); + + await LoadAsync(tableView); + try { - AutoGenerateColumns = false, - SelectionMode = ListViewSelectionMode.Single, - SelectionUnit = TableViewSelectionUnit.Row, - }; + tableView.CurrentCellSlot = new TableViewCellSlot(0, 0); + tableView.SetIsEditing(true); + + tableView.SetIsEditing(false); + tableView.CurrentCellSlot = null; + Assert.IsNull(tableView.CurrentCellSlot); - tableView.Columns.Add(new TableViewTextColumn { Header = "Name" }); + var row = tableView.ContainerFromIndex(0) as TableViewRow; + Assert.IsNotNull(row); + row.EnsureAlternateColors(); // should work normally after edit cleared + } + finally + { + await UnloadAsync(tableView); + } + } - // Set CurrentCellSlot so SetIsEditing can track the row index - tableView.CurrentCellSlot = new TableViewCellSlot(2, 0); + // Scenario 7: Virtualization — cell "Current" border reset on recycled container + [UITestMethod] + public async Task Virtualization_CellCurrentState_ResetOnPrepare() + { + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.Row, items); - tableView.SetIsEditing(true); - Assert.IsTrue(tableView.IsEditing); + await LoadAsync(tableView); + try + { + tableView.CurrentCellSlot = new TableViewCellSlot(1, 0); + tableView.CurrentCellSlot = null; - // ContainerFromIndex returns null without a visual tree, - // but the row index should still be tracked internally - tableView.SetIsEditing(false); - Assert.IsFalse(tableView.IsEditing); + // Simulates PrepareContainerForItemOverride resetting stale borders + var row = tableView.ContainerFromIndex(1) as TableViewRow; + Assert.IsNotNull(row); + foreach (var cell in row.Cells) + { + cell.ApplyCurrentCellState(); + } + } + finally + { + await UnloadAsync(tableView); + } } + // Cross-cutting: stop editing clears state, switch rows, restart cleanly [UITestMethod] - public void SetIsEditing_HighlightsInCellOrRowMode() + public async Task EditingState_SwitchBetweenRows_ClearsAndRestarts() + { + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.Row, items); + + await LoadAsync(tableView); + try + { + // Edit row 0 + tableView.CurrentCellSlot = new TableViewCellSlot(0, 0); + tableView.SetIsEditing(true); + Assert.IsTrue(tableView.IsEditing); + + // Stop — should fully clear state + tableView.SetIsEditing(false); + Assert.IsFalse(tableView.IsEditing); + + // Switch to row 3, different column — no leftover state + tableView.CurrentCellSlot = new TableViewCellSlot(3, 1); + tableView.SetIsEditing(true); + Assert.IsTrue(tableView.IsEditing); + Assert.AreEqual(3, tableView.CurrentCellSlot?.Row); + + tableView.SetIsEditing(false); + Assert.IsFalse(tableView.IsEditing); + } + finally + { + await UnloadAsync(tableView); + } + } + + // ── Helpers ── + + private static TableView CreateTableView( + TableViewSelectionUnit selectionUnit, + ObservableCollection? items = null) { var tableView = new TableView { AutoGenerateColumns = false, SelectionMode = ListViewSelectionMode.Single, - SelectionUnit = TableViewSelectionUnit.CellOrRow, + SelectionUnit = selectionUnit, }; - tableView.Columns.Add(new TableViewTextColumn { Header = "Name" }); + tableView.Columns.Add(new TableViewTextColumn + { + Header = "Name", + Binding = new Binding { Path = new PropertyPath("Name") } + }); + + tableView.Columns.Add(new TableViewNumberColumn + { + Header = "Value", + Binding = new Binding { Path = new PropertyPath("Value") } + }); + + if (items is not null) + { + tableView.ItemsSource = items; + } - // CellOrRow mode should also attempt highlight (same as Row mode) - tableView.SetIsEditing(true); - Assert.IsTrue(tableView.IsEditing); + return tableView; + } - tableView.SetIsEditing(false); - Assert.IsFalse(tableView.IsEditing); + private static ObservableCollection CreateTestItems(int count) + { + var list = new ObservableCollection(); + for (int i = 0; i < count; i++) + { + list.Add(new TestItem { Id = i, Name = $"Item{i}", Value = count - i }); + } + return list; } - [UITestMethod] - public void ApplyEditingHighlight_Toggles() + private static Task LoadAsync(FrameworkElement content) { - var row = new TableViewRow(); + return UnitTestApp.Current.MainWindow.LoadTestContentAsync(content); + } - // Apply and clear highlight multiple times — should not crash - row.ApplyEditingHighlight(true); - row.ApplyEditingHighlight(false); - row.ApplyEditingHighlight(true); - row.ApplyEditingHighlight(false); + private static Task UnloadAsync(FrameworkElement content) + { + return UnitTestApp.Current.MainWindow.UnloadTestContentAsync(content); } }