Files
microsoft-ui-xaml/Samples/TableViewSampleApp/Controls/SamplePresenter.xaml
T
Hitesh KumarandCopilot d5132fd2e4 TableView: templated column headers, UIA resize, sort fixes, recycle perf, theming (#12307)
## Fixes

| Internal issue | Type | Change |
|---|---|---|
| https://task.ms/64475625 | Bug | Column headers are a templated,
restylable `TableViewColumnHeader` with visual states |
| https://task.ms/64475623 | Bug | Header typography comes from the
header Style, not C++ local values |
| https://task.ms/64143451 | Bug | Header column separators are always
drawn (no new property); hide them by overriding
`TabularSurfaceHeaderSeparatorBrush` |
| https://task.ms/63996584 | Task | Column headers expose the UIA
Transform pattern so UIA clients can resize columns |
| https://task.ms/64475611 | Bug | Outer border uses new
`TabularSurfaceOuterBorderBrush` (`CardStrokeColorDefault`); grid-line
values were in #12349 |
| https://task.ms/64475629 | Bug | Default group header uses theme
tokens (SemiBold key style, 8px spacing); the item count inherits the
header `Foreground` |
| https://task.ms/64652961 | Task | Row recycling no longer regenerates
cells or refreshes row chrome twice (scroll performance) |
| https://task.ms/63790855 | Task | Sort: `CanSort` is enforced and
`SortByColumn(column, None)` always clears; a sort through a source
another table owns returns false; no spurious `Sorted`; an app-owned
sort survives a `CanSort`/`CanUserSortColumns` opt-out |
| https://task.ms/63787021 | Task | Cell automation-peer constructor
validates its arguments; `BeginEdit` rolls back when it throws |

Not changed: https://task.ms/64512732 (by design: Auto columns follow
content width), https://task.ms/64475627 (design decision: keep chevron
sort glyphs), https://task.ms/64513223 (investigation only; fix
deferred).

## PR Type

- [x] Bugfix
- [x] Feature
- [ ] Code style update (formatting, renaming)
- [ ] Refactoring (no functional changes, no API changes)
- [ ] Build related changes
- [x] Documentation content changes
- [ ] Other (please describe):

## Description

Follow-up to #12349 (merged) and #12285.

Column headers move from code-built Grids to a new templated
`TableViewColumnHeader`, restylable through
`TableView.ColumnHeaderStyle` or `TableViewColumn.HeaderStyle`. Header
peers gain the UIA Transform pattern for column resize. Sort behavior is
tightened around `CanSort`, opt-outs and shared `TableViewSource`
ownership. Row recycling reuses cells. Outer border, header lines and
the default group header follow Fluent tokens.

Review-driven fixes included here:
- The header Style's `TargetType` `TypeName` is kept alive while it is
compared; a Style with only `BasedOn` typed is applied, and its setters
are read before applying it seals the Style. Incompatible, untyped or
cyclic `BasedOn` Styles are ignored with a debug diagnostic.
- A header disabled by its Style reports `IsEnabled=false` and is not
invokable through UIA.
- UIA `Transform.Resize` converts physical pixels through the table's
full visual scale (not only `RasterizationScale`), and a width change
below the drag jitter deadband is written once, exactly.
- Disabling a header in its High Contrast hover/pressed state keeps the
disabled foreground (state order fixed); the disabled sort glyph follows
the text brush across theme changes.
- A `CanSort` opt-out made inside `BeginningEdit` is replayed after the
edit it let open closes, and rebuilds headers even when a `Sorted`
handler throws.
- Header rebuilds caused by `Content`/`Header` or `Columns` changes keep
focus in the header band, respect an app's own focus move, and release
`UIElement` headers back to the app.
- A disabled group header does not dim its item count twice. The count
opacity follows `ContentTemplate` changes while the header is disabled.

<details><summary>Product changes by file</summary>

