Files
microsoft-ui-xaml/controls/dev/TableView/TableView.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

611 lines
44 KiB
XML

<!-- Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. See LICENSE in the project root for license information. -->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.UI.Xaml.Controls">
<Thickness x:Key="TableViewHeaderCellPadding">8,4,8,4</Thickness>
<!-- Metric defaults remain available alongside the default styles. -->
<x:Double x:Key="TableViewResizeGripperWidth">8</x:Double>
<!-- TabularControlsResources is required to supply TabularSurface* brushes for all themes. -->
<!-- TableView default style. Header cells stack horizontally (one per
column); the body is an ItemsRepeater of TableViewRow, each building its cells from
the owning TableView's Columns. -->
<Style TargetType="controls:TableView">
<!-- The control is a focus SCOPE, not a focus STOP: Tab must land on the header band and
then the body, never on the table element itself. Matches the ListView default style
(dxaml\xcp\dxaml\themes\generic.xaml), which sets IsTabStop="False" for the same
reason. Control.IsTabStop defaults to true, so this has to be stated.
Deliberately NOT paired with TabNavigation="Once". ListView sets that because it wants
exactly ONE stop for the whole control; TableView wants TWO - the header band and the
body - and Once here would swallow the body, leaving it unreachable by Tab. The two
stops come from the two bands owning their own scopes: KeyboardNavigationMode::Once on
PART_HeaderHost (TableView::OnApplyTemplate) and the same mode that ItemsRepeater sets
on itself for PART_RowsRepeater. -->
<Setter Property="IsTabStop" Value="False" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="Background" Value="{ThemeResource TabularSurfaceRowBackgroundBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TabularSurfaceRowForegroundBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource TabularSurfaceOuterBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:TableView">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border x:Name="PART_HeaderRow"
Grid.Row="0"
Background="{ThemeResource TabularSurfaceHeaderBackgroundBrush}"
BorderBrush="{ThemeResource TabularSurfaceHeaderSeparatorBrush}"
BorderThickness="0,0,0,1">
<Grid>
<ScrollViewer x:Name="PART_HeaderScroller"
HorizontalScrollMode="Disabled"
VerticalScrollMode="Disabled"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Disabled">
<!-- Left, not the default Stretch: the panel reports the summed column
width, and a wider presenter would centre it. PART_CellsHost
must match. -->
<controls:TableViewCellsPanel x:Name="PART_HeaderHost"
HorizontalAlignment="Left" />
</ScrollViewer>
</Grid>
</Border>
<ScrollViewer x:Name="PART_BodyScroller"
Grid.Row="1"
HorizontalScrollMode="Auto"
HorizontalScrollBarVisibility="Auto"
VerticalScrollMode="Auto"
VerticalScrollBarVisibility="Auto"
VerticalContentAlignment="Stretch">
<Grid x:Name="PART_BodyContent" Background="Transparent">
<ItemsRepeater x:Name="PART_RowsRepeater"
VerticalCacheLength="2.0">
<ItemsRepeater.Layout>
<StackLayout Orientation="Vertical" />
</ItemsRepeater.Layout>
</ItemsRepeater>
<ContentControl x:Name="PART_EmptyStatePresenter"
Visibility="Collapsed"
IsTabStop="False"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center" />
</Grid>
</ScrollViewer>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="controls:TableViewColumnHeader">
<Setter Property="IsTabStop" Value="True" />
<Setter Property="UseSystemFocusVisuals" Value="True" />
<!-- Without a fill the padding takes no pointer input, killing the tooltip and click-to-sort there. -->
<Setter Property="Background" Value="Transparent" />
<Setter Property="FontSize" Value="{ThemeResource TableViewHeaderFontSize}" />
<Setter Property="FontWeight" Value="{ThemeResource TableViewHeaderFontWeight}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:TableViewColumnHeader">
<!-- CornerRadius is set only while keyboard-focused: a rounded root clips. -->
<Grid x:Name="PART_Root"
Background="{TemplateBinding Background}"
CornerRadius="{TemplateBinding CornerRadius}">
<!-- See TableViewRow: state groups must sit on the template root. -->
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="PART_Root.Background" Value="{ThemeResource TabularSurfaceHeaderBackgroundPointerOverBrush}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="PART_Root.Background" Value="{ThemeResource TabularSurfaceHeaderBackgroundPressedBrush}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="PART_ContentPresenter.Foreground" Value="{ThemeResource TabularSurfaceRowForegroundDisabledBrush}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="HighContrastStates">
<VisualState x:Name="HighContrastNormal" />
<VisualState x:Name="HighContrastPointerOver">
<VisualState.Setters>
<Setter Target="PART_ContentPresenter.Foreground" Value="{ThemeResource TabularSurfaceHeaderForegroundPointerOverBrush}" />
<Setter Target="PART_ContentPresenter.HighContrastAdjustment" Value="None" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="HighContrastPressed">
<VisualState.Setters>
<Setter Target="PART_ContentPresenter.Foreground" Value="{ThemeResource TabularSurfaceHeaderForegroundPressedBrush}" />
<Setter Target="PART_ContentPresenter.HighContrastAdjustment" Value="None" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="SortStates">
<VisualState x:Name="Unsorted" />
<VisualState x:Name="SortAscending">
<VisualState.Setters>
<Setter Target="PART_SortIndicatorHost.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SortDescending">
<VisualState.Setters>
<Setter Target="PART_SortIndicatorHost.Visibility" Value="Visible" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter x:Name="PART_ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
AutomationProperties.AccessibilityView="Raw" />
<Border x:Name="PART_GridLine"
Width="1"
HorizontalAlignment="Right"
Background="{ThemeResource TabularSurfaceHeaderSeparatorBrush}"
IsHitTestVisible="False"
Visibility="Collapsed"
AutomationProperties.AccessibilityView="Raw" />
<Border x:Name="PART_SortIndicatorHost"
HorizontalAlignment="Right"
VerticalAlignment="Center"
IsHitTestVisible="False"
Visibility="Collapsed"
AutomationProperties.AccessibilityView="Raw" />
<!-- Last, so the gripper wins the hit test on the shared edge. -->
<Border x:Name="PART_ResizeGripperHost" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<x:Double x:Key="TableViewRowExpanderSize">24</x:Double>
<!-- Indent added per hierarchy level on a tree row's first column. Override in an app's
resources to retheme every table, or in one TableView's Resources for a single table. -->
<x:Double x:Key="TableViewRowIndentSize">16</x:Double>
<Style TargetType="controls:TableViewRow">
<!-- The row IS a focus target. Body navigation is TWO-LEVEL, following the W3C ARIA APG
`treegrid` pattern: focus enters the body on a ROW, Right drills into that row's first
CELL, and Left from the first cell comes back out to the ROW. So Control.IsTabStop's
default of true is what we want here and there is deliberately no setter overriding it.
The row's IsTabStop is not static, though: TableViewRow::SetCellLevelInternal clears it
for exactly as long as the cursor is drilled into that row's cells, and restores it on
the way back out. That is what keeps the body ONE tab stop in BOTH directions - see the
comment on SetCellLevelInternal for why the framework's tab walk forces the trade. -->
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Background" Value="{ThemeResource TabularSurfaceRowBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource TabularSurfaceGridLineBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
<Setter Property="UseSystemFocusVisuals" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:TableViewRow">
<Grid>
<!-- State groups belong on the TEMPLATE ROOT: VisualStateManager resolves
them from the root's attached property, so hosting them on an inner
element disables every state silently - no parse error and no warning,
just a row that never paints its selected / hover / pressed visuals. -->
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_RootBorder" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowBackgroundPointerOverBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowForegroundPointerOverBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="HighContrastAdjustment">
<DiscreteObjectKeyFrame KeyTime="0" Value="None" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_RootBorder" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowBackgroundPressedBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowForegroundPressedBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="HighContrastAdjustment">
<DiscreteObjectKeyFrame KeyTime="0" Value="None" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_RootBorder" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowBackgroundDisabledBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowForegroundDisabledBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<!-- Selected states live in this SAME group rather than a second one:
they animate the same PART_RootBorder.Background, and two groups
would make the result depend on GoToState call order. One group
whose names encode both dimensions is the pattern TreeViewItem
and ItemContainer use. -->
<VisualState x:Name="Selected">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_RootBorder" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowBackgroundSelectedBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowForegroundSelectedBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="HighContrastAdjustment">
<DiscreteObjectKeyFrame KeyTime="0" Value="None" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="PART_SelectionIndicator" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0:0:0.083" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="SelectedPointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_RootBorder" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowBackgroundSelectedPointerOverBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowForegroundSelectedPointerOverBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="HighContrastAdjustment">
<DiscreteObjectKeyFrame KeyTime="0" Value="None" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="PART_SelectionIndicator" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0:0:0.083" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="SelectedPressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_RootBorder" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowBackgroundSelectedPressedBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowForegroundSelectedPressedBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="HighContrastAdjustment">
<DiscreteObjectKeyFrame KeyTime="0" Value="None" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="PART_SelectionIndicator" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0:0:0.083" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="SelectedDisabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_RootBorder" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowBackgroundDisabledBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="Foreground">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceRowForegroundDisabledBrush}" />
</ObjectAnimationUsingKeyFrames>
<!-- Suppress the High Contrast text backplate wherever the row paints a state fill (hover, press, selection). -->
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_CellForegroundPresenter" Storyboard.TargetProperty="HighContrastAdjustment">
<DiscreteObjectKeyFrame KeyTime="0" Value="None" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_SelectionIndicator" Storyboard.TargetProperty="Fill">
<DiscreteObjectKeyFrame KeyTime="0" Value="{ThemeResource TabularSurfaceSelectionIndicatorDisabledBrush}" />
</ObjectAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="PART_SelectionIndicator" Storyboard.TargetProperty="Opacity">
<LinearDoubleKeyFrame KeyTime="0:0:0.083" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<!-- Hierarchy groups. Inert for flat/grouped sources (Level 0). -->
<VisualStateGroup x:Name="RowExpansionStates">
<VisualState x:Name="RowCollapsed">
<VisualState.Setters>
<Setter Target="PART_RowExpanderIcon.Glyph" Value="&#xE76C;" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="RowExpanded">
<VisualState.Setters>
<Setter Target="PART_RowExpanderIcon.Glyph" Value="&#xE70D;" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="RowExpandabilityStates">
<VisualState x:Name="RowExpandable" />
<!-- Only the glyph hides; the gutter keeps its width so leaf text
aligns with sibling branch text. -->
<VisualState x:Name="RowNotExpandable">
<VisualState.Setters>
<Setter Target="PART_RowExpanderIcon.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="RowHierarchyStates">
<!-- TableViewRow sets gutter Visibility directly; these states exist
for retemplating apps to key off. -->
<VisualState x:Name="RowNotHierarchical" />
<VisualState x:Name="RowHierarchical" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="PART_RootBorder"
Grid.Row="0"
Grid.Column="0"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<Grid>
<!-- Cell host. TableViewRow populates PART_CellsHost with one cell wrapper
per TableViewColumn, in Columns order, laid out left-to-right.
The presenter exists so the Selected* states have something with a
Foreground to animate: PART_RootBorder is a Border and has none, and
cell text inherits Foreground down from here. Without it the selected
foreground brushes are unreachable, which is only cosmetic in
light/dark (the selected fill is ~4% alpha) but makes a selected row
illegible in High Contrast, where the fill is the opaque
SystemColorHighlightColor. -->
<ContentPresenter x:Name="PART_CellForegroundPresenter"
Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<Grid>
<!-- Left, matching PART_HeaderHost. -->
<controls:TableViewCellsPanel x:Name="PART_CellsHost"
HorizontalAlignment="Left" />
<!-- Hierarchy chevron. Overlays the first cell (TableViewRow pads it by
the same indent) so cells stay aligned with headers; later child so it
stays on top as the hit target. No own Foreground: inherits the
presenter's, keeping this optional part out of CommonStates. -->
<Border x:Name="PART_RowExpanderGutter"
Width="{ThemeResource TableViewRowExpanderSize}"
Background="Transparent"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Visibility="Collapsed">
<FontIcon x:Name="PART_RowExpanderIcon"
FontSize="12"
Glyph="&#xE76C;"
MirroredWhenRightToLeft="True"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</Grid>
</ContentPresenter>
<!-- Fluent selection indicator: overlays the leading edge, Opacity 0 by
default; the Selected* states animate it to 1. Not hit-testable, so it
cannot steal a press from the cell underneath it. -->
<Rectangle x:Name="PART_SelectionIndicator"
Width="3"
Height="16"
RadiusX="1.5"
RadiusY="1.5"
Fill="{ThemeResource TabularSurfaceSelectionIndicatorBrush}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Margin="2,0,0,0"
IsHitTestVisible="False"
Opacity="0" />
</Grid>
</Border>
<Border x:Name="PART_GridLineBorder"
Grid.Row="0"
Grid.Column="0"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
IsHitTestVisible="False" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
TableViewGroupHeader. The ControlTemplate owns the band chrome and the chevron
(PART_ExpanderGutter / PART_ExpanderIcon); PART_ContentPresenter is the only
app-customizable region, showing ContentTemplate over the TableViewGroupInfo Content.
ContentTemplate always resolves: the default Style seeds it, and
TableView.GroupHeaderTemplate overrides it as a local value that ClearValue reverts. So
there is no default-vs-custom branch, and an app template cannot drop the expander or
the band. Expansion, enabled and pointer state are VisualStates, so the chevron glyph
is declared here rather than assigned from C++.
-->
<x:Double x:Key="TableViewGroupExpanderSize">24</x:Double>
<!-- Gap between the expander gutter and the content; root rows are indented by size + spacing. -->
<x:Double x:Key="TableViewGroupExpanderSpacing">8</x:Double>
<Thickness x:Key="TableViewGroupHeaderPadding">8,8,12,8</Thickness>
<!-- BodyStrongTextBlockStyle equivalent, single line. -->
<Style x:Key="TableViewGroupHeaderKeyTextStyle" TargetType="TextBlock">
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="TextTrimming" Value="CharacterEllipsis" />
<Setter Property="TextWrapping" Value="NoWrap" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!-- Opacity is a Style setter, not a local value, so TableViewGroupHeader can override it while disabled and clear it back. -->
<Style x:Key="TableViewGroupHeaderCountTextStyle" TargetType="TextBlock">
<Setter Property="Opacity" Value="{ThemeResource TabularSurfaceGroupHeaderCountOpacity}" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<DataTemplate x:Key="TableViewDefaultGroupHeaderContentTemplate">
<StackPanel x:Name="TableViewDefaultGroupHeaderContentRoot" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<TextBlock Text="{Binding KeyText, Mode=OneWay}"
Style="{StaticResource TableViewGroupHeaderKeyTextStyle}" />
<!-- Inherits Foreground so it follows the header's states and an app's Foreground. -->
<TextBlock x:Name="TableViewGroupHeaderItemCountText"
Text="{Binding ItemCountText, Mode=OneWay}"
Style="{StaticResource TableViewGroupHeaderCountTextStyle}" />
</StackPanel>
</DataTemplate>
<Style TargetType="controls:TableViewGroupHeader">
<Setter Property="Background" Value="{ThemeResource TabularSurfaceGroupHeaderBackgroundBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TabularSurfaceGroupHeaderForegroundBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource TabularSurfaceGroupHeaderBorderBrush}" />
<Setter Property="BorderThickness" Value="{ThemeResource TabularSurfaceGroupHeaderBorderThickness}" />
<Setter Property="Padding" Value="{ThemeResource TableViewGroupHeaderPadding}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<!-- Focusable so keyboard users can expand/collapse: TableViewGroupHeader::OnKeyDown
handles Enter/Space (toggle) and Right/Left (expand/collapse), matching TreeViewItem. -->
<Setter Property="IsTabStop" Value="True" />
<Setter Property="UseSystemFocusVisuals" Value="True" />
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
<Setter Property="ContentTemplate" Value="{StaticResource TableViewDefaultGroupHeaderContentTemplate}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="controls:TableViewGroupHeader">
<Grid>
<!-- See TableViewRow: state groups must sit on the template root. -->
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="PART_Root.Background" Value="{ThemeResource TabularSurfaceRowBackgroundPointerOverBrush}" />
<Setter Target="PART_ContentPresenter.Foreground" Value="{ThemeResource TabularSurfaceRowForegroundPointerOverBrush}" />
<Setter Target="PART_ExpanderIcon.Foreground" Value="{ThemeResource TabularSurfaceRowForegroundPointerOverBrush}" />
<Setter Target="PART_ContentPresenter.HighContrastAdjustment" Value="None" />
<Setter Target="PART_ExpanderIcon.HighContrastAdjustment" Value="None" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="PART_Root.Background" Value="{ThemeResource TabularSurfaceRowBackgroundPressedBrush}" />
<Setter Target="PART_ContentPresenter.Foreground" Value="{ThemeResource TabularSurfaceRowForegroundPressedBrush}" />
<Setter Target="PART_ExpanderIcon.Foreground" Value="{ThemeResource TabularSurfaceRowForegroundPressedBrush}" />
<Setter Target="PART_ContentPresenter.HighContrastAdjustment" Value="None" />
<Setter Target="PART_ExpanderIcon.HighContrastAdjustment" Value="None" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="PART_ContentPresenter.Foreground" Value="{ThemeResource TabularSurfaceRowForegroundDisabledBrush}" />
<Setter Target="PART_ExpanderIcon.Foreground" Value="{ThemeResource TabularSurfaceRowForegroundDisabledBrush}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ExpansionStates">
<VisualState x:Name="Collapsed">
<VisualState.Setters>
<Setter Target="PART_ExpanderIcon.Glyph" Value="&#xE76C;" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Expanded">
<VisualState.Setters>
<Setter Target="PART_ExpanderIcon.Glyph" Value="&#xE70D;" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="ExpandabilityStates">
<VisualState x:Name="Expandable" />
<VisualState x:Name="NotExpandable">
<!-- Only the chevron hides; PART_ExpanderGutter keeps its width so
content stays aligned across expandable and non-expandable groups. -->
<VisualState.Setters>
<Setter Target="PART_ExpanderIcon.Visibility" Value="Collapsed" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="PART_Root"
Grid.Row="0"
Grid.Column="0"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="Stretch">
<Grid ColumnSpacing="{ThemeResource TableViewGroupExpanderSpacing}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- The chevron is a pure visual. The whole band is the toggle target
(TableViewGroupHeader::OnPointerReleased), matching ListView /
TreeView, so there is no nested Button to inflate, focus or name. -->
<Border x:Name="PART_ExpanderGutter"
Grid.Column="0"
Width="{ThemeResource TableViewGroupExpanderSize}">
<FontIcon x:Name="PART_ExpanderIcon"
FontSize="12"
Foreground="{TemplateBinding Foreground}"
MirroredWhenRightToLeft="True"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<ContentPresenter x:Name="PART_ContentPresenter"
Grid.Column="1"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
</Border>
<Border x:Name="PART_GridLineBorder"
Grid.Row="0"
Grid.Column="0"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
IsHitTestVisible="False" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>