mirror of
https://github.com/microsoft/microsoft-ui-xaml.git
synced 2026-10-28 16:35:19 +08:00
## 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
526 lines
19 KiB
C++
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));
|
|
}
|
|
}
|