| File | +/- | What changed | Items |
|---|---|---|---|
| `TableView/TableView.cpp` (+ .h) | +684 / −382 | `RebuildHeaders`
creates `TableViewColumnHeader` controls; applies
`ColumnHeaderStyle`/`HeaderStyle` with TargetType/BasedOn checks;
density MinHeight/Padding only when the Style doesn't set
them<br>`ReleaseHeaderCells` and header-focus handling for
removed/replaced columns, re-entrant rebuilds and app focus
moves<br>Header bottom rule thickness set on `PART_HeaderRow`; header
lines no longer follow `GridLinesVisibility`<br>Row prepare/clear
refresh chrome once; `TryResizeColumnFromAutomation` writes small widths
exactly | 64475625, 64475623, 64143451, 64652961, 63996584, 63790855 |
| `TableView/TableViewColumnHeader.cpp` (+ .h; replaces
`TableViewHeaderCell.h`) | +435 / −210 | New sealed templated
`ContentControl` with read-only `Column`<br>Resolves
`PART_ContentPresenter`, `PART_GridLine`, `PART_SortIndicatorHost`,
`PART_ResizeGripperHost`; hosts the sort glyph and gripper<br>Drives
`CommonStates`/`HighContrastStates`/`SortStates`; disabled sort glyph
bound to the text brush | 64475625, 64475623 |
| `TableView/TableViewRow.cpp` (+ .h) | +191 / −35 | Recycled rows keep
their cells unless cell automation peers exist; chrome refreshed once;
tooltips re-applied on recycle<br>`BeginCellEdit` restores display
element, edit state and focus on throw | 64652961, 63787021 |
| `TableView/TableView.xaml` | +128 / −18 | Default
`TableViewColumnHeader` Style/template: typography setters, visual
states, parts<br>Outer border uses `TabularSurfaceOuterBorderBrush`;
header lines use `TabularSurfaceHeaderSeparatorBrush`<br>Group header:
`TableViewGroupExpanderSpacing` (8), padding 8,8,12,8, key/count text
styles, count inherits `Foreground` | 64475625, 64475623, 64475611,
64475629 |
| `TableView/TableViewColumnHeaderAutomationPeer.cpp` (+ .h) | +127 / −9
| `ITransformProvider`: `Resize` converts physical px through the visual
scale; `CanResize` tracks gripper availability (pinned →
false)<br>`IsEnabled` follows the header; name read from the content
presenter | 63996584, 64475625 |
| `TableView/TableView_Sort.cpp` | +113 / −39 | `SortByColumn(None)`
bypasses the `CanSort` gate; other sorts refused<br>Sort through a
source owned by another table returns false; implicit clears keep the
app's sort and raise no `Sorted`; app-lit columns left alone on opt-out
| 63790855 |
| `TableView/TableView_Layout.cpp` | +74 / −14 | Realized-row walk only
when a visible Auto column exists<br>Transform `CanResize` changes
raised for existing header peers | 64652961, 63996584 |
| `TableView/TableView_Editing.cpp` | +63 / −15 | `BeginEdit` unwinds on
throw; `CanSort` opt-out during `BeginningEdit` queued until the edit
closes | 63787021, 63790855 |
| `TableView/TableViewCellAutomationPeer.cpp` (+ .h, `TableViewCell.h`)
| +48 / −0 | Public constructor throws `E_INVALIDARG` for null args,
negative/out-of-range index or a foreign column; internal path uses
`TrustedOwnerTag` | 63787021 |
| `TableView/TableViewGroupHeader.cpp` (+ .h) | +39 / −0 | Default
count's opacity is not applied twice when disabled | 64475629 |
| `TableView/TableView.idl` | +31 / −13 | `TableViewColumnHeader`,
`TableView.ColumnHeaderStyle`,
`TableViewColumn.HeaderStyle`<br>`CanSort`/`SortByColumn`/`ToggleSortDirection`/`GridLinesVisibility`
docs; cell-peer `E_INVALIDARG`; group-header parts | 64475625, 63790855,
64143451, 63787021 |
| `TableView/TableView_Columns.cpp` | +19 / −0 | Releases removed
columns' header cells; old `UIElement` `Header` returned to the app on
change | 64475625 |
| `TableView/TableViewColumn.cpp` | +7 / −0 | `HeaderStyle` change
queues a header rebuild | 64475625 |
| `TableView/TableViewSource.h` | +6 / −0 | `IsOwnedByOther` helper for
sort sync | 63790855 |
| `CommonStyles/TabularSurfaces_themeresources.xaml` | +5 / −2 | New
`TabularSurfaceOuterBorderBrush` (Dark/Light/HC); group count opacity
documented as secondary-over-primary text (0.77 Dark) | 64475611,
64475629 |
| `TableView/TableView.vcxitems` | +2 / −1 | Swap
`TableViewHeaderCell.h` for `TableViewColumnHeader.*` | 64475625 |
| Generated / metadata (`Generated/TableView.properties.*`,
`Generated/TableViewColumn.properties.*`,
`dll-tabular/XamlMetadataProviderGenerated.h`) | +123 / −0 | Codegen for
the new properties and type | 64475625 |

**Total: 17 rows, 29 product files, +2117 / −738.** Also: 19 test files
(+3718 / −32), 3 docs (+161 / −35), 6 sample files (+2 / −9).

</details>

### New preview API (needs API review)

- **`TableViewColumnHeader`**: sealed, templated `ContentControl`,
created only by `TableView`.
  - Read-only `Column` property.
- Template parts (optional): `PART_ContentPresenter`, `PART_GridLine`,
`PART_SortIndicatorHost`, `PART_ResizeGripperHost`.
- Visual state groups: `CommonStates`, `HighContrastStates`,
`SortStates`.
- **`TableView.ColumnHeaderStyle`**: Style applied to every column
header.
- **`TableViewColumn.HeaderStyle`**: per-column Style; wins over
`TableView.ColumnHeaderStyle`.

There is no `HeaderGridLinesVisibility` property. `GridLinesVisibility`
affects body lines only; header lines are always drawn. To hide them,
override `TabularSurfaceHeaderSeparatorBrush` with `Transparent`.

### Current Behavior
- Headers are built in C++ with no template, no visual states and forced
local typography, so apps can't restyle them. Header separators don't
show.
- Header peers report `HeaderItem` without Transform, so UIA clients
can't resize columns.
- `SortByColumn` ignores `CanSort`; `CanSort=false` on a sorted column
leaves the sort in place; sorting through a source another table owns
reports success; some implicit clears raise `Sorted(None)` while the
app's sort remains.
- The cell peer constructor accepts invalid arguments; a throw in
`BeginEdit` leaves edit state half-open.
- Recycling a row rebuilt every cell, refreshed chrome up to twice and
invalidated the table's measure.
- The outer border has no dedicated brush; the default group header uses
raw weights and off-grid spacing.

### New Behavior
- Headers use the templated `TableViewColumnHeader` (see API above);
typography, padding and brushes come from Style setters.
- Header peers implement `ITransformProvider` through the existing
resize path (Min/Max clamping, scale conversion; pinned columns report
`CanResize=false`).
- `CanSort=false` blocks header clicks, `SortByColumn` (except `None`)
and `ToggleSortDirection`, and clears the column's own active sort;
sorts through a source owned by another table return false; `Sorted` is
raised only when the table's sort state changes.
- Cell peer constructor throws `E_INVALIDARG` on bad input; `BeginEdit`
rolls back on failure.
- Recycled rows reuse their cells unless cell automation peers exist;
chrome refreshes once.

> **Visible changes**
> - Outer border uses `CardStrokeColorDefault`.
> - Header lines are always shown, even when `GridLinesVisibility` is
`None` or `Horizontal`.
> - Group header restyled: 8px spacing; the count follows the header
`Foreground`.
> - Disabled header text and sort glyph dim.
> - TableView samples no longer opt out of High Contrast backplates
app-wide.

## Customer Impact

Restylable column headers, visible header separators, Fluent-aligned
outer border and group headers, UIA column resize for assistive
technology, smoother scrolling and predictable sort events.

## Regression Potential

- [ ] Low risk — isolated change, limited scope
- [x] Medium risk — touches shared components or public APIs
- [ ] High risk — architectural or breaking API change

Header construction (focus restore, frozen columns, UIA peer, tooltips,
resize, sort glyph) moved to a templated control. Sort event semantics
are tighter: `CanSort` is enforced on programmatic sorts and fewer
`Sorted` events fire. Outer border color and always-on header lines
change visibly. Covered by new and existing tests.

## How Has This Been Tested?

- [x] I have performed a self-review of my own code
- [x] I have added tests to cover my changes
- [x] Existing tests pass locally

Full TableView API and interaction suites on test VMs: 877/882 pass at
224d10d5c; 3 Localization tests are [Ignore] and 2 intermittent failures
passed 3/3 on rerun. Product, test apps and TableView sample apps build
(x64 chk).

Also covered:
`VerifyDisabledGroupHeaderCountFollowsContentTemplateChanges`.

