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

526 lines
19 KiB
C++

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
#include "pch.h"
#include "common.h"
#include "TableViewColumn.h"
#include "TableView.h"
#include "TableViewRow.h"
#include "TVDiag.h"
#include <algorithm>
#include <atomic>
#include <cmath>
#include <limits>
namespace
{
std::atomic<uint32_t> s_nextAutomationIdentity{ 1 };
bool TryFocusEditor(winrt::FrameworkElement const& editor)
{
if (editor.Focus(winrt::FocusState::Programmatic))
{
return true;
}
if (auto const target = winrt::FocusManager::FindFirstFocusableElement(editor))
{
if (auto const control = target.try_as<winrt::Control>())
{
return control.Focus(winrt::FocusState::Programmatic);
}
if (auto const element = target.try_as<winrt::UIElement>())
{
return element.Focus(winrt::FocusState::Programmatic);
}
}
return false;
}
}
TableViewColumn::TableViewColumn()
{
// Defaults let initial ActualWidth equal Width without clamping.
}
winrt::FrameworkElement TableViewColumn::GenerateElement(const winrt::IInspectable& dataItem)
{
// Forward to the overridable display-cell factory.
return overridable().GenerateElementCore(dataItem);
}
winrt::FrameworkElement TableViewColumn::GenerateElementCore(const winrt::IInspectable& /*dataItem*/)
{
// Base columns have no display surface.
return nullptr;
}
winrt::FrameworkElement TableViewColumn::GenerateEditingElement(const winrt::IInspectable& dataItem)
{
// Forward to the overridable editing-cell factory.
return GenerateEditingElementCore(dataItem);
}
winrt::FrameworkElement TableViewColumn::GenerateEditingElementCore(const winrt::IInspectable& /*dataItem*/)
{
// A CellEditingTemplate is the column-agnostic way to supply an editor, so the base honours it
// for every column type. Without one a base column declines the edit: returning null is how a
// column says "this cell cannot be edited", rather than opening an empty editor over it.
auto const editingTemplate = CellEditingTemplate();
if (!editingTemplate)
{
return nullptr;
}
winrt::ContentPresenter presenter;
presenter.HorizontalAlignment(winrt::HorizontalAlignment::Stretch);
presenter.VerticalAlignment(winrt::VerticalAlignment::Stretch);
presenter.ContentTemplate(editingTemplate);
// Unlike the display cell - whose Content is bound by TableViewRow::RebuildCells so it survives
// recycle - the editing presenter lives only for the duration of one edit on one item, so its
// Content is bound here against its own inherited DataContext.
winrt::Microsoft::UI::Xaml::Data::Binding contentBinding;
contentBinding.Path(winrt::PropertyPath{ L"DataContext" });
contentBinding.RelativeSource(winrt::Microsoft::UI::Xaml::Data::RelativeSource{});
contentBinding.RelativeSource().Mode(winrt::Microsoft::UI::Xaml::Data::RelativeSourceMode::Self);
winrt::BindingOperations::SetBinding(presenter, winrt::ContentPresenter::ContentProperty(), contentBinding);
return presenter;
}
winrt::IInspectable TableViewColumn::PrepareCellForEdit(const winrt::FrameworkElement& editingElement, const winrt::RoutedEventArgs& editingEventArgs)
{
return PrepareCellForEditCore(editingElement, editingEventArgs);
}
winrt::IInspectable TableViewColumn::PrepareCellForEditCore(const winrt::FrameworkElement& editingElement, const winrt::RoutedEventArgs& /*editingEventArgs*/)
{
if (!editingElement)
{
return nullptr;
}
// Focus so typing goes straight into the editor; otherwise the row keeps focus and the user has
// to click the editor they just opened. The editing root is not necessarily focusable - a
// template column produces a ContentPresenter - so fall back to its first focusable descendant.
auto const root = editingElement.XamlRoot();
auto const initialFocus = root ? winrt::FocusManager::GetFocusedElement(root) : nullptr;
if (!TryFocusEditor(editingElement))
{
if (root && winrt::FocusManager::GetFocusedElement(root) != initialFocus)
{
return nullptr;
}
auto const owner = GetOwningTableView();
auto const editingParent = winrt::VisualTreeHelper::GetParent(editingElement);
winrt::TableViewRow row{ nullptr };
for (auto parent = editingParent; parent;
parent = winrt::VisualTreeHelper::GetParent(parent))
{
if (auto const candidate = parent.try_as<winrt::TableViewRow>())
{
row = candidate;
break;
}
}
auto const item = row ? row.DataContext() : nullptr;
// The editing ContentPresenter may not have instantiated its DataTemplate.
// Realize it before returning from F2 so the first keystroke has a target.
editingElement.UpdateLayout();
// Layout can run application code and recycle the row or end the edit.
if (owner && (GetOwningTableView() != owner ||
winrt::get_self<TableView>(owner)->CurrentEditingElement() != editingElement ||
!editingParent || winrt::VisualTreeHelper::GetParent(editingElement) != editingParent ||
!row || winrt::get_self<TableViewRow>(row)->GetOwningTableView() != owner ||
!TableView::SameInspectableIdentity(row.DataContext(), item)))
{
return nullptr;
}
if (root && winrt::FocusManager::GetFocusedElement(root) != initialFocus)
{
// A Loaded/focus handler chose another target; preserve that choice.
return nullptr;
}
if (!TryFocusEditor(editingElement))
{
TVDiag::LogRetailF(L"[TableView] The realized cell editor has no available keyboard focus target.");
}
}
// The base has no single value to snapshot - cancel is served by re-pulling from the source
// (see CancelCellEditCore), which needs nothing carried across.
return nullptr;
}
bool TableViewColumn::CommitCellEdit(const winrt::FrameworkElement& editingElement)
{
return CommitCellEditCore(editingElement);
}
// The editing bindings use UpdateSourceTrigger::Explicit, so this is what actually moves the typed
// value onto the data item.
//
// Resolved HERE rather than when the edit opened: a CellEditingTemplate's ContentPresenter has not
// stamped its template at begin time, so a walk then finds nothing. By commit time the editor is
// realized. WPF sidesteps the same ordering problem with an UpdateLayout() call in BeginEdit.
//
// Returns false when nothing could be written, so the control keeps the edit open instead of
// reporting a commit that never reached the item.
bool TableViewColumn::CommitCellEditCore(const winrt::FrameworkElement& editingElement)
{
auto const expressions = TableViewColumn::CollectEditingBindingExpressions(editingElement);
if (expressions.empty())
{
return false;
}
bool wroteEverything = true;
for (auto const& expression : expressions)
{
try
{
expression.UpdateSource();
}
catch (...)
{
// A setter that throws is the app rejecting the value, not a control failure.
wroteEverything = false;
}
}
return wroteEverything;
}
void TableViewColumn::CancelCellEdit(const winrt::FrameworkElement& editingElement, const winrt::IInspectable& uneditedValue)
{
CancelCellEditCore(editingElement, uneditedValue);
}
// Cancel needs to do nothing in the general case, and deliberately so.
//
// The editing binding is UpdateSourceTrigger::Explicit, so a cancelled edit never reached the data
// item: the source still holds the pre-edit value. The editor is then discarded and the display
// element - bound to that same untouched source - is put back. WinUI's BindingExpression has no
// UpdateTarget(), but none is needed, because the target being refreshed is thrown away.
//
// A column whose editor writes outside its binding must override this.
void TableViewColumn::CancelCellEditCore(const winrt::FrameworkElement& /*editingElement*/, const winrt::IInspectable& /*uneditedValue*/)
{
}
// Binding expressions on the editor subtree, for the properties an editor realistically writes.
// Used by the base commit so a CellEditingTemplate works without the column knowing what the app
// put in it. A column that knows its own editor should override and answer exactly.
std::vector<winrt::BindingExpression> TableViewColumn::CollectEditingBindingExpressions(const winrt::FrameworkElement& element)
{
std::vector<winrt::BindingExpression> expressions;
if (!element)
{
return expressions;
}
std::vector<winrt::DependencyProperty> properties;
if (element.try_as<winrt::TextBox>())
{
properties.push_back(winrt::TextBox::TextProperty());
}
if (element.try_as<winrt::AutoSuggestBox>())
{
properties.push_back(winrt::AutoSuggestBox::TextProperty());
}
if (element.try_as<winrt::ToggleSwitch>())
{
properties.push_back(winrt::ToggleSwitch::IsOnProperty());
}
if (element.try_as<winrt::ToggleButton>())
{
properties.push_back(winrt::ToggleButton::IsCheckedProperty());
}
if (element.try_as<winrt::Selector>())
{
properties.push_back(winrt::Selector::SelectedItemProperty());
properties.push_back(winrt::Selector::SelectedIndexProperty());
properties.push_back(winrt::Selector::SelectedValueProperty());
}
if (element.try_as<winrt::Slider>())
{
properties.push_back(winrt::RangeBase::ValueProperty());
}
if (element.try_as<winrt::NumberBox>())
{
properties.push_back(winrt::NumberBox::ValueProperty());
}
for (auto const& property : properties)
{
if (auto const expression = element.GetBindingExpression(property))
{
expressions.push_back(expression);
}
}
const auto childCount = winrt::VisualTreeHelper::GetChildrenCount(element);
for (int32_t i = 0; i < childCount; ++i)
{
if (auto const child = winrt::VisualTreeHelper::GetChild(element, i).try_as<winrt::FrameworkElement>())
{
auto childExpressions = TableViewColumn::CollectEditingBindingExpressions(child);
expressions.insert(expressions.end(), childExpressions.begin(), childExpressions.end());
}
}
return expressions;
}
// Typed accessor for the owning TableView.
winrt::TableView TableViewColumn::GetOwningTableView()
{
// Keep the owner weak; callers acquire a strong ref only for synchronous work.
return m_owningTableView.get();
}
uint32_t TableViewColumn::AutomationIdentity()
{
if (m_automationIdentity == 0)
{
m_automationIdentity = s_nextAutomationIdentity.fetch_add(1);
}
return m_automationIdentity;
}
void TableViewColumn::OnPropertyChanged(const winrt::DependencyPropertyChangedEventArgs& args)
{
const auto property = args.Property();
if (property == s_WidthProperty ||
property == s_MinWidthProperty ||
property == s_MaxWidthProperty)
{
// Only an app-driven assignment replaces the authored width; a user resize does not.
if (property == s_WidthProperty && !m_inUserResize)
{
m_authoredWidth = ValueHelper<winrt::GridLength>::CastOrUnbox(args.NewValue());
}
UpdateActualWidth();
if (auto owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->OnColumnWidthChanged(*this);
}
}
else if (property == s_IsReadOnlyProperty)
{
// Closing an edit is the control's job - it owns the edit state - but the column is where
// the property change surfaces. Without this the editor stays open on a cell that has just
// declared itself read-only.
if (auto const owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->OnColumnIsReadOnlyChanged(*this);
}
}
else if (property == s_CellEditingTemplateProperty)
{
// Realized editors were generated from the previous template. An editor does not change
// what the cell displays, so the Auto width keeps its history.
NotifyCellContentChanged(false /* resetAutoWidth */);
}
else if (property == s_HeaderTemplateProperty ||
property == s_HeaderTemplateSelectorProperty ||
property == s_HeaderProperty)
{
// Re-render headers and recompute Auto widths after header content changes.
if (auto owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->OnColumnHeaderChanged(*this);
}
}
else if (property == s_HeaderToolTipProperty)
{
// Not the header branch above: that recomputes Auto width, which a tooltip cannot change.
if (auto owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->OnColumnHeaderToolTipChanged(*this);
}
}
else if (property == s_HeaderStyleProperty)
{
if (auto owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->QueueRebuildHeaders();
}
}
else if (property == s_FrozenEdgeProperty)
{
if (auto owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->OnColumnFrozenEdgeChanged(*this);
}
}
else if (property == s_CanResizeProperty)
{
// Adds or removes this column's gripper, so the header band has to be rebuilt.
if (auto owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->QueueRebuildHeaders();
}
}
else if (property == s_VisibilityProperty)
{
if (auto owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->OnColumnVisibilityChanged(*this);
}
}
else if (property == s_CanSortProperty)
{
// The sort affordance is stamped at header-build time, so opting a column in or out at
// runtime has to rebuild the headers for the chevron to appear or disappear.
if (auto owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->OnColumnCanSortChanged(*this);
}
}
}
void TableViewColumn::CustomSortComparer(winrt::ITableViewSortComparer const& value)
{
if (m_customSortComparer == value)
{
return;
}
m_customSortComparer = value;
// Swapping the comparer invalidates the order the column is currently sorted in, so re-apply
// the active direction rather than leaving the rows in the old comparer's order.
if (auto owner = GetOwningTableView(); owner && SortDirection() != winrt::SortDirection::None)
{
auto const direction = SortDirection();
auto ownerImpl = winrt::get_self<TableView>(owner);
// Force a re-sort: SortByColumn no-ops when the column already carries this direction.
ownerImpl->SortByColumn(*this, winrt::SortDirection::None);
ownerImpl->SortByColumn(*this, direction);
}
}
winrt::hstring TableViewColumn::GetSortMemberPathCore(){
return SortMemberPath();
}
void TableViewColumn::SetSortStateInternal(winrt::SortDirection direction)
{
if (SortDirection() == direction)
{
return;
}
// Read-only DP: same SetValue-via-key convention as ActualWidth.
SetValue(s_SortDirectionProperty, winrt::box_value(direction));
if (auto owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->RefreshSortIndicators();
}
}
// Lets a derived column refresh its realized cells when one of ITS OWN properties changes.
//
// The base deliberately does not enumerate derived types' dependency properties: a third-party
// column would then have no way to invalidate its cells without editing this file. This mirrors
// WPF, where DataGridColumn owns the notification and each derived column routes its own property
// callbacks into it.
void TableViewColumn::NotifyCellContentChanged(bool resetAutoWidth)
{
if (auto owner = GetOwningTableView())
{
winrt::get_self<TableView>(owner)->OnColumnCellTemplateChanged(*this, resetAutoWidth);
}
}
winrt::Microsoft::UI::Xaml::Data::Binding TableViewColumn::CellToolTipBinding()
{
return m_cellToolTipBinding.get();
}
void TableViewColumn::CellToolTipBinding(const winrt::Microsoft::UI::Xaml::Data::Binding& value)
{
m_cellToolTipBinding.set(value);
// Realized cells carry the previous binding. A tooltip does not change what the cell displays,
// so the Auto width keeps its history.
NotifyCellContentChanged(false /* resetAutoWidth */);
}
bool TableViewColumn::SetOwningTableViewInternal(winrt::TableView const& owner)
{
// Keep the owner weak to avoid TableView -> Columns -> Column -> TableView cycles.
if (owner)
{
// Single-owner: detach (owner cleared to null) from the first TableView before attaching to another.
if (auto const existing = m_owningTableView.get(); existing && existing != owner)
{
// Runs inside a Columns VectorChanged callback; an escaping throw would failfast.
// Assert in debug, keep the existing owner in retail.
MUX_ASSERT_MSG(false, "TableViewColumn re-owned without removing it from the previous TableView.Columns first");
return false;
}
m_owningTableView = winrt::make_weak(owner);
return true;
}
else
{
m_owningTableView = nullptr;
return true;
}
}
void TableViewColumn::SetResolvedActualWidthInternal(double width)
{
// TableView resolved this column against the viewport / desired content. Uses the same
// SetValue-via-key read-only DP convention as UpdateActualWidth; the caller has already
// clamped to Min/MaxWidth.
if (std::abs(width - ActualWidth()) > 0.0001)
{
SetValue(s_ActualWidthProperty, winrt::box_value(width));
}
}
void TableViewColumn::SetDesiredWidthInternal(double rowsDesiredWidth, double headerDesiredWidth)
{
m_rowsDesiredWidth = std::max(0.0, rowsDesiredWidth);
m_headerDesiredWidth = std::max(0.0, headerDesiredWidth);
}
void TableViewColumn::ResetDesiredWidthInternal()
{
m_rowsDesiredWidth = 0.0;
m_headerDesiredWidth = 0.0;
}
void TableViewColumn::UpdateActualWidth()
{
// Pixel/Auto resolve to a fixed width here; Star gets a provisional default and is later
// resolved against the viewport by the owning TableView (TableView::ResolveColumnWidths).
const auto width = Width();
const double widthPixels =
width.GridUnitType == winrt::GridUnitType::Pixel
? NonNegativeFiniteOrDefault(width.Value)
: c_widthDefault.Value;
// Reject malformed widths and bounds the same way layout does, so provisional ActualWidth and
// resolved layout widths cannot drift.
const double lo = NonNegativeFiniteOrZero(MinWidth());
const double hi = MaxWidthForColumn(*this, lo);
const double clamped = std::clamp(widthPixels, lo, hi);
// ActualWidth uses the SetValue-via-key read-only DP convention.
if (std::abs(clamped - ActualWidth()) > 0.0001)
{
SetValue(s_ActualWidthProperty, winrt::box_value(clamped));
}
}