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 e21a6d3e..bcb6e918 100644
--- a/src/TableView.cs
+++ b/src/TableView.cs
@@ -103,6 +103,11 @@ protected override void PrepareContainerForItemOverride(DependencyObject element
{
base.PrepareContainerForItemOverride(element, item);
+ if (element is TableViewRow { } recycledRow)
+ {
+ recycledRow.ApplyEditingHighlight(false);
+ }
+
DispatcherQueue.TryEnqueue(() =>
{
if (element is TableViewRow row)
@@ -111,6 +116,13 @@ 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);
@@ -184,7 +196,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);
diff --git a/src/TableViewCell.cs b/src/TableViewCell.cs
index 758c3b14..6778c2a5 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);
}
@@ -224,6 +226,21 @@ protected override async void OnTapped(TappedRoutedEventArgs e)
if (e.Handled) return;
}
+ if (TableView?.TapToEdit is true
+ && TableView.CurrentCellSlot == Slot
+ && !IsReadOnly
+ && !TableView.IsEditing
+ && Column?.UseSingleElement is not true)
+ {
+ if (TableView.SelectionUnit is TableViewSelectionUnit.Row)
+ {
+ MakeSelection();
+ }
+
+ e.Handled = await BeginCellEditing(e);
+ return;
+ }
+
if (TableView?.CurrentCellSlot != Slot || TableView?.LastSelectionUnit is TableViewSelectionUnit.Row)
{
MakeSelection();
@@ -398,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;
@@ -444,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 d403a149..80112a0e 100644
--- a/src/TableViewRow.cs
+++ b/src/TableViewRow.cs
@@ -603,6 +603,14 @@ internal void UpdateSelectCheckMarkOpacity()
}
}
+ ///
+ /// Highlights or unhighlights the row to indicate that a cell is being edited.
+ ///
+ internal void ApplyEditingHighlight(bool isEditing)
+ {
+ RowPresenter?.ApplyEditingHighlight(isEditing);
+ }
+
///
/// Gets the height of the horizontal gridlines.
///
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: editing highlight via cell lifecycle,
+/// TapToEdit property, overlay independence from alternate colors, and virtualization.
+///
+[TestClass]
+public class TableViewRowEditingTests
+{
+ [UITestMethod]
+ 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);
+ await LoadAsync(tableView);
+
+ try
+ {
+ var row = tableView.ContainerFromIndex(1) as TableViewRow;
+ Assert.IsNotNull(row);
+ Assert.IsTrue(row.Cells.Count > 0);
+
+ 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.IsTrue(tableView.IsEditing);
+ Assert.AreEqual(Visibility.Visible, overlay.Visibility);
+
+ tableView.EndCellEditing(TableViewEditAction.Cancel, cell);
+ Assert.AreEqual(Visibility.Collapsed, overlay.Visibility);
+ }
+ finally
+ {
+ await UnloadAsync(tableView);
+ }
+ }
+
+ [UITestMethod]
+ public async Task Lifecycle_BeginEditing_ShowsHighlight_CellOrRowMode()
+ {
+ var items = CreateTestItems(5);
+ var tableView = CreateTableView(TableViewSelectionUnit.CellOrRow, items);
+ await LoadAsync(tableView);
+
+ try
+ {
+ 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
+ {
+ await UnloadAsync(tableView);
+ }
+ }
+
+ [UITestMethod]
+ public async Task Lifecycle_CellMode_NoHighlight()
+ {
+ var items = CreateTestItems(5);
+ var tableView = CreateTableView(TableViewSelectionUnit.Cell, items);
+ await LoadAsync(tableView);
+
+ try
+ {
+ var row = tableView.ContainerFromIndex(1) as TableViewRow;
+ Assert.IsNotNull(row);
+
+ var cell = row.Cells[0];
+ var overlay = row.FindDescendant(b => b.Name is "EditingHighlightOverlay");
+ Assert.IsNotNull(overlay);
+
+ var started = await cell.BeginCellEditing(new RoutedEventArgs());
+ Assert.IsTrue(started);
+ Assert.IsTrue(tableView.IsEditing);
+ Assert.AreEqual(Visibility.Collapsed, overlay.Visibility);
+
+ tableView.EndCellEditing(TableViewEditAction.Cancel, cell);
+ Assert.AreEqual(Visibility.Collapsed, overlay.Visibility);
+ }
+ finally
+ {
+ await UnloadAsync(tableView);
+ }
+ }
+
+ [UITestMethod]
+ public async Task AlternateColors_PreservedDuringEditingHighlight()
+ {
+ var items = CreateTestItems(5);
+ var tableView = CreateTableView(TableViewSelectionUnit.Row, items);
+ var alternateBrush = new SolidColorBrush(Colors.LightGray);
+ tableView.AlternateRowBackground = alternateBrush;
+ await LoadAsync(tableView);
+
+ try
+ {
+ var oddRow = tableView.ContainerFromIndex(1) as TableViewRow;
+ Assert.IsNotNull(oddRow);
+
+ 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
+ {
+ 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.TapToEdit);
+ 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_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()
+ {
+ var items = CreateTestItems(5);
+ var tableView = CreateTableView(TableViewSelectionUnit.Row, items);
+ await LoadAsync(tableView);
+
+ try
+ {
+ 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);
+
+ await cell3.BeginCellEditing(new RoutedEventArgs());
+ Assert.IsTrue(tableView.IsEditing);
+ 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
+ {
+ 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);
+ }
+}