Key new tests:
- `VerifyColumnHeaderTemplateParts`, `VerifyColumnHeaderSortStates`,
`VerifyColumnHeaderTypographyHasNoLocalValues`
- `VerifyColumnHeaderStyleApplies`,
`VerifyBasedOnOnlyColumnHeaderStyleApplies`,
`VerifyIncompatibleColumnHeaderStyleIsIgnored`
- `VerifyHeaderGridLinesShowWhateverTheBodyGridLines`,
`VerifyTransparentHeaderSeparatorBrushHidesHeaderLines`,
`VerifyGridLineAndOuterBorderBrushesMatchFluentStrokeTokens`
- `VerifyDisablingHighContrastHoveredHeaderKeepsDisabledForeground`,
`VerifyDefaultGroupHeaderCountInheritsForegroundThatFollowsStates`
- `VerifyTransformResizeAccountsForAncestorScale`,
`VerifyTransformResizeSmallWidthChangeWritesOnce`,
`VerifyHeaderDisabledByStyleIsNotEnabledOrInvokable`
- `VerifySortByColumnRespectsCanSort`,
`VerifyImplicitClearKeepsSourceSort`,
`VerifyUngatingKeepsAppLitColumnWithoutEvents`,
`VerifySortOptOutDuringBeginningEditKeepsTheEditOpen`
- `VerifyRecycledRowWithoutCellPeersReusesItsCells`,
`VerifyHeaderFocusStaysInHeaderBandWhenColumnsReplaced`,
`VerifyBeginEditThatThrowsRollsBackAndAllowsALaterEdit`,
`VerifyCellPeerConstructorValidatesArguments`

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 94961bb8-d187-483c-bece-19e203752c67
2026-10-23 14:10:17 +05:30

231 lines
10 KiB
XML

