From 5e9d05e67e122de5fb8b5fd8a34ff1aee221a86b Mon Sep 17 00:00:00 2001
From: Phil Wade <38622251+SolidRockProgrammer@users.noreply.github.com>
Date: Sat, 12 Sep 2026 02:57:23 +0800
Subject: [PATCH 1/5] Add public TableViewCell.BeginEdit() to start a cell edit
session
The control begins editing on a double tap and on F2/Enter, but exposes no public
way to ask for it. A column whose resting cell carries its own affordance - a
drop-down arrow, a picker button - needs to open the editor on a single click of
that affordance, which the control cannot infer from the gesture.
BeginEdit() wraps the existing internal BeginCellEditing(RoutedEventArgs) and
applies the same preconditions as the double-tap path, so it adds no behaviour
beyond the verb the control already performs.
---
src/TableViewCell.cs | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/src/TableViewCell.cs b/src/TableViewCell.cs
index 9463bbc8..aa78b6dc 100644
--- a/src/TableViewCell.cs
+++ b/src/TableViewCell.cs
@@ -502,6 +502,30 @@ internal bool BeginCellEditing(RoutedEventArgs editingArgs)
return false;
}
+ ///
+ /// Begins editing this cell, as a double tap or F2 does.
+ ///
+ ///
+ /// The control starts an edit session from and from the
+ /// TableView key handler, but offers no public way to ask for one. A column whose RESTING
+ /// cell carries an affordance of its own - a drop-down arrow, a picker button - needs to open the
+ /// editor on a single click of that affordance, and that is not a gesture the control can infer.
+ /// The same conditions apply as to a double tap: a read-only cell, a cell with no owning
+ /// , a table that is already editing, or a column drawing itself through
+ /// will not begin an edit, and this returns
+ /// . BeginningEdit is raised, and a handler that cancels it is
+ /// honoured, exactly as on the existing paths.
+ ///
+ /// if the cell entered edit mode; otherwise .
+ public bool BeginEdit()
+ {
+ if (IsReadOnly || TableView is null || TableView.IsEditing || Column?.UseSingleElement is not false)
+ {
+ return false;
+ }
+
+ return BeginCellEditing(new RoutedEventArgs());
+ }
///
/// Prepares the cell for editing.
///
From 67d871c4648c5c6a13e0a1f69d65b2f17478306d Mon Sep 17 00:00:00 2001
From: Phil Wade <38622251+SolidRockProgrammer@users.noreply.github.com>
Date: Sat, 12 Sep 2026 03:01:56 +0800
Subject: [PATCH 2/5] Separate BeginEdit() from the next member with a blank
line
---
src/TableViewCell.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/TableViewCell.cs b/src/TableViewCell.cs
index aa78b6dc..e62d8e95 100644
--- a/src/TableViewCell.cs
+++ b/src/TableViewCell.cs
@@ -526,6 +526,7 @@ public bool BeginEdit()
return BeginCellEditing(new RoutedEventArgs());
}
+
///
/// Prepares the cell for editing.
///
From 39605a28c950a6c323b83da65a379300706cc2e8 Mon Sep 17 00:00:00 2001
From: Phil Wade <38622251+SolidRockProgrammer@users.noreply.github.com>
Date: Tue, 29 Sep 2026 00:28:42 +0800
Subject: [PATCH 3/5] Replace TableViewCell.BeginEdit() with
TableView.BeginEditAsync(slot)
Review feedback on #431: calling BeginEdit() on a cell entered edit
mode without telling the TableView and without making that cell the
current cell, so later keyboard navigation and commit behaviour would
start from the wrong cell.
BeginEditAsync now follows the same sequence as the existing gestures:
commit an edit in progress on another cell (as tap/Tab do), make the
target cell current and selected (as keyboard navigation does), then
begin the edit as F2 does. The edit starts from OnCurrentCellChanged
after the cell has been scrolled into view and focused, so that focus
call cannot pull focus back out of the new editing element.
Co-Authored-By: Claude Opus 5.5
---
src/TableView.cs | 94 +++++++++++++++++
src/TableViewCell.cs | 25 -----
tests/TableViewBeginEditTests.cs | 172 +++++++++++++++++++++++++++++++
3 files changed, 266 insertions(+), 25 deletions(-)
create mode 100644 tests/TableViewBeginEditTests.cs
diff --git a/src/TableView.cs b/src/TableView.cs
index e7c9ceb4..9aab0798 100644
--- a/src/TableView.cs
+++ b/src/TableView.cs
@@ -50,6 +50,7 @@ public partial class TableView : ListView
private TableViewCellSlotRange? _lastDragSelectionCellRange;
private ItemIndexRange? _lastDragSelectionRowRange;
private bool _cellStateDispatchPending;
+ private (TableViewCellSlot Slot, TaskCompletionSource Completion)? _pendingEdit;
private readonly HashSet _pendingCellStateRows = [];
private TableViewColumn? _resizingColumn;
private double _resizingOriginalWidth;
@@ -1034,6 +1035,90 @@ private int CalculateAvailablePageSize()
return (int)Math.Floor(availableHeight / rowHeight);
}
+ ///
+ /// Makes the cell at the current cell and begins editing it, as pressing F2 on
+ /// that cell does.
+ ///
+ ///
+ /// This is for a cell that exposes an affordance of its own while it is not being edited - a
+ /// drop-down arrow, a picker button - and needs a single click on that affordance to open the editor.
+ /// The control starts an edit only from a double tap and from F2, and a single click is not a gesture it
+ /// can infer.
+ /// The sequence matches the existing gestures:
+ ///
+ /// - If another cell is being edited, that edit is committed first, as tapping or tabbing to a
+ /// different cell does. If committing is cancelled, no edit begins.
+ /// - The cell becomes the and is selected, as keyboard navigation to
+ /// it does, and is scrolled into view and focused.
+ /// - The edit then begins. is raised, and a handler that cancels it is
+ /// honoured.
+ ///
+ /// No edit begins for a read-only table, column or cell, for a column that draws itself through
+ /// (a double tap does not begin one either), or when the
+ /// cell is already being edited.
+ ///
+ /// The cell to edit.
+ /// A task whose result is if the cell entered edit mode; otherwise
+ /// .
+ public async Task BeginEditAsync(TableViewCellSlot slot)
+ {
+ if (IsReadOnly || !slot.IsValid(this))
+ {
+ return false;
+ }
+
+ if (IsEditing)
+ {
+ if (CurrentCellSlot == slot)
+ {
+ return false;
+ }
+
+ if (CurrentCellSlot is { } currentSlot
+ && GetCellFromSlot(currentSlot) is { } currentCell
+ && !EndCellEditing(TableViewEditAction.Commit, currentCell))
+ {
+ return false;
+ }
+
+ SetIsEditing(false);
+ }
+
+ // A later request supersedes an earlier one that has not started yet.
+ _pendingEdit?.Completion.TrySetResult(false);
+ _pendingEdit = null;
+
+ if (CurrentCellSlot == slot)
+ {
+ // Already current, so OnCurrentCellChanged will not run; the cell may still be out of view.
+ return TryBeginEdit(await ScrollCellIntoView(slot));
+ }
+
+ var pending = new TaskCompletionSource();
+ _pendingEdit = (slot, pending);
+
+ MakeSelection(slot, false);
+
+ if (CurrentCellSlot != slot)
+ {
+ // Selection declined to make the cell current, so OnCurrentCellChanged will not start the edit.
+ _pendingEdit = null;
+ return false;
+ }
+
+ return await pending.Task;
+ }
+
+ ///
+ /// Begins editing if the same conditions a double tap applies allow it.
+ ///
+ private bool TryBeginEdit(TableViewCell? cell)
+ {
+ return cell is { IsReadOnly: false, Column.UseSingleElement: false }
+ && !IsEditing
+ && cell.BeginCellEditing(new RoutedEventArgs());
+ }
+
///
/// Ends the editing of a cell, committing or canceling the edit based on the specified action.
///
@@ -2181,6 +2266,15 @@ private async Task OnCurrentCellChanged(TableViewCellSlot? oldSlot, TableViewCel
var cell = await ScrollCellIntoView(newSlot.Value);
cell?.ApplyCurrentCellState();
cell?.Focus(FocusState.Programmatic);
+
+ // A BeginEditAsync call that moved the current cell starts its edit here, AFTER the cell has
+ // been scrolled into view and focused. Starting it any earlier would let the Focus call above
+ // take focus away from the editing element the moment it had received it.
+ if (_pendingEdit is { } pending && pending.Slot == newSlot.Value)
+ {
+ _pendingEdit = null;
+ pending.Completion.TrySetResult(CurrentCellSlot == newSlot && TryBeginEdit(cell));
+ }
}
}
diff --git a/src/TableViewCell.cs b/src/TableViewCell.cs
index e62d8e95..9463bbc8 100644
--- a/src/TableViewCell.cs
+++ b/src/TableViewCell.cs
@@ -502,31 +502,6 @@ internal bool BeginCellEditing(RoutedEventArgs editingArgs)
return false;
}
- ///
- /// Begins editing this cell, as a double tap or F2 does.
- ///
- ///
- /// The control starts an edit session from and from the
- /// TableView key handler, but offers no public way to ask for one. A column whose RESTING
- /// cell carries an affordance of its own - a drop-down arrow, a picker button - needs to open the
- /// editor on a single click of that affordance, and that is not a gesture the control can infer.
- /// The same conditions apply as to a double tap: a read-only cell, a cell with no owning
- /// , a table that is already editing, or a column drawing itself through
- /// will not begin an edit, and this returns
- /// . BeginningEdit is raised, and a handler that cancels it is
- /// honoured, exactly as on the existing paths.
- ///
- /// if the cell entered edit mode; otherwise .
- public bool BeginEdit()
- {
- if (IsReadOnly || TableView is null || TableView.IsEditing || Column?.UseSingleElement is not false)
- {
- return false;
- }
-
- return BeginCellEditing(new RoutedEventArgs());
- }
-
///
/// Prepares the cell for editing.
///
diff --git a/tests/TableViewBeginEditTests.cs b/tests/TableViewBeginEditTests.cs
new file mode 100644
index 00000000..ef780849
--- /dev/null
+++ b/tests/TableViewBeginEditTests.cs
@@ -0,0 +1,172 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
+using System.Threading.Tasks;
+
+namespace WinUI.TableView.Tests;
+
+[TestClass]
+public class TableViewBeginEditTests
+{
+ [UITestMethod]
+ public async Task BeginEditAsync_MakesTheCellCurrentAndBeginsEditingIt()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(1, 1);
+
+ var began = await tableView.BeginEditAsync(slot);
+
+ Assert.IsTrue(began);
+ Assert.AreEqual(slot, tableView.CurrentCellSlot);
+ Assert.IsTrue(tableView.IsEditing);
+ Assert.IsInstanceOfType(tableView.GetCellFromSlot(slot)!.Content);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_LeavesFocusInTheEditingElement()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(1, 1);
+
+ await tableView.BeginEditAsync(slot);
+ await Task.Delay(200); // Let the editing element load and take focus.
+
+ var editor = (TextBox)tableView.GetCellFromSlot(slot)!.Content;
+ var focused = FocusManager.GetFocusedElement(tableView.XamlRoot) as DependencyObject;
+
+ Assert.IsTrue(IsSelfOrDescendant(focused, editor), $"Focus is on {focused?.GetType().Name ?? "nothing"}, not the editor.");
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_OnTheCurrentCell_BeginsEditingIt()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(0, 1);
+ tableView.CurrentCellSlot = slot;
+ await Task.Delay(100);
+
+ var began = await tableView.BeginEditAsync(slot);
+
+ Assert.IsTrue(began);
+ Assert.IsTrue(tableView.IsEditing);
+ Assert.AreEqual(slot, tableView.CurrentCellSlot);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_CommitsTheEditInProgressOnAnotherCell()
+ {
+ var tableView = await CreateTableViewAsync();
+ var ended = new System.Collections.Generic.List();
+ tableView.CellEditEnded += (_, e) => ended.Add(e.EditAction);
+
+ Assert.IsTrue(await tableView.BeginEditAsync(new TableViewCellSlot(0, 0)));
+ Assert.IsTrue(await tableView.BeginEditAsync(new TableViewCellSlot(1, 1)));
+
+ CollectionAssert.AreEqual(new[] { TableViewEditAction.Commit }, ended);
+ Assert.AreEqual(new TableViewCellSlot(1, 1), tableView.CurrentCellSlot);
+ Assert.IsTrue(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_OnTheCellAlreadyBeingEdited_ReturnsFalseAndKeepsEditing()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(0, 0);
+ Assert.IsTrue(await tableView.BeginEditAsync(slot));
+
+ Assert.IsFalse(await tableView.BeginEditAsync(slot));
+ Assert.IsTrue(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_HonoursACancelledBeginningEdit()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.BeginningEdit += (_, e) => e.Cancel = true;
+
+ var began = await tableView.BeginEditAsync(new TableViewCellSlot(1, 0));
+
+ Assert.IsFalse(began);
+ Assert.IsFalse(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_DoesNotEditAReadOnlyColumn()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.Columns[1].IsReadOnly = true;
+
+ var began = await tableView.BeginEditAsync(new TableViewCellSlot(0, 1));
+
+ Assert.IsFalse(began);
+ Assert.IsFalse(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_DoesNotEditAReadOnlyTable()
+ {
+ var tableView = await CreateTableViewAsync();
+ tableView.IsReadOnly = true;
+
+ Assert.IsFalse(await tableView.BeginEditAsync(new TableViewCellSlot(0, 0)));
+ Assert.IsFalse(tableView.IsEditing);
+ }
+
+ [UITestMethod]
+ public async Task BeginEditAsync_RejectsASlotOutsideTheTable()
+ {
+ var tableView = await CreateTableViewAsync();
+
+ Assert.IsFalse(await tableView.BeginEditAsync(new TableViewCellSlot(5, 0)));
+ Assert.IsFalse(tableView.IsEditing);
+ }
+
+ private static bool IsSelfOrDescendant(DependencyObject? element, DependencyObject ancestor)
+ {
+ for (var current = element; current is not null; current = VisualTreeHelper.GetParent(current))
+ {
+ if (ReferenceEquals(current, ancestor)) return true;
+ }
+
+ return false;
+ }
+
+ private static async Task CreateTableViewAsync()
+ {
+ var tableView = new TableView
+ {
+ SelectionMode = ListViewSelectionMode.Extended,
+ SelectionUnit = TableViewSelectionUnit.Cell,
+ };
+
+ tableView.Columns.Add(new TableViewTextColumn
+ {
+ Header = "Name",
+ Binding = new Binding { Path = new PropertyPath(nameof(EditItem.Name)), Mode = BindingMode.TwoWay }
+ });
+ tableView.Columns.Add(new TableViewTextColumn
+ {
+ Header = "Note",
+ Binding = new Binding { Path = new PropertyPath(nameof(EditItem.Note)), Mode = BindingMode.TwoWay }
+ });
+ tableView.ItemsSource = new[]
+ {
+ new EditItem { Name = "A", Note = "first" },
+ new EditItem { Name = "B", Note = "second" }
+ };
+
+ await UnitTestApp.Current.MainWindow.LoadTestContentAsync(tableView);
+
+ return tableView;
+ }
+
+ public sealed class EditItem
+ {
+ public string Name { get; set; } = string.Empty;
+ public string Note { get; set; } = string.Empty;
+ }
+}
From 8e4436f1d7338b43d1d4d2a83d17bf3b46178ff6 Mon Sep 17 00:00:00 2001
From: Phil Wade <38622251+SolidRockProgrammer@users.noreply.github.com>
Date: Tue, 29 Sep 2026 00:47:47 +0800
Subject: [PATCH 4/5] Drop a focus test that could not tell the orderings apart
A negative control showed the focus assertion passes whether the edit
starts before or after the current-cell change is processed, so it
pinned nothing about the ordering; it also failed once on a cold first
deploy, which would make it a flake in CI. The comment on the hand-off
now states what the ordering does rather than a failure it prevents.
Co-Authored-By: Claude Opus 5.5
---
src/TableView.cs | 6 +++---
tests/TableViewBeginEditTests.cs | 27 ---------------------------
2 files changed, 3 insertions(+), 30 deletions(-)
diff --git a/src/TableView.cs b/src/TableView.cs
index 9aab0798..e5a200f1 100644
--- a/src/TableView.cs
+++ b/src/TableView.cs
@@ -2267,9 +2267,9 @@ private async Task OnCurrentCellChanged(TableViewCellSlot? oldSlot, TableViewCel
cell?.ApplyCurrentCellState();
cell?.Focus(FocusState.Programmatic);
- // A BeginEditAsync call that moved the current cell starts its edit here, AFTER the cell has
- // been scrolled into view and focused. Starting it any earlier would let the Focus call above
- // take focus away from the editing element the moment it had received it.
+ // A BeginEditAsync call that moved the current cell starts its edit here, once the cell has
+ // been scrolled into view and focused, so that the editing element is the last thing to take
+ // focus rather than racing the Focus call above.
if (_pendingEdit is { } pending && pending.Slot == newSlot.Value)
{
_pendingEdit = null;
diff --git a/tests/TableViewBeginEditTests.cs b/tests/TableViewBeginEditTests.cs
index ef780849..16b62662 100644
--- a/tests/TableViewBeginEditTests.cs
+++ b/tests/TableViewBeginEditTests.cs
@@ -1,8 +1,6 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
-using Microsoft.UI.Xaml.Input;
-using Microsoft.UI.Xaml.Media;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.VisualStudio.TestTools.UnitTesting.AppContainer;
using System.Threading.Tasks;
@@ -26,21 +24,6 @@ public async Task BeginEditAsync_MakesTheCellCurrentAndBeginsEditingIt()
Assert.IsInstanceOfType(tableView.GetCellFromSlot(slot)!.Content);
}
- [UITestMethod]
- public async Task BeginEditAsync_LeavesFocusInTheEditingElement()
- {
- var tableView = await CreateTableViewAsync();
- var slot = new TableViewCellSlot(1, 1);
-
- await tableView.BeginEditAsync(slot);
- await Task.Delay(200); // Let the editing element load and take focus.
-
- var editor = (TextBox)tableView.GetCellFromSlot(slot)!.Content;
- var focused = FocusManager.GetFocusedElement(tableView.XamlRoot) as DependencyObject;
-
- Assert.IsTrue(IsSelfOrDescendant(focused, editor), $"Focus is on {focused?.GetType().Name ?? "nothing"}, not the editor.");
- }
-
[UITestMethod]
public async Task BeginEditAsync_OnTheCurrentCell_BeginsEditingIt()
{
@@ -125,16 +108,6 @@ public async Task BeginEditAsync_RejectsASlotOutsideTheTable()
Assert.IsFalse(tableView.IsEditing);
}
- private static bool IsSelfOrDescendant(DependencyObject? element, DependencyObject ancestor)
- {
- for (var current = element; current is not null; current = VisualTreeHelper.GetParent(current))
- {
- if (ReferenceEquals(current, ancestor)) return true;
- }
-
- return false;
- }
-
private static async Task CreateTableViewAsync()
{
var tableView = new TableView
From cc8ac9f5f05c0075f22c84834f487ed4a6d9154c Mon Sep 17 00:00:00 2001
From: Phil Wade <38622251+SolidRockProgrammer@users.noreply.github.com>
Date: Tue, 29 Sep 2026 08:43:12 +0800
Subject: [PATCH 5/5] Begin editing the current realized cell synchronously
ScrollRowIntoView always yields, so BeginEditAsync could never complete before its caller's event handler returned - not even for the current, visible cell, where F2 begins synchronously. A key or character handler needs to know whether the edit began before it returns (found by a consumer that starts an edit when a character is typed on a selected cell). A realized current cell is now edited directly; only a cell scrolled out of view waits for the scroll.
Co-Authored-By: Claude Opus 5.5
---
src/TableView.cs | 5 +++--
tests/TableViewBeginEditTests.cs | 17 +++++++++++++++++
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/src/TableView.cs b/src/TableView.cs
index e5a200f1..8b88aef2 100644
--- a/src/TableView.cs
+++ b/src/TableView.cs
@@ -1090,8 +1090,9 @@ public async Task BeginEditAsync(TableViewCellSlot slot)
if (CurrentCellSlot == slot)
{
- // Already current, so OnCurrentCellChanged will not run; the cell may still be out of view.
- return TryBeginEdit(await ScrollCellIntoView(slot));
+ // Already current, so OnCurrentCellChanged will not run. A realized cell is edited directly,
+ // as F2 does, so the call completes synchronously; only a cell scrolled out of view waits.
+ return TryBeginEdit(GetCellFromSlot(slot) ?? await ScrollCellIntoView(slot));
}
var pending = new TaskCompletionSource();
diff --git a/tests/TableViewBeginEditTests.cs b/tests/TableViewBeginEditTests.cs
index 16b62662..fa4cb488 100644
--- a/tests/TableViewBeginEditTests.cs
+++ b/tests/TableViewBeginEditTests.cs
@@ -39,6 +39,23 @@ public async Task BeginEditAsync_OnTheCurrentCell_BeginsEditingIt()
Assert.AreEqual(slot, tableView.CurrentCellSlot);
}
+ [UITestMethod]
+ public async Task BeginEditAsync_OnTheCurrentVisibleCell_CompletesSynchronously()
+ {
+ var tableView = await CreateTableViewAsync();
+ var slot = new TableViewCellSlot(1, 0);
+ tableView.CurrentCellSlot = slot;
+ await Task.Delay(100);
+
+ // A caller handling a key or character event must know before its handler returns whether
+ // the edit began, as it does for F2; waiting on a scroll here would make that impossible.
+ var begin = tableView.BeginEditAsync(slot);
+
+ Assert.IsTrue(begin.IsCompleted, "BeginEditAsync on the current, realized cell did not complete synchronously.");
+ Assert.IsTrue(await begin);
+ Assert.IsTrue(tableView.IsEditing);
+ }
+
[UITestMethod]
public async Task BeginEditAsync_CommitsTheEditInProgressOnAnotherCell()
{