Skip to content

Tabular: add opt-in per-cell tooltips to TableView - #11669

Open
Hitesh Kumar (hiteshkrmsft) wants to merge 5 commits into
mainfrom
user/hik/tableview-cell-tooltips
Open

Tabular: add opt-in per-cell tooltips to TableView#11669
Hitesh Kumar (hiteshkrmsft) wants to merge 5 commits into
mainfrom
user/hik/tableview-cell-tooltips

Conversation

@hiteshkrmsft

@hiteshkrmsft Hitesh Kumar (hiteshkrmsft) commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Adds an opt-in per-cell tooltip to TableView.

Text cells render with CharacterEllipsis and no wrapping, so a value wider than its column is unreadable. TableViewColumn.CellToolTipBinding surfaces the full value:

<tabular:TableViewTextColumn Header="Notes"
                             Binding="{Binding Notes}"
                             CellToolTipBinding="{Binding Notes}" />

A CLR Binding property, matching TableViewTextColumn.Binding. Use an IValueConverter for computed content.

Design

The binding is set once on the cell wrapper, so the row's DataContext drives it: a recycled row re-resolves through ordinary inheritance, and a source PropertyChanged updates a live tooltip in place. There is no invalidation API, and the control never calls app code while realizing a cell — so no re-entrancy surface.

The control owns the ToolTip and its placement (PlacementMode.Mouse, matching TabViewItem). The bound value is the tooltip's content, not a ToolTip; a UIElement is parented by that cell's tooltip, so a converter returns a fresh element per evaluation. A private attached record holds the tooltip the control attached and the HelpText it published, so a tooltip the app set — including a bare string — is left alone. On recycle, tooltips are neutralized in place rather than detached, also matching TabViewItem.

Accessibility

String content is published as the cell's AutomationProperties.HelpText. TableViewCellAutomationPeer::GetHelpTextCore suppresses it when it merely repeats the cell's own text, gated on the ownership record so text the app set is never dropped.

Non-string content is mouse-only: no HelpText is published, and cell focus is row-level, so keyboard and screen-reader users get nothing. TabViewItem and NavigationViewItem refuse non-string tooltip content for this reason. Use a converter that returns text when the value must be accessible; parity needs a public cell element, which is post-v1.

Notes

  • Column- and group-header tooltips are deferred — PART_HeaderScroller is IsHitTestVisible="False", so a header tooltip could never open.
  • The two attached properties are registered lazily, outside the generated TableViewProperties, so XamlMetadataProviderGenerated.tt emits their include and clear call. Both are marked as hand edits.
  • PR builds do not compile TableView.vcxitems; verified with a local Tabular build and the sample's Cell tooltips page.
  • No automated tests: there is no TableView test project, and Tabular is not in MUXControls.sln.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@microsoft-github-policy-service microsoft-github-policy-service Bot added the needs-triage Issue needs to be triaged by the area owners label Aug 27, 2026
@hiteshkrmsft
Hitesh Kumar (hiteshkrmsft) force-pushed the user/hik/tableview-cell-tooltips branch 2 times, most recently from 1262bcb to 9527bc1 Compare August 31, 2026 21:17
@hiteshkrmsft
Hitesh Kumar (hiteshkrmsft) marked this pull request as ready for review August 31, 2026 21:17
@hiteshkrmsft
Hitesh Kumar (hiteshkrmsft) requested a review from a team as a code owner August 31, 2026 21:17
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@hiteshkrmsft

Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
1 pipeline(s) were filtered out due to trigger conditions.

@hiteshkrmsft
Hitesh Kumar (hiteshkrmsft) force-pushed the user/hik/tableview-cell-tooltips branch from 9527bc1 to 2226e88 Compare September 1, 2026 13:28
TableView text cells render with CharacterEllipsis and no wrapping, so a value wider than
its column is unreadable and there is no way for an app to surface the full text.

Adds a CellToolTipRequested event: the handler receives the row's Item and the cell's Column
and sets Content, which the control shows as that cell's tooltip. It is opt-in - with no
handler attached the control does no per-cell work and allocates nothing - and it is resolved
per cell as rows realize and recycle, so a virtualized table only pays for what is on screen.

The control owns the ToolTip object. Content is the tooltip's content, not a ToolTip to
attach: ToolTipService rebinds a ToolTip's owner and container on every registration, so a
single instance handed back for several cells would leave them contending for one owner slot.
A UIElement returned as Content is parented by that cell's ToolTip, so a handler returns a
fresh element per raise.

Ownership is recorded rather than inferred. A private attached record holds the ToolTip the
control attached and the exact HelpText it published, tested against the raw value on the
element, so a tooltip the app set - including a bare string - is detected and left alone.

Accessibility: the tooltip text is published as the cell's AutomationProperties.HelpText, and
the cell automation peer suppresses it when it merely repeats the cell's own text so the value
is not announced twice. The suppression is resolved in the peer rather than when the tooltip
is attached: at attach time the cell content has just been generated and its binding has not
produced a value, so the two never compared equal. Resolving it in the peer also keeps
automation peer creation off the realization path. When Content is not a string the handler
sets AutomationHelpText, since the cell wrapper is internal and an app cannot reach it.

