From 72647094929d6a4c265f0022f53695836b038a82 Mon Sep 17 00:00:00 2001 From: rashmithakur Date: Thu, 9 Apr 2026 17:05:10 +0530 Subject: [PATCH 1/2] cell editing in row mode highlighting fixes --- src/TableView.cs | 64 +++++++- src/TableViewCell.cs | 16 +- src/TableViewRow.cs | 78 +++++++++- tests/TableViewRowEditingTests.cs | 247 ++++++++++++++++++++++++++++++ 4 files changed, 399 insertions(+), 6 deletions(-) create mode 100644 tests/TableViewRowEditingTests.cs diff --git a/src/TableView.cs b/src/TableView.cs index e21a6d3e..256d900b 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -35,6 +35,8 @@ public partial class TableView : ListView private RowDefinition? _headerRowDefinition; private bool _shouldThrowSelectionModeChangedException; private bool _ensureColumns = true; + private TableViewRow? _editingHighlightRow; + private int _editingHighlightRowIndex = -1; private readonly List _rows = []; private readonly CollectionView _collectionView = []; @@ -103,6 +105,17 @@ 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) @@ -111,10 +124,25 @@ protected override void PrepareContainerForItemOverride(DependencyObject element row.ApplyCellsSelectionState(); row.RowPresenter?.ApplyDetailsPaneState(item); + // 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); } + + // 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); + } } }); } @@ -184,7 +212,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 || (e.Key is VirtualKey.Tab && SelectionUnit is TableViewSelectionUnit.Row)); } while (isEditing && Columns[newSlot.Column].IsReadOnly); @@ -196,6 +224,21 @@ private async Task HandleNavigations(KeyRoutedEventArgs e, bool shiftKey, bool c { SetIsEditing(false); } + 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; + } + } } MakeSelection(newSlot, false); @@ -1502,6 +1545,25 @@ internal void SetIsEditing(bool value) IsEditing = value; UpdateCornerButtonState(); + + if (value && SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow) + { + if (CurrentCellSlot.HasValue) + { + _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 758c3b14..82505ddc 100644 --- a/src/TableViewCell.cs +++ b/src/TableViewCell.cs @@ -190,7 +190,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 or TableViewSelectionUnit.CellOrRow && !IsReadOnly)) { VisualStates.GoToState(this, false, VisualStates.StatePointerOver); } @@ -203,7 +204,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 or TableViewSelectionUnit.CellOrRow && !IsReadOnly)) { VisualStates.GoToState(this, false, VisualStates.StateNormal); } @@ -229,6 +231,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 d403a149..a398df02 100644 --- a/src/TableViewRow.cs +++ b/src/TableViewRow.cs @@ -37,6 +37,8 @@ public partial class TableViewRow : ListViewItem private ListViewItemPresenter? _itemPresenter; private Border? _selectionBackground; private bool _ensureCells = true; + private bool _hasEditingHighlight; + private bool _isBeginningEdit; private Brush? _cellPresenterBackground; private Brush? _cellPresenterForeground; @@ -177,7 +179,7 @@ protected override void OnPointerReleased(PointerRoutedEventArgs e) } /// - protected override void OnTapped(TappedRoutedEventArgs e) + protected override async void OnTapped(TappedRoutedEventArgs e) { base.OnTapped(e); @@ -186,15 +188,48 @@ 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 + && !_isBeginningEdit + && cell.Column?.UseSingleElement is not true) + { + _isBeginningEdit = true; + TableView.MakeSelection(cell.Slot, false); + e.Handled = await cell.BeginCellEditing(e); + _isBeginningEdit = false; + } } /// - protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e) + protected override async void OnDoubleTapped(DoubleTappedRoutedEventArgs e) { var eventArgs = new TableViewRowDoubleTappedEventArgs(Index, this, Content); TableView?.OnRowDoubleTapped(eventArgs); e.Handled = eventArgs.Handled; + if (e.Handled) { base.OnDoubleTapped(e); return; } + + if (TableView?.SelectionUnit is TableViewSelectionUnit.Row + && 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; + } + base.OnDoubleTapped(e); } @@ -584,7 +619,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 || _hasEditingHighlight) return; RowPresenter.Background = Index % 2 == 1 && TableView.AlternateRowBackground is not null ? TableView.AlternateRowBackground : _cellPresenterBackground; @@ -603,6 +638,43 @@ internal void UpdateSelectCheckMarkOpacity() } } + /// + /// Highlights or unhighlights the row to indicate that a cell is being edited. + /// + internal void ApplyEditingHighlight(bool isEditing) + { + _hasEditingHighlight = isEditing; + if (isEditing) + { +#if WINDOWS + if (RowPresenter is not null && _itemPresenter?.PointerOverBackground is { } pointerOverBrush) + { + RowPresenter.Background = pointerOverBrush; + } +#else + if (_selectionBackground is not null) + { + _selectionBackground.Opacity = 1; + } +#endif + } + else + { +#if WINDOWS + if (RowPresenter is not null) + { + RowPresenter.Background = _cellPresenterBackground; + } +#else + if (_selectionBackground is not null) + { + _selectionBackground.Opacity = IsSelected ? 1 : 0; + } +#endif + EnsureAlternateColors(); + } + } + /// /// Gets the height of the horizontal gridlines. /// diff --git a/tests/TableViewRowEditingTests.cs b/tests/TableViewRowEditingTests.cs new file mode 100644 index 00000000..3f43de97 --- /dev/null +++ b/tests/TableViewRowEditingTests.cs @@ -0,0 +1,247 @@ +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 +{ + [UITestMethod] + public async Task RowMode_EditingHighlight_AppliedToRow() + { + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.Row, items); + await LoadAsync(tableView); + + try + { + tableView.CurrentCellSlot = new TableViewCellSlot(1, 0); + tableView.SetIsEditing(true); + + var row = tableView.ContainerFromIndex(1) as TableViewRow; + Assert.IsNotNull(row); + + row.EnsureAlternateColors(); + + tableView.SetIsEditing(false); + + row.EnsureAlternateColors(); + } + finally + { + await UnloadAsync(tableView); + } + } + + [UITestMethod] + public async Task CellOrRowMode_PointerHover_Prerequisites() + { + var items = CreateTestItems(3); + var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items); + await LoadAsync(tableView); + + try + { + Assert.AreEqual(TableViewSelectionUnit.CellOrRow, tableView.SelectionUnit); + Assert.IsFalse(tableView.IsReadOnly); + } + finally + { + await UnloadAsync(tableView); + } + } + + [UITestMethod] + public async Task CellOrRowMode_EditingHighlight_AppliedToRow() + { + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items); + await LoadAsync(tableView); + + try + { + tableView.CurrentCellSlot = new TableViewCellSlot(2, 0); + tableView.SetIsEditing(true); + + var row = tableView.ContainerFromIndex(2) as TableViewRow; + Assert.IsNotNull(row); + + row.EnsureAlternateColors(); + + tableView.SetIsEditing(false); + + row.EnsureAlternateColors(); + } + finally + { + await UnloadAsync(tableView); + } + } + + [UITestMethod] + public async Task CellOrRowMode_TapToEdit_Prerequisites() + { + var items = CreateTestItems(3); + var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items); + await LoadAsync(tableView); + + try + { + Assert.AreEqual(TableViewSelectionUnit.CellOrRow, tableView.SelectionUnit); + Assert.IsFalse(tableView.IsReadOnly); + Assert.IsFalse(tableView.IsEditing); + + foreach (var column in tableView.Columns) + { + Assert.IsFalse(column.UseSingleElement); + } + } + finally + { + await UnloadAsync(tableView); + } + } + + [UITestMethod] + public async Task Virtualization_EditingHighlight_ClearedOnRecycle() + { + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.Row, items); + await LoadAsync(tableView); + + try + { + tableView.CurrentCellSlot = new TableViewCellSlot(0, 0); + tableView.SetIsEditing(true); + tableView.SetIsEditing(false); + tableView.CurrentCellSlot = null; + + Assert.IsNull(tableView.CurrentCellSlot); + + var row = tableView.ContainerFromIndex(0) as TableViewRow; + Assert.IsNotNull(row); + + row.EnsureAlternateColors(); + } + finally + { + await UnloadAsync(tableView); + } + } + + [UITestMethod] + public async Task Virtualization_CellCurrentState_ResetOnPrepare() + { + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.Row, items); + await LoadAsync(tableView); + + try + { + tableView.CurrentCellSlot = new TableViewCellSlot(1, 0); + tableView.CurrentCellSlot = null; + + var row = tableView.ContainerFromIndex(1) as TableViewRow; + Assert.IsNotNull(row); + + foreach (var cell in row.Cells) + { + cell.ApplyCurrentCellState(); + } + } + finally + { + await UnloadAsync(tableView); + } + } + + [UITestMethod] + public async Task EditingState_SwitchBetweenRows_ClearsAndRestarts() + { + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.Row, items); + await LoadAsync(tableView); + + try + { + tableView.CurrentCellSlot = new TableViewCellSlot(0, 0); + tableView.SetIsEditing(true); + Assert.IsTrue(tableView.IsEditing); + + tableView.SetIsEditing(false); + Assert.IsFalse(tableView.IsEditing); + + 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); + } + } + + private static TableView CreateTableView( + TableViewSelectionUnit selectionUnit, + ObservableCollection? items = null) + { + var tableView = new TableView + { + AutoGenerateColumns = false, + SelectionMode = ListViewSelectionMode.Single, + SelectionUnit = selectionUnit, + }; + + 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; + } + + return tableView; + } + + 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; + } + + private static Task LoadAsync(FrameworkElement content) + { + return UnitTestApp.Current.MainWindow.LoadTestContentAsync(content); + } + + private static Task UnloadAsync(FrameworkElement content) + { + return UnitTestApp.Current.MainWindow.UnloadTestContentAsync(content); + } +} From b8001a1ac55d23ab3c1afef542d36f267f859f0e Mon Sep 17 00:00:00 2001 From: rashmithakur Date: Wed, 15 Apr 2026 17:13:00 +0530 Subject: [PATCH 2/2] Add row editing highlight overlay, TapToEdit property, and lifecycle-driven cell editing --- .../Pages/SelectionPage.xaml | 4 + src/TableView.Properties.cs | 14 ++ src/TableView.cs | 50 ----- src/TableViewCell.cs | 34 +++- src/TableViewRow.cs | 72 +------- src/TableViewRowPresenter.cs | 13 ++ src/Themes/Resources.xaml | 3 + src/Themes/TableViewRowPresenter.xaml | 5 + tests/TableViewRowEditingTests.cs | 171 ++++++++++++------ 9 files changed, 187 insertions(+), 179 deletions(-) diff --git a/samples/WinUI.TableView.SampleApp/Pages/SelectionPage.xaml b/samples/WinUI.TableView.SampleApp/Pages/SelectionPage.xaml index fcafdbb0..2dcc8349 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}" /> + diff --git a/src/TableView.Properties.cs b/src/TableView.Properties.cs index d90e7d26..0907bd24 100644 --- a/src/TableView.Properties.cs +++ b/src/TableView.Properties.cs @@ -72,6 +72,11 @@ public partial class TableView /// public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(TableView), new PropertyMetadata(false, OnIsReadOnlyChanged)); + /// + /// Identifies the TapToEdit dependency property. + /// + public static readonly DependencyProperty TapToEditProperty = DependencyProperty.Register(nameof(TapToEdit), typeof(bool), typeof(TableView), new PropertyMetadata(false)); + /// /// Identifies the CornerButtonMode dependency property. /// @@ -485,6 +490,15 @@ public bool IsReadOnly set => SetValue(IsReadOnlyProperty, value); } + /// + /// Gets or sets a value indicating whether tapping an already-selected cell initiates editing. + /// + public bool TapToEdit + { + get => (bool)GetValue(TapToEditProperty); + set => SetValue(TapToEditProperty, value); + } + /// /// Gets or sets the mode of the corner button. /// diff --git a/src/TableView.cs b/src/TableView.cs index 256d900b..bcb6e918 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -35,8 +35,6 @@ public partial class TableView : ListView private RowDefinition? _headerRowDefinition; private bool _shouldThrowSelectionModeChangedException; private bool _ensureColumns = true; - private TableViewRow? _editingHighlightRow; - private int _editingHighlightRowIndex = -1; private readonly List _rows = []; private readonly CollectionView _collectionView = []; @@ -105,15 +103,9 @@ 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(() => @@ -135,14 +127,6 @@ 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); - } } }); } @@ -224,21 +208,6 @@ private async Task HandleNavigations(KeyRoutedEventArgs e, bool shiftKey, bool c { SetIsEditing(false); } - 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; - } - } } MakeSelection(newSlot, false); @@ -1545,25 +1514,6 @@ internal void SetIsEditing(bool value) IsEditing = value; UpdateCornerButtonState(); - - if (value && SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow) - { - if (CurrentCellSlot.HasValue) - { - _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 82505ddc..6778c2a5 100644 --- a/src/TableViewCell.cs +++ b/src/TableViewCell.cs @@ -226,20 +226,25 @@ protected override async void OnTapped(TappedRoutedEventArgs e) if (e.Handled) return; } - if (TableView?.CurrentCellSlot != Slot || TableView?.LastSelectionUnit is TableViewSelectionUnit.Row) - { - MakeSelection(); - e.Handled = true; - } - else if (TableView?.SelectionUnit is TableViewSelectionUnit.CellOrRow + if (TableView?.TapToEdit is true + && TableView.CurrentCellSlot == Slot && !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). + if (TableView.SelectionUnit is TableViewSelectionUnit.Row) + { + MakeSelection(); + } + e.Handled = await BeginCellEditing(e); + return; + } + + if (TableView?.CurrentCellSlot != Slot || TableView?.LastSelectionUnit is TableViewSelectionUnit.Row) + { + MakeSelection(); + e.Handled = true; } } @@ -410,6 +415,11 @@ internal void PrepareForEdit(RoutedEventArgs editingArgs) TableView.UpdateCornerButtonState(); } + if (TableView?.SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow) + { + Row?.ApplyEditingHighlight(true); + } + if (editingElement is { IsHitTestVisible: true }) { _editingArgs = editingArgs; @@ -456,6 +466,12 @@ private void OnEditingElementLoaded(object sender, RoutedEventArgs e) internal void EndEditing(TableViewEditAction editAction) { Column?.EndCellEditing(this, Row?.Content, editAction, _uneditedValue); + + if (TableView?.SelectionUnit is TableViewSelectionUnit.Row or TableViewSelectionUnit.CellOrRow) + { + Row?.ApplyEditingHighlight(false); + } + SetElement(); } diff --git a/src/TableViewRow.cs b/src/TableViewRow.cs index a398df02..80112a0e 100644 --- a/src/TableViewRow.cs +++ b/src/TableViewRow.cs @@ -37,8 +37,6 @@ public partial class TableViewRow : ListViewItem private ListViewItemPresenter? _itemPresenter; private Border? _selectionBackground; private bool _ensureCells = true; - private bool _hasEditingHighlight; - private bool _isBeginningEdit; private Brush? _cellPresenterBackground; private Brush? _cellPresenterForeground; @@ -179,7 +177,7 @@ protected override void OnPointerReleased(PointerRoutedEventArgs e) } /// - protected override async void OnTapped(TappedRoutedEventArgs e) + protected override void OnTapped(TappedRoutedEventArgs e) { base.OnTapped(e); @@ -188,48 +186,15 @@ protected override async 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 - && !_isBeginningEdit - && cell.Column?.UseSingleElement is not true) - { - _isBeginningEdit = true; - TableView.MakeSelection(cell.Slot, false); - e.Handled = await cell.BeginCellEditing(e); - _isBeginningEdit = false; - } } /// - protected override async void OnDoubleTapped(DoubleTappedRoutedEventArgs e) + protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e) { var eventArgs = new TableViewRowDoubleTappedEventArgs(Index, this, Content); TableView?.OnRowDoubleTapped(eventArgs); e.Handled = eventArgs.Handled; - if (e.Handled) { base.OnDoubleTapped(e); return; } - - if (TableView?.SelectionUnit is TableViewSelectionUnit.Row - && 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; - } - base.OnDoubleTapped(e); } @@ -619,7 +584,7 @@ private async void EnsureSelectionIndicatorPosition(double detailsHeight, Border /// internal void EnsureAlternateColors() { - if (TableView is null || RowPresenter is null || _hasEditingHighlight) return; + if (TableView is null || RowPresenter is null) return; RowPresenter.Background = Index % 2 == 1 && TableView.AlternateRowBackground is not null ? TableView.AlternateRowBackground : _cellPresenterBackground; @@ -643,36 +608,7 @@ internal void UpdateSelectCheckMarkOpacity() /// internal void ApplyEditingHighlight(bool isEditing) { - _hasEditingHighlight = isEditing; - if (isEditing) - { -#if WINDOWS - if (RowPresenter is not null && _itemPresenter?.PointerOverBackground is { } pointerOverBrush) - { - RowPresenter.Background = pointerOverBrush; - } -#else - if (_selectionBackground is not null) - { - _selectionBackground.Opacity = 1; - } -#endif - } - else - { -#if WINDOWS - if (RowPresenter is not null) - { - RowPresenter.Background = _cellPresenterBackground; - } -#else - if (_selectionBackground is not null) - { - _selectionBackground.Opacity = IsSelected ? 1 : 0; - } -#endif - EnsureAlternateColors(); - } + RowPresenter?.ApplyEditingHighlight(isEditing); } /// diff --git a/src/TableViewRowPresenter.cs b/src/TableViewRowPresenter.cs index 00f382eb..f0a0c8eb 100644 --- a/src/TableViewRowPresenter.cs +++ b/src/TableViewRowPresenter.cs @@ -33,6 +33,7 @@ public partial class TableViewRowPresenter : Control private ContentPresenter? _detailsPresenter; private ToggleButton? _detailsToggleButton; private ListViewItemPresenter? _itemPresenter; + private Border? _editingHighlightOverlay; /// /// Initializes a new instance of the class. @@ -56,6 +57,7 @@ protected override void OnApplyTemplate() _detailsPanel = GetTemplateChild("DetailsPanel") as Panel; _detailsPresenter = GetTemplateChild("DetailsPresenter") as ContentPresenter; _detailsToggleButton = GetTemplateChild("DetailsToggleButton") as ToggleButton; + _editingHighlightOverlay = GetTemplateChild("EditingHighlightOverlay") as Border; _itemPresenter = this.FindAscendant(); TableViewRow = this.FindAscendant(); @@ -239,6 +241,17 @@ internal void ApplyDetailsPaneState(object? item) } } + /// + /// Toggles the editing highlight overlay visibility. + /// + internal void ApplyEditingHighlight(bool isEditing) + { + if (_editingHighlightOverlay is not null) + { + _editingHighlightOverlay.Visibility = isEditing ? Visibility.Visible : Visibility.Collapsed; + } + } + /// /// Sets the DataTemplate for the row details. /// diff --git a/src/Themes/Resources.xaml b/src/Themes/Resources.xaml index 31d47b4a..9767bae8 100644 --- a/src/Themes/Resources.xaml +++ b/src/Themes/Resources.xaml @@ -73,6 +73,7 @@ + @@ -142,6 +143,7 @@ + @@ -211,6 +213,7 @@ + diff --git a/src/Themes/TableViewRowPresenter.xaml b/src/Themes/TableViewRowPresenter.xaml index 65dd7e88..7c43aa7f 100644 --- a/src/Themes/TableViewRowPresenter.xaml +++ b/src/Themes/TableViewRowPresenter.xaml @@ -135,6 +135,11 @@ + + -/// Tests for the row-editing feature: tap-to-edit in Row and CellOrRow modes, -/// editing highlight, pointer hover, and virtualization. +/// Tests for the row-editing feature: editing highlight via cell lifecycle, +/// TapToEdit property, overlay independence from alternate colors, and virtualization. /// [TestClass] public class TableViewRowEditingTests { [UITestMethod] - public async Task RowMode_EditingHighlight_AppliedToRow() + public async Task TapToEdit_DefaultsToFalse() + { + var items = CreateTestItems(3); + var tableView = CreateTableView(TableViewSelectionUnit.Cell, items); + await LoadAsync(tableView); + + try + { + Assert.IsFalse(tableView.TapToEdit); + } + finally + { + await UnloadAsync(tableView); + } + } + + [UITestMethod] + public async Task Lifecycle_BeginEditing_ShowsHighlight_RowMode() { var items = CreateTestItems(5); var tableView = CreateTableView(TableViewSelectionUnit.Row, items); @@ -24,17 +44,22 @@ public async Task RowMode_EditingHighlight_AppliedToRow() try { - tableView.CurrentCellSlot = new TableViewCellSlot(1, 0); - tableView.SetIsEditing(true); - var row = tableView.ContainerFromIndex(1) as TableViewRow; Assert.IsNotNull(row); + Assert.IsTrue(row.Cells.Count > 0); - row.EnsureAlternateColors(); + var cell = row.Cells[0]; + var overlay = row.FindDescendant(b => b.Name is "EditingHighlightOverlay"); + Assert.IsNotNull(overlay); + Assert.AreEqual(Visibility.Collapsed, overlay.Visibility); - tableView.SetIsEditing(false); + var started = await cell.BeginCellEditing(new RoutedEventArgs()); + Assert.IsTrue(started); + Assert.IsTrue(tableView.IsEditing); + Assert.AreEqual(Visibility.Visible, overlay.Visibility); - row.EnsureAlternateColors(); + tableView.EndCellEditing(TableViewEditAction.Cancel, cell); + Assert.AreEqual(Visibility.Collapsed, overlay.Visibility); } finally { @@ -43,16 +68,28 @@ public async Task RowMode_EditingHighlight_AppliedToRow() } [UITestMethod] - public async Task CellOrRowMode_PointerHover_Prerequisites() + public async Task Lifecycle_BeginEditing_ShowsHighlight_CellOrRowMode() { - var items = CreateTestItems(3); + var items = CreateTestItems(5); var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items); await LoadAsync(tableView); try { - Assert.AreEqual(TableViewSelectionUnit.CellOrRow, tableView.SelectionUnit); - Assert.IsFalse(tableView.IsReadOnly); + var row = tableView.ContainerFromIndex(2) as TableViewRow; + Assert.IsNotNull(row); + + var cell = row.Cells[0]; + var overlay = row.FindDescendant(b => b.Name is "EditingHighlightOverlay"); + Assert.IsNotNull(overlay); + Assert.AreEqual(Visibility.Collapsed, overlay.Visibility); + + var started = await cell.BeginCellEditing(new RoutedEventArgs()); + Assert.IsTrue(started); + Assert.AreEqual(Visibility.Visible, overlay.Visibility); + + tableView.EndCellEditing(TableViewEditAction.Cancel, cell); + Assert.AreEqual(Visibility.Collapsed, overlay.Visibility); } finally { @@ -61,25 +98,28 @@ public async Task CellOrRowMode_PointerHover_Prerequisites() } [UITestMethod] - public async Task CellOrRowMode_EditingHighlight_AppliedToRow() + public async Task Lifecycle_CellMode_NoHighlight() { var items = CreateTestItems(5); - var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items); + var tableView = CreateTableView(TableViewSelectionUnit.Cell, items); await LoadAsync(tableView); try { - tableView.CurrentCellSlot = new TableViewCellSlot(2, 0); - tableView.SetIsEditing(true); - - var row = tableView.ContainerFromIndex(2) as TableViewRow; + var row = tableView.ContainerFromIndex(1) as TableViewRow; Assert.IsNotNull(row); - row.EnsureAlternateColors(); + var cell = row.Cells[0]; + var overlay = row.FindDescendant(b => b.Name is "EditingHighlightOverlay"); + Assert.IsNotNull(overlay); - tableView.SetIsEditing(false); + var started = await cell.BeginCellEditing(new RoutedEventArgs()); + Assert.IsTrue(started); + Assert.IsTrue(tableView.IsEditing); + Assert.AreEqual(Visibility.Collapsed, overlay.Visibility); - row.EnsureAlternateColors(); + tableView.EndCellEditing(TableViewEditAction.Cancel, cell); + Assert.AreEqual(Visibility.Collapsed, overlay.Visibility); } finally { @@ -88,22 +128,34 @@ public async Task CellOrRowMode_EditingHighlight_AppliedToRow() } [UITestMethod] - public async Task CellOrRowMode_TapToEdit_Prerequisites() + public async Task AlternateColors_PreservedDuringEditingHighlight() { - var items = CreateTestItems(3); - var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items); + var items = CreateTestItems(5); + var tableView = CreateTableView(TableViewSelectionUnit.Row, items); + var alternateBrush = new SolidColorBrush(Colors.LightGray); + tableView.AlternateRowBackground = alternateBrush; await LoadAsync(tableView); try { - Assert.AreEqual(TableViewSelectionUnit.CellOrRow, tableView.SelectionUnit); - Assert.IsFalse(tableView.IsReadOnly); - Assert.IsFalse(tableView.IsEditing); + var oddRow = tableView.ContainerFromIndex(1) as TableViewRow; + Assert.IsNotNull(oddRow); - foreach (var column in tableView.Columns) - { - Assert.IsFalse(column.UseSingleElement); - } + oddRow.EnsureAlternateColors(); + Assert.AreEqual(alternateBrush, oddRow.RowPresenter?.Background); + + var cell = oddRow.Cells[0]; + var overlay = oddRow.FindDescendant(b => b.Name is "EditingHighlightOverlay"); + Assert.IsNotNull(overlay); + + var started = await cell.BeginCellEditing(new RoutedEventArgs()); + Assert.IsTrue(started); + Assert.AreEqual(Visibility.Visible, overlay.Visibility); + Assert.AreEqual(alternateBrush, oddRow.RowPresenter?.Background); + + tableView.EndCellEditing(TableViewEditAction.Cancel, cell); + Assert.AreEqual(Visibility.Collapsed, overlay.Visibility); + Assert.AreEqual(alternateBrush, oddRow.RowPresenter?.Background); } finally { @@ -112,25 +164,23 @@ public async Task CellOrRowMode_TapToEdit_Prerequisites() } [UITestMethod] - public async Task Virtualization_EditingHighlight_ClearedOnRecycle() + public async Task CellOrRowMode_TapToEdit_Prerequisites() { - var items = CreateTestItems(5); - var tableView = CreateTableView(TableViewSelectionUnit.Row, items); + var items = CreateTestItems(3); + var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items); await LoadAsync(tableView); try { - tableView.CurrentCellSlot = new TableViewCellSlot(0, 0); - tableView.SetIsEditing(true); - tableView.SetIsEditing(false); - tableView.CurrentCellSlot = null; - - Assert.IsNull(tableView.CurrentCellSlot); - - var row = tableView.ContainerFromIndex(0) as TableViewRow; - Assert.IsNotNull(row); + Assert.AreEqual(TableViewSelectionUnit.CellOrRow, tableView.SelectionUnit); + Assert.IsFalse(tableView.TapToEdit); + Assert.IsFalse(tableView.IsReadOnly); + Assert.IsFalse(tableView.IsEditing); - row.EnsureAlternateColors(); + foreach (var column in tableView.Columns) + { + Assert.IsFalse(column.UseSingleElement); + } } finally { @@ -165,7 +215,7 @@ public async Task Virtualization_CellCurrentState_ResetOnPrepare() } [UITestMethod] - public async Task EditingState_SwitchBetweenRows_ClearsAndRestarts() + public async Task EditingState_SwitchBetweenRows() { var items = CreateTestItems(5); var tableView = CreateTableView(TableViewSelectionUnit.Row, items); @@ -173,20 +223,37 @@ public async Task EditingState_SwitchBetweenRows_ClearsAndRestarts() try { - tableView.CurrentCellSlot = new TableViewCellSlot(0, 0); - tableView.SetIsEditing(true); + var row0 = tableView.ContainerFromIndex(0) as TableViewRow; + var row3 = tableView.ContainerFromIndex(3) as TableViewRow; + Assert.IsNotNull(row0); + Assert.IsNotNull(row3); + + var cell0 = row0.Cells[0]; + var overlay0 = row0.FindDescendant(b => b.Name is "EditingHighlightOverlay"); + Assert.IsNotNull(overlay0); + + await cell0.BeginCellEditing(new RoutedEventArgs()); Assert.IsTrue(tableView.IsEditing); + Assert.AreEqual(Visibility.Visible, overlay0.Visibility); + tableView.EndCellEditing(TableViewEditAction.Commit, cell0); tableView.SetIsEditing(false); Assert.IsFalse(tableView.IsEditing); + Assert.AreEqual(Visibility.Collapsed, overlay0.Visibility); + + var cell3 = row3.Cells[1]; + var overlay3 = row3.FindDescendant(b => b.Name is "EditingHighlightOverlay"); + Assert.IsNotNull(overlay3); - tableView.CurrentCellSlot = new TableViewCellSlot(3, 1); - tableView.SetIsEditing(true); + await cell3.BeginCellEditing(new RoutedEventArgs()); Assert.IsTrue(tableView.IsEditing); - Assert.AreEqual(3, tableView.CurrentCellSlot?.Row); + Assert.AreEqual(Visibility.Visible, overlay3.Visibility); + Assert.AreEqual(Visibility.Collapsed, overlay0.Visibility); + tableView.EndCellEditing(TableViewEditAction.Commit, cell3); tableView.SetIsEditing(false); Assert.IsFalse(tableView.IsEditing); + Assert.AreEqual(Visibility.Collapsed, overlay3.Visibility); } finally {