<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="TableViewSampleApp.Controls.SamplePresenter"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:TableViewSampleApp.Controls"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<!--
The header sits above OuterScroller so it stays visible. The Options column is a fixed 320 px:
auto-sizing made the table shake between pages. Narrow layouts stack the rail under the Example.
-->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid x:Name="HeaderPanel" Grid.Row="0" RowSpacing="8" MaxWidth="{StaticResource PageMaxContentWidth}" HorizontalAlignment="Left">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock
x:Name="HeaderText"
Grid.Row="0"
Style="{ThemeResource TitleTextBlockStyle}"
Text="{x:Bind Header, Mode=OneWay}"
AutomationProperties.HeadingLevel="Level1"
VerticalAlignment="Center"/>
<!-- Description -->
<TextBlock
x:Name="DescriptionText"
Grid.Row="1"
Style="{ThemeResource BodyTextBlockStyle}"
Text="{x:Bind Description, Mode=OneWay}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
MaxWidth="1000"
HorizontalAlignment="Left"/>
</Grid>
<ScrollViewer
x:Name="OuterScroller"
Grid.Row="1"
VerticalScrollMode="Auto"
VerticalScrollBarVisibility="Auto"
HorizontalScrollMode="Disabled"
HorizontalScrollBarVisibility="Disabled">
<Grid x:Name="OuterGrid" Margin="{StaticResource PageContentMargin}" RowSpacing="8" MaxWidth="{StaticResource PageMaxContentWidth}" HorizontalAlignment="Left">
<Grid.RowDefinitions>
<RowDefinition x:Name="ExampleRow" Height="Auto" MinHeight="320"/>
<RowDefinition x:Name="SourceRow" Height="Auto"/>
<RowDefinition x:Name="NarrowSourceRow" Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition x:Name="OptionsColumn" Width="0"/>
</Grid.ColumnDefinitions>
<!-- No card frame: tables draw their own border. -->
<!-- No x:Name on the InfoBars: WinUI reports an unset AutomationId as the x:Name. -->
<Grid x:Name="ExampleGrid" Grid.Row="0" Grid.Column="0" RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition x:Name="ExampleHostRow" Height="Auto"/>
</Grid.RowDefinitions>
<!-- Not an InfoBar, which announces on open. Unnamed for the same AutomationId reason. -->
<Border
Grid.Row="0"
MinHeight="104"
Padding="16,14"
CornerRadius="{ThemeResource ControlCornerRadius}"
BorderThickness="1"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
Background="{ThemeResource SystemFillColorAttentionBackgroundBrush}"
Visibility="Collapsed">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<FontIcon
Glyph="&#xE946;"
FontSize="16"
VerticalAlignment="Top"
Margin="0,2,0,0"
Foreground="{ThemeResource SystemFillColorAttentionBrush}"
AutomationProperties.AccessibilityView="Raw"/>
<TextBlock Grid.Column="1" TextWrapping="Wrap">
<Run Text="Try it" FontWeight="SemiBold"/><Run Text=" "/><Run x:Name="TryItMessageRun"/>
</TextBlock>
</Grid>
</Border>
<ContentControl
x:Name="ExampleHost"
Grid.Row="1"
IsTabStop="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Content="{x:Bind Example, Mode=OneWay}"/>
</Grid>
<!-- Options rail (right in wide layouts, stacked under the example in narrow
layouts) — collapses when Options is null. In wide mode its width is
governed by the parent OptionsColumn (fixed 320 px) so the table never
shakes, and RailHost bounds its height so the rail scrolls, not the page. -->
<local:RailHost
x:Name="OptionsRail"
Grid.Row="0" Grid.Column="1"
Margin="12,0,0,0"
Visibility="Collapsed">
<Border
x:Name="OptionsBorder"
CornerRadius="6"
BorderThickness="1"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
Background="{ThemeResource LayerFillColorDefaultBrush}">
<Grid x:Name="RailGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<SelectorBar
x:Name="RailTabs"
Margin="8,4,8,0"
AutomationProperties.AutomationId="RailTabs"
AutomationProperties.Name="Sample panels"
SelectionChanged="OnRailTabsSelectionChanged">
<SelectorBarItem
x:Name="RailOptionsTab"
Text="Options"
IsSelected="True"
AutomationProperties.AutomationId="RailOptionsTab"
AutomationProperties.Name="Options"/>
<SelectorBarItem
x:Name="RailInfoTab"
Text="Info"
AutomationProperties.AutomationId="RailInfoTab"
AutomationProperties.Name="Info"/>
</SelectorBar>
<!-- A tab stop only while the text-only Info tab is shown, so arrows can scroll it. -->
<ScrollViewer
Grid.Row="1"
VerticalScrollMode="Auto"
VerticalScrollBarVisibility="Auto"
HorizontalScrollMode="Disabled"
HorizontalScrollBarVisibility="Disabled"
IsVerticalScrollChainingEnabled="False"
IsTabStop="False"
UseSystemFocusVisuals="True"
AutomationProperties.Name="Sample panel content">
<!-- Right padding clears the overlay scrollbar when it expands (64478783). -->
<Grid Padding="16,8,24,16">
<ContentControl
x:Name="OptionsHost"
IsTabStop="False"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Content="{x:Bind Options, Mode=OneWay}"/>
<StackPanel x:Name="InfoPanel" Spacing="12" Visibility="Collapsed">
<InfoBar
Title="What to look for"
Severity="Informational"
IsOpen="True"
IsClosable="False"
Visibility="Collapsed"/>
<ContentControl
x:Name="InfoExtrasHost"
IsTabStop="False"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Content="{x:Bind InfoExtras, Mode=OneWay}"/>
<local:KeyboardReference x:Name="KeyboardReferenceSection"/>
</StackPanel>
</Grid>
</ScrollViewer>
</Grid>
</Border>
</local:RailHost>
<!-- Source expander spans both columns -->
<Expander
x:Name="SourceExpander"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Margin="0,4,0,0"
MinHeight="40"
Padding="12"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
AutomationProperties.AutomationId="SampleSourceExpander"
AutomationProperties.Name="Source code"
Expanding="OnSourceExpanderExpanding"
Collapsed="OnSourceExpanderCollapsed"
Visibility="Collapsed">
<Expander.Header>
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon Glyph="&#xE943;" FontSize="14" AutomationProperties.AccessibilityView="Raw"/>
<TextBlock Text="Source" FontWeight="SemiBold"/>
</StackPanel>
</Expander.Header>
<ScrollViewer
x:Name="SourceContentScroller"
VerticalScrollMode="Auto"
VerticalScrollBarVisibility="Auto"
HorizontalScrollMode="Disabled"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Spacing="12">
<local:CodeBlock
x:Name="SourceCodeBlock"
Caption="{x:Bind SourceCaption, Mode=OneWay}"
Code="{x:Bind SourceXaml, Mode=OneWay}"/>
<local:CodeBlock
x:Name="AdditionalSourceCodeBlock"
Caption="{x:Bind AdditionalSnippetCaption, Mode=OneWay}"
Code="{x:Bind AdditionalSourceCode, Mode=OneWay}"
Visibility="{x:Bind AdditionalSourceVisibility, Mode=OneWay}"/>
</StackPanel>
</ScrollViewer>
</Expander>
</Grid>
</ScrollViewer>
</Grid>
</UserControl>