Re-entrancy: the event reaches app code from the cell realization path, so the pass snapshots
its targets rather than walking the live children collection and holds a strong reference
across the raise. The editing cell is re-read after each raise, because a handler can start an
edit on the cell being resolved; a refresh requested from inside a pass, such as an edit
closing under a handler, is recorded and replayed. Rebuild and refresh share one bounded drain,
so app code is never raised more than a fixed number of times per row. A handler that throws
loses only its own cell's tooltip, and InvalidateCellToolTips is coalesced onto the dispatcher
and bounded, so calling it from a handler cannot queue a callback every tick.

Recycling: a recycled row never shows or announces a previous item's tooltip, including after
the last handler is removed. Control-created tooltips are neutralized in place rather than
detached, so scrolling reuses the object instead of allocating a Control per cell per scroll,
matching TabViewItem.

Column-header and group-header tooltips are deferred. The header band is IsHitTestVisible=
"False" and is scrolled programmatically from the body, so a tooltip there could never open;
both surfaces land with the work that makes them interactive.

Adds a sample page covering the contract: string content, non-string content with
AutomationHelpText, a column that returns nothing, a column whose own template owns its
tooltip, and toggles for late attach, re-entrant invalidation and a throwing handler.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hiteshkrmsft
Hitesh Kumar (hiteshkrmsft) force-pushed the user/hik/tableview-cell-tooltips branch from 2226e88 to 46e4efc Compare September 1, 2026 13:40
…ipBinding

Replaces CellToolTipRequested with a binding on the column. The binding is set
once on the cell wrapper, so the row's DataContext drives it: a recycled row
re-resolves through ordinary inheritance and a source PropertyChanged updates a
live tooltip in place.

Because the control no longer calls app code while realizing a cell, the
re-entrancy surface disappears with it - the per-row drain loops, the pending
flags, the dispatcher pass counter and InvalidateCellToolTips() are all gone.
Computed content is authored with an IValueConverter.

HelpText suppression in TableViewCellAutomationPeer::GetHelpTextCore is now
gated on the ownership record, so it never drops text the app set.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 003c9ad0-6c65-4016-ac84-a6a0d75fb31b
- Restore the UTF-8 BOM on TableView.cpp, stripped while excising the
  tooltip event machinery.
- Restore the two [TestProperty("Ignore", "True")] attributes on
  InkToolbarTests.cs. They were dropped accidentally, reverting
  "Temporarily skip failing InkToolbar cleanup tests (#11733)" and
  re-enabling two tests the team had deliberately skipped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 003c9ad0-6c65-4016-ac84-a6a0d75fb31b
@hiteshkrmsft Hitesh Kumar (hiteshkrmsft) changed the title Tabular: add opt-in per-cell tooltips to TableView Tabular: add opt-in per-cell tooltips to TableView via CellToolTipBinding Sep 1, 2026
@hiteshkrmsft Hitesh Kumar (hiteshkrmsft) changed the title Tabular: add opt-in per-cell tooltips to TableView via CellToolTipBinding Tabular: add opt-in per-cell tooltips to TableView Sep 1, 2026
- Restore the OnDataContextChanged re-entry guard and the original
  RebuildCells comment. They were removed so the tooltip drain could
  record pending rebuilds; that machinery is gone, and the guard in
  RebuildCells was always the real one.
- Revert the GetCellValueTextFromWrapper extraction. It was pulled into
  TableViewAutomationHelpers.h to share with the tooltip pass; the peer
  is the only caller again, so the body moves back inline.
- Keep the AbandonCellEdit local named cellWrapper; only the hoist out of
  the if is needed.
- Tighten comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 003c9ad0-6c65-4016-ac84-a6a0d75fb31b
Findings from an orchestrated multi-lens review of the tooltip change. Only
the ones that verified against current code are here; a high-severity
"recycled cells lose their tooltip" finding was disproved by a liveness probe
(all realized rows keep live tooltips and HelpText; the neutralized ones are
pooled rows, which is correct) and is not fixed.

- Drop the unreachable null-binding branch in ApplyCellToolTipBinding. Its one
  caller is guarded by `if (auto const toolTipBinding = column.CellToolTipBinding())`,
  so the binding is never null.
- EnsureCellToolTipRecordProperty returns void; its only caller discarded the
  DependencyProperty and called it purely to force lazy registration.
- Diagnose a ToolTip handed back as tooltip content instead of swallowing it.
  ToolTipService.ToolTip does accept a ToolTip, so this is an easy authoring
  mistake with no other feedback.
- Spec: state plainly that non-string content is mouse-only with no accessible
  representation, and that TabViewItem/NavigationViewItem refuse such content.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hiteshkrmsft

Copy link
Copy Markdown
Contributor Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
1 pipeline(s) were filtered out due to trigger conditions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-triage Issue needs to be triaged by the area owners

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant