Files
microsoft-ui-xaml/controls/dev/TableView/TableView_Sort.cpp
T
Hitesh KumarandCopilot 32f8ff44af TableView: fix build after merging telemetry/TiP with source-reclaim rerank
#12120 added a TiP generation argument to SyncTableViewSourceSort; the
RerankCustomSortAfterReclaim call added by #12248 still passed two arguments
(C2660). The rerank is control-driven, so it carries no TiP operation (0).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b54b3a5d-2e82-43fa-9664-760e896cc4a3
2026-10-14 21:20:57 +05:30

1092 lines
38 KiB
C++

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.
//
// Single-column sort for TableView.
//
// The control owns the ordering for every source it can display: an ItemsSource that is not
// already a TableViewSource is projected through one, so a header click installs a sort axis on
// that projection and the rows re-project. Sorting is raised first as a cancellable pre-event; an
// app that would rather order its own collection cancels it and owns both the rows and the
// column's sort state from there.
//
// A column supplies its key either as a property path (SortMemberPath, or the cell binding for a
// text column) or as a CustomSortComparer. A comparer cannot be pushed into the projection - the
// projection sorts by key - so comparer columns are ranked up front and the rank becomes the key.
#include "pch.h"
#include "common.h"
#include "GroupedEntry.h"
#include "TableView.h"
#include "TableViewColumn.h"
#include "TableViewSource.h"
#include "TableViewSortingEventArgs.h"
#include "TableViewSortedEventArgs.h"
#include "TableViewAutomationHelpers.h"
#include "ResourceAccessor.h"
#include "Utils.h"
#include "ShapingHelpers.h"
#include "SortMemberPathResolver.h"
#include "TVDiag.h"
#include <algorithm>
#include <numeric>
#include <unordered_map>
void TableViewSourceSortBinding::ResetCustomSort()
{
if (CustomSortState)
{
CustomSortState->Reset();
}
}
void TableViewSourceSortBinding::Clear()
{
MemberPath = {};
AxisToken = {};
KeySelector = nullptr;
// Dropped, not reset: the axis left on a source this table no longer owns still ranks through
// it. The next sort mints a fresh adapter and selector.
CustomSortState.reset();
}
bool TableView::RepositionCustomSortItem(const winrt::IInspectable& item, bool reshapePending)
{
auto const& state = m_tableViewSourceSort.CustomSortState;
if (!state || !state->HasComparer())
{
return false;
}
if (!item)
{
state->InvalidateRanks();
return false;
}
return state->Reposition(item, reshapePending);
}
void TableView::ReleaseCustomSortRows(const winrt::IInspectable& item, const std::vector<winrt::IInspectable>& liveRows)
{
auto const& state = m_tableViewSourceSort.CustomSortState;
if (!state || !state->HasComparer())
{
return;
}
if (item)
{
state->Forget(item);
}
else
{
state->RetainOnly(liveRows);
}
}
// Rank() starts from the current rows, so it drops ranks rooted while released and re-reads values
// edited meanwhile; re-declaring the axis makes the projection read the new keys.
void TableView::RerankCustomSortAfterReclaim()
{
auto const& state = m_tableViewSourceSort.CustomSortState;
if (!state || !state->HasComparer() || m_sortedColumns.size() != 1)
{
return;
}
auto const column = m_sortedColumns.front().get();
if (!column || !column.CustomSortComparer())
{
return;
}
const auto direction = column.SortDirection();
if (direction == winrt::SortDirection::None || !CanApplySortTo(direction))
{
return;
}
const auto preservedSelection = SelectedItemInternal();
try
{
// A rerank after reclaim is control-driven, not a user sort, so it carries no TiP operation.
if (SyncTableViewSourceSort(column, direction, 0) && preservedSelection)
{
ApplySelection(FindEntryIndexForDataItem(preservedSelection));
}
}
catch (...)
{
// Dispatcher callback: best-effort, the previous order stands.
TVDiag::LogRetailF(L"[TableView] Re-ranking the custom sort after reclaim failed.");
}
}
namespace
{
winrt::hstring LocalizedOrFallback(std::wstring_view resourceName, std::wstring_view fallback) noexcept
{
try
{
if (auto const resolved = ResourceAccessor::GetLocalizedStringResource(resourceName); !resolved.empty())
{
return resolved;
}
}
catch (...)
{
}
return winrt::hstring{ fallback };
}
// Evaluates a property path against a row item; see SortMemberPathResolver.h. The same
// evaluator backs TableViewSource's path-based Sort verb, so a column and a fluent sort on
// the same path produce the same keys.
bool HasResolvedSortMemberPath(const winrt::TableViewColumn& column)
{
// Dispatch on the projection, not winrt::get_self: GetSortMemberPathCore is `overridable`,
// so get_self would call the C++ implementation's virtual directly and bypass WinRT
// composition. A column authored in C# that overrides it to compute a sort path would then
// be treated as having none, and silently become unsortable.
return column && !column.GetSortMemberPathCore().empty();
}
bool CanSortColumn(const winrt::TableViewColumn& column)
{
if (!column || !column.CanSort())
{
return false;
}
return column.CustomSortComparer() || HasResolvedSortMemberPath(column);
}
// Identity-derived so two columns with the same SortMemberPath still own separate axes; falls
// back to the path only if the column has no usable identity.
winrt::hstring GetTableViewSourceSortAxisToken(const winrt::TableViewColumn& column, const winrt::hstring& sortMemberPath)
{
if (column)
{
if (auto unknown = column.try_as<::IUnknown>())
{
wchar_t buffer[48];
swprintf_s(buffer, L"column:%p", unknown.get());
return winrt::hstring{ buffer };
}
}
return L"path:" + sortMemberPath;
}
}
winrt::TableViewSource TableView::ShapingSourceInternal() const
{
// AdoptItemsSource is the single place that decides what the source is; everything else
// reads the answer here rather than re-deriving it from ItemsSource.
return m_activeSource.get();
}
bool TableView::IsSortClearStillValid() const
{
return !m_sortedColumns.empty();
}
// Refused a source another table owns: there is nothing to sort until it is released, and the
// columns keep their state for the reconcile that follows.
bool TableView::CanSortColumnNow(const winrt::TableViewColumn& column) const
{
return !m_awaitedSource.get() && CanSortColumn(column);
}
bool TableView::CanApplySortTo(winrt::SortDirection direction) const
{
if (direction == winrt::SortDirection::None)
{
return true;
}
auto const source = ShapingSourceInternal();
return !source || winrt::get_self<::TableViewSource>(source)->CanApplySort();
}
bool TableView::IsSortRequestStillValid(const winrt::TableViewColumn& column) const
{
if (!column || !CanSortColumnNow(column))
{
return false;
}
// The column must still be one of ours: a Sorting handler is free to remove it.
if (auto columns = const_cast<TableView*>(this)->Columns())
{
for (auto const& ownedColumn : columns)
{
if (ownedColumn == column)
{
return true;
}
}
}
return false;
}
bool TableView::SortByColumn(const winrt::TableViewColumn& column, winrt::SortDirection direction)
{
return SortByColumnInternal(column, direction, true);
}
bool TableView::SortByColumnInternal(
const winrt::TableViewColumn& column, winrt::SortDirection direction, bool measureTip)
{
if (!CanSortColumnNow(column))
{
return false;
}
if (direction == winrt::SortDirection::None && m_sortedColumns.empty())
{
return false;
}
if (m_sortedColumns.size() == 1)
{
if (auto sortedColumn = m_sortedColumns.front().get())
{
if (sortedColumn == column && sortedColumn.SortDirection() == direction)
{
return false;
}
}
}
// A null item or one object on two rows can't be sorted; refuse before any state changes.
if (!CanApplySortTo(direction))
{
return false;
}
// An open editor is showing a row at an index that is about to move. Close it first; if it
// cannot close synchronously, replay this request once it does.
if (!TryTerminateEditForControlInitiatedReshape())
{
if (m_editState == EditState::Ending && !m_isApplyingCoalescedEditReshape)
{
QueueCoalescedEditReshape([this, column, direction, measureTip]()
{
SortByColumnInternal(column, direction, measureTip);
});
}
return false;
}
if (!IsSortRequestStillValid(column))
{
return false;
}
if (RaiseSortingAndCheckCanceled(column, direction))
{
return false;
}
// The edit commit and the Sorting handler are app code that can change the rows.
if (!IsSortRequestStillValid(column) || !CanApplySortTo(direction))
{
return false;
}
ApplySingleColumnSortState(column, direction);
RecomputeSortDPsAndRaiseInternal(column, measureTip);
DrainCoalescedEditReshape();
return true;
}
bool TableView::ToggleSortDirection(const winrt::TableViewColumn& column)
{
if (!CanSortColumnNow(column))
{
return false;
}
// The column's SortCycle names both halves of the policy: which direction an unsorted column
// opens in, and whether a trailing unsorted step exists. Read from the column being clicked,
// so columns in one table can carry different cycle policies.
//
// Opening Ascending: None -> Ascending -> Descending -> (None | Ascending)
// Opening Descending: None -> Descending -> Ascending -> (None | Descending)
const auto cycle = column.SortCycle();
const bool opensDescending =
cycle == winrt::TableViewSortCycle::DescendingAscending ||
cycle == winrt::TableViewSortCycle::DescendingAscendingNone;
const bool cyclesToUnsorted =
cycle == winrt::TableViewSortCycle::AscendingDescendingNone ||
cycle == winrt::TableViewSortCycle::DescendingAscendingNone;
const auto opening = opensDescending
? winrt::SortDirection::Descending
: winrt::SortDirection::Ascending;
const auto second = opensDescending
? winrt::SortDirection::Ascending
: winrt::SortDirection::Descending;
// Compared against the cycle's own directions rather than switching on Ascending/Descending,
// so the same walk serves both opening directions.
const auto current = column.SortDirection();
winrt::SortDirection next;
if (current == opening)
{
next = second;
}
else if (current == second)
{
next = cyclesToUnsorted ? winrt::SortDirection::None : opening;
}
else
{
next = opening;
}
if (!CanApplySortTo(next))
{
return false;
}
if (!TryTerminateEditForControlInitiatedReshape())
{
if (m_editState == EditState::Ending && !m_isApplyingCoalescedEditReshape)
{
QueueCoalescedEditReshape([this, column]()
{
ToggleSortDirection(column);
});
}
return false;
}
if (!IsSortRequestStillValid(column))
{
return false;
}
if (RaiseSortingAndCheckCanceled(column, next))
{
return false;
}
if (!IsSortRequestStillValid(column) || !CanApplySortTo(next))
{
return false;
}
ApplySingleColumnSortState(column, next);
RecomputeSortDPsAndRaiseInternal(column);
DrainCoalescedEditReshape();
return true;
}
bool TableView::ClearSort()
{
return ClearSortInternal(true);
}
bool TableView::ClearSortInternal(bool measureTip)
{
if (m_sortedColumns.empty())
{
return false;
}
if (!TryTerminateEditForControlInitiatedReshape())
{
if (m_editState == EditState::Ending && !m_isApplyingCoalescedEditReshape)
{
QueueCoalescedEditReshape([this, measureTip]()
{
ClearSortInternal(measureTip);
});
}
return false;
}
if (!IsSortClearStillValid())
{
return false;
}
if (RaiseSortingAndCheckCanceled(nullptr, winrt::SortDirection::None))
{
return false;
}
if (!IsSortClearStillValid())
{
return false;
}
for (auto const& weakColumn : m_sortedColumns)
{
if (auto column = weakColumn.get())
{
winrt::get_self<TableViewColumn>(column)->SetSortStateInternal(winrt::SortDirection::None);
}
}
m_sortedColumns.clear();
// Null trigger: Sorted fires with Column=null to signal a full clear.
RecomputeSortDPsAndRaiseInternal(nullptr, measureTip);
DrainCoalescedEditReshape();
return true;
}
void TableView::ResetSortStateForNewItemsSource()
{
// A new data set invalidates the sort, and the projection the sort was applied to is discarded
// with it. Left alone, the control keeps each column reporting a SortDirection and drawing the
// chevron for an order the new rows are not actually in. WPF DataGrid does exactly this in
// ClearSortDescriptionsOnItemsSourceChange, down to clearing each column's SortDirection.
//
// Silent, unlike ClearSort: there is no reshape to perform because the source is being rebuilt
// from scratch, and the data set is already gone by the time a handler could react, so this is
// not a cancellable Sorting nor a Sorted the app could act on.
if (m_sortedColumns.empty() && m_tableViewSourceSort.AxisToken.empty())
{
return;
}
for (auto const& weakColumn : m_sortedColumns)
{
if (auto column = weakColumn.get())
{
winrt::get_self<TableViewColumn>(column)->SetSortStateInternal(winrt::SortDirection::None);
}
}
m_sortedColumns.clear();
// The axis token and key selector belong to the projection being discarded, so carrying them
// into the next one would address an axis that no longer exists.
m_tableViewSourceSort.Clear();
}
// v1 is single-column sort, so applying a direction always clears every other column first.
void TableView::ApplySingleColumnSortState(const winrt::TableViewColumn& column, winrt::SortDirection direction)
{
for (auto const& weakColumn : m_sortedColumns)
{
if (auto sortedColumn = weakColumn.get(); sortedColumn && sortedColumn != column)
{
winrt::get_self<TableViewColumn>(sortedColumn)->SetSortStateInternal(winrt::SortDirection::None);
}
}
m_sortedColumns.clear();
winrt::get_self<TableViewColumn>(column)->SetSortStateInternal(direction);
if (direction != winrt::SortDirection::None)
{
m_sortedColumns.push_back(tracker_ref<winrt::TableViewColumn>{ this, column });
}
}
bool TableView::RaiseSortingAndCheckCanceled(
const winrt::TableViewColumn& trigger,
winrt::SortDirection direction)
{
if (!m_sortingEventSource)
{
return false;
}
auto args = winrt::make_self<TableViewSortingEventArgs>(trigger, direction);
m_sortingEventSource(*this, *args);
return args->Cancel();
}
winrt::TableViewKeySelector TableView::GetTableViewSourceSortKeySelector(
const winrt::hstring& sortMemberPath)
{
const bool sortMemberPathChanged = m_tableViewSourceSort.MemberPath != sortMemberPath;
if (!m_tableViewSourceSort.CustomSortState)
{
m_tableViewSourceSort.CustomSortState = std::make_shared<ShapingHelpers::CustomSortRankAdapter>();
}
m_tableViewSourceSort.MemberPath = sortMemberPath;
m_tableViewSourceSort.ResetCustomSort();
// Reused across re-sorts of the same path so the projection is not handed a new delegate
// identity every time. The selector closes over the state by shared_ptr, which is what lets
// ResetCustomSort swap the comparer underneath a live selector.
if (!m_tableViewSourceSort.KeySelector || sortMemberPathChanged)
{
auto customSortState = m_tableViewSourceSort.CustomSortState;
auto sortMemberPathResolver = std::make_shared<SortMemberPathResolver>(sortMemberPath);
m_tableViewSourceSort.KeySelector = winrt::TableViewKeySelector{
[sortMemberPathResolver, customSortState](const winrt::IInspectable& item) -> winrt::IInspectable
{
if (customSortState && customSortState->HasComparer())
{
return customSortState->KeyFor(item);
}
return sortMemberPathResolver->Resolve(item);
}
};
}
return m_tableViewSourceSort.KeySelector;
}
bool TableView::SyncTableViewSourceSort(
const winrt::TableViewColumn& trigger,
winrt::SortDirection direction,
uint64_t tipGeneration)
{
auto tableViewSource = ShapingSourceInternal();
if (!tableViewSource)
{
return false;
}
winrt::TableViewKeySelector keySelector{ nullptr };
winrt::hstring sortAxisToken;
// What this axis sorts on, purely descriptive. Stays empty for a CustomSortComparer: the
// ordering is the comparer's, not the path's, so nothing else may claim the axis sorts by a
// property.
winrt::hstring axisSortMemberPath;
if (trigger)
{
if (const auto customComparer = trigger.CustomSortComparer())
{
keySelector = GetTableViewSourceSortKeySelector(L"");
sortAxisToken = GetTableViewSourceSortAxisToken(trigger, L"");
if (direction != winrt::SortDirection::None)
{
auto rowsView = m_rowsItemsSourceView;
if (!rowsView)
{
rowsView = winrt::get_self<::TableViewSource>(tableViewSource)->GetItemsSourceView();
}
// Snapshot the rows for the engine, which ranks over a plain vector and never sees
// an ItemsSourceView. The comparer is wrapped into a neutral pairwise functor; the
// engine catches a throw and degrades it to "equal", so this lambda stays trivial.
std::vector<winrt::IInspectable> rows;
if (rowsView)
{
const auto count = rowsView.Count();
rows.reserve(count > 0 ? static_cast<size_t>(count) : 0);
for (int32_t i = 0; i < count; ++i)
{
auto const row = rowsView.GetAt(i);
// In a grouped projection the headers are the only synthesized rows; data
// rows are the app's own items. Rank the data items only - a header is not
// something the app's comparer has ever seen.
if (row && !TryGetGroupedEntry(row))
{
rows.push_back(row);
}
}
}
if (auto const& rankAdapter = m_tableViewSourceSort.CustomSortState)
{
ShapingHelpers::PairwiseComparer comparer =
[customComparer](winrt::IInspectable const& a, winrt::IInspectable const& b)
{
return static_cast<int>(customComparer.Compare(a, b));
};
rankAdapter->Rank(comparer, rows);
}
}
}
else
{
// Projected dispatch - see the note in HasResolvedSortMemberPath.
AppCalloutTip callout{ *this, TableViewTip::Stage::Sort, TableViewTip::AppFaultKind::Callback };
winrt::hstring sortMemberPath;
try { sortMemberPath = trigger.GetSortMemberPathCore(); }
catch (...) { callout.Fault(winrt::to_hresult()); throw; }
if (sortMemberPath.empty())
{
return false;
}
keySelector = GetTableViewSourceSortKeySelector(sortMemberPath);
sortAxisToken = GetTableViewSourceSortAxisToken(trigger, sortMemberPath);
axisSortMemberPath = sortMemberPath;
}
}
else
{
// Clear-all: reuse whatever axis is currently installed so it can be removed by token.
keySelector = m_tableViewSourceSort.KeySelector;
sortAxisToken = m_tableViewSourceSort.AxisToken;
if (direction == winrt::SortDirection::None)
{
m_tableViewSourceSort.ResetCustomSort();
}
}
if (!keySelector)
{
return false;
}
// Limitation: this reshapes on explicit sort and on collection notifications only.
// INotifyPropertyChanged-only mutations of the active sort key do not live re-sort until the
// next collection change.
auto tableViewSourceImpl = winrt::get_self<::TableViewSource>(tableViewSource);
// The control is the last writer here, so its axis becomes the only one. Without this an
// app-declared fluent Sort axis would survive alongside it and, being declared earlier, would
// outrank it as the primary sort while the chevron advertised this column.
auto const reconcileGuard = BeginControlInitiatedSortScope(tipGeneration);
if (direction == winrt::SortDirection::None)
{
// A control-initiated clear means "nothing is sorted", so it clears EVERY axis, not just
// the one this control installed. Clearing only its own token would leave an app-declared
// fluent axis ordering the rows while every chevron said the sort was off.
tableViewSourceImpl->ClearSort();
}
else
{
// Replacing rather than adding: re-sorting a column must swap its axis, not stack a
// second one on top of the first.
try
{
tableViewSourceImpl->SortReplacing(m_tableViewSourceSort.AxisToken, sortAxisToken, keySelector, axisSortMemberPath, direction);
}
catch (...)
{
// The axis is declared before the projection fails; keep owning it so the next sort
// replaces it instead of stacking on an orphan.
m_tableViewSourceSort.AxisToken = sortAxisToken;
throw;
}
tableViewSourceImpl->ClearSortsExcept(sortAxisToken);
}
if (direction == winrt::SortDirection::None)
{
m_tableViewSourceSort.Clear();
}
else
{
m_tableViewSourceSort.AxisToken = sortAxisToken;
}
// The verb mutated the active source in place, so the source lifetime is unchanged - just push
// its rewritten projection into the repeater.
RefreshRowsPipeline();
UpdateEmptyState();
return true;
}
bool TableView::PurgeColumnFromSortState(const winrt::TableViewColumn& removedColumn)
{
// Clear the leaving column's own SortDirection DP explicitly. The walk below drops it from
// m_sortedColumns, but the DP on the instance is sticky - a consumer who kept a strong
// reference and re-attached the column to another TableView would carry a phantom sort state
// across with it.
if (removedColumn != nullptr)
{
winrt::get_self<TableViewColumn>(removedColumn)->SetSortStateInternal(winrt::SortDirection::None);
}
bool removedAny = false;
auto it = m_sortedColumns.begin();
while (it != m_sortedColumns.end())
{
auto const column = it->get();
const bool isDead = (column == nullptr);
const bool isRemoved = (removedColumn != nullptr && column == removedColumn);
if (isDead || isRemoved)
{
it = m_sortedColumns.erase(it);
removedAny = true;
}
else
{
++it;
}
}
return removedAny;
}
int32_t TableView::FindEntryIndexForDataItem(const winrt::IInspectable& item) const
{
if (!item || !m_rowsItemsSourceView)
{
return -1;
}
// Identity, not equality: the projection holds the app's own objects, and a value-based match
// would re-select the wrong row whenever two rows compare equal.
auto const target = item.try_as<::IUnknown>();
if (!target)
{
return -1;
}
const auto count = m_rowsItemsSourceView.Count();
for (int32_t i = 0; i < count; ++i)
{
if (auto const candidate = m_rowsItemsSourceView.GetAt(i).try_as<::IUnknown>();
candidate && candidate.get() == target.get())
{
return i;
}
}
return -1;
}
void TableView::RecomputeSortDPsAndRaiseInternal(
const winrt::TableViewColumn& trigger, bool measureTip)
{
// Stale-deferred-clear guard. A null trigger means "clear", and every legitimate null-trigger
// caller - ClearSort, SortByColumn(col, None), ToggleSortDirection cycling to None - clears
// m_sortedColumns BEFORE calling this. So a null trigger arriving with a non-empty
// m_sortedColumns can only be a clear that was queued earlier and overtaken by a newer sort;
// running it would tear down the sort the app just asked for. If a future edit adds a
// null-trigger path that does not clear first, this guard will silently swallow a legitimate
// clear - update the invariant before adding one.
if (!trigger && !m_sortedColumns.empty())
{
return;
}
const auto invalidatedSort = !measureTip && m_tip.operation == TableViewTip::Operation::Sort
? m_tip.operationGeneration : 0;
auto const tipGeneration = BeginTipOperation(TableViewTip::Operation::Sort, measureTip);
m_controlSortPublishedGeneration = 0;
bool completed = false;
auto tipCompletion = wil::scope_exit([this, tipGeneration, measureTip, &completed]() noexcept
{
if (measureTip && !completed)
{
FailTipOperation(tipGeneration, TableViewTip::Stage::Sort, {});
}
else if (measureTip && m_controlSortPublishedGeneration == tipGeneration)
{
CompleteTipOperation(tipGeneration);
}
else if (measureTip)
{
TableViewTip::IgnoreOperation(m_tip, tipGeneration, TableViewTip::IgnoreReason::DeferredOrUnproven);
}
EndTipOperation();
});
// Reshapes through whichever source is active. A cancelled Sorting never reaches this point,
// so anything that does gets both the reshape and the published state.
const auto requestedDirection = trigger ? trigger.SortDirection() : winrt::SortDirection::None;
// Selection is index-based, and a re-sort moves every index. Capture the item, then re-find it
// afterwards so the selection follows the row rather than the slot.
const auto preservedSelection = SelectedItemInternal();
const bool reshaped = SyncTableViewSourceSort(trigger, requestedDirection, tipGeneration);
if (reshaped && invalidatedSort)
{
TableViewTip::IgnoreOperation(m_tip, invalidatedSort, TableViewTip::IgnoreReason::Stale);
}
if (!reshaped && tipGeneration == m_tip.operationGeneration)
{
TableViewTip::IgnoreOperation(m_tip, tipGeneration, TableViewTip::IgnoreReason::Stale);
}
if (reshaped && preservedSelection)
{
// A row that fell out of the projection surfaces as a real deselect, which is the honest
// answer - the selected item is no longer displayed.
ApplySelection(FindEntryIndexForDataItem(preservedSelection));
}
if (m_sortedEventSource)
{
const auto appliedDirection = trigger ? trigger.SortDirection() : winrt::SortDirection::None;
auto args = winrt::make_self<TableViewSortedEventArgs>(trigger, appliedDirection);
AppCalloutTip callout{ *this, TableViewTip::Stage::Sort, TableViewTip::AppFaultKind::Callback };
try
{
m_sortedEventSource(*this, *args);
}
catch (...)
{
callout.Fault(winrt::to_hresult());
throw;
}
}
if (winrt::AutomationPeer::ListenerExists(winrt::AutomationEvents::Notification))
{
// A programmatic or header-driven re-sort has no input event behind it, so without an
// announcement a screen-reader user has no way to learn the order changed.
if (trigger)
{
// The realized header peer also names template headers, which carry no string.
winrt::hstring header;
if (auto const headerPeer = GetRealizedColumnHeaderPeer(*this, trigger))
{
header = headerPeer.GetName();
}
if (header.empty())
{
header = TableViewDetails::GetColumnHeaderText(trigger);
}
switch (trigger.SortDirection())
{
case winrt::SortDirection::Ascending:
AnnounceSortChange(StringUtil::FormatString(
LocalizedOrFallback(SR_TableViewSortedAscending, L"Sorted by %1!s! ascending."), header.c_str()));
break;
case winrt::SortDirection::Descending:
AnnounceSortChange(StringUtil::FormatString(
LocalizedOrFallback(SR_TableViewSortedDescending, L"Sorted by %1!s! descending."), header.c_str()));
break;
case winrt::SortDirection::None:
default:
AnnounceSortChange(StringUtil::FormatString(
LocalizedOrFallback(SR_TableViewSortCleared, L"Sorting cleared for %1!s!."), header.c_str()));
break;
}
}
else
{
AnnounceSortChange(LocalizedOrFallback(SR_TableViewSortClearedAll, L"All sorting cleared."));
}
}
// The chevrons are already current: SetSortStateInternal republished them through
// RefreshSortIndicators as each column's DP was written.
completed = true;
}
void TableView::QueueReconcileSortStateWithSource()
{
// Deferred, never inline: the notification that brings us here is raised from inside the
// engine's own reshape, and reconciling clears an axis, which reshapes again.
if (m_sortReconcileQueued)
{
return;
}
m_sortReconcileQueued = true;
auto weakThis = get_weak();
if (auto const queue = winrt::DispatcherQueue::GetForCurrentThread())
{
queue.TryEnqueue([weakThis]()
{
if (auto strongThis = weakThis.get())
{
strongThis->m_sortReconcileQueued = false;
strongThis->ReconcileSortStateWithSource();
}
});
}
else
{
m_sortReconcileQueued = false;
}
}
void TableView::ReconcileSortStateWithSource()
{
auto tableViewSource = ShapingSourceInternal();
// Released on Unloaded: another table may own it now, and Loaded reconciles on reclaim.
if (!tableViewSource || m_activeSourceReleased)
{
return;
}
const bool rerankCustomSort = std::exchange(m_rerankCustomSortOnReconcile, false);
auto const& ownToken = m_tableViewSourceSort.AxisToken;
auto tableViewSourceImpl = winrt::get_self<::TableViewSource>(tableViewSource);
auto axes = tableViewSourceImpl->ActiveSortAxisInfos();
// A comparer axis another table (or this one, before an ItemsSource change) installed ranks
// through an adapter nothing here can update, so it would order rows on stale ranks.
bool clearedForeignComparerAxis = false;
for (auto const& axis : axes)
{
if (axis.AxisToken != ownToken && axis.SortMemberPath.empty() &&
std::wstring_view{ axis.AxisToken }.starts_with(L"column:"))
{
auto const reconcileGuard = BeginControlInitiatedSortScope();
tableViewSourceImpl->ClearSort(axis.AxisToken);
clearedForeignComparerAxis = true;
}
}
if (clearedForeignComparerAxis)
{
axes = tableViewSourceImpl->ActiveSortAxisInfos();
}
bool ownAxisPresent = false;
// Infos arrive in precedence order, so the first foreign axis is the one that outranks ours
// and is therefore the only one a single-column chevron could honestly describe.
::TableViewSource::ActiveSortAxisInfo const* primaryForeignAxis = nullptr;
for (auto const& axis : axes)
{
if (!ownToken.empty() && axis.AxisToken == ownToken)
{
ownAxisPresent = true;
}
else if (primaryForeignAxis == nullptr)
{
primaryForeignAxis = &axis;
}
}
if (ownAxisPresent && primaryForeignAxis == nullptr)
{
// The control's axis is still the only sort: chevrons already describe it.
if (rerankCustomSort)
{
RerankCustomSortAfterReclaim();
}
return;
}
if (primaryForeignAxis == nullptr && ownToken.empty() && m_sortedColumns.empty())
{
// Nothing sorted anywhere and no chevron lit. Reached on every shaping notification -
// a filter or group change also lands here - so it must not raise a spurious Sorted.
return;
}
if (primaryForeignAxis != nullptr && ownAxisPresent)
{
// The app declared a sort while ours was installed. Last writer wins, so ours goes: left
// alone it would keep whichever axis was declared FIRST as the primary sort, which is the
// hidden-precedence case this reconciliation exists to prevent.
auto const reconcileGuard = BeginControlInitiatedSortScope();
tableViewSourceImpl->ClearSort(ownToken);
}
// A path-declared axis names the property it sorts on, so the chevron can follow the app's own
// sort instead of going dark. A key-selector axis names nothing, and guessing which column it
// meant would be worse than showing no chevron at all.
winrt::TableViewColumn matchedColumn{ nullptr };
if (primaryForeignAxis != nullptr && !primaryForeignAxis->SortMemberPath.empty())
{
if (auto const columns = Columns())
{
for (auto const& column : columns)
{
if (column && column.GetSortMemberPathCore() == primaryForeignAxis->SortMemberPath)
{
matchedColumn = column;
break;
}
}
}
}
// matchedColumn can only be set from a non-null primaryForeignAxis, but that coupling is not
// something static analysis can prove at each use. Reading the direction once here keeps the
// dereference next to the null check it depends on.
auto const foreignDirection = primaryForeignAxis != nullptr
? primaryForeignAxis->Direction
: winrt::SortDirection::None;
// Already reconciled to exactly this state. Reached whenever an unrelated shaping change - a
// filter or a group - renotifies while an app-owned sort is standing; re-raising Sorted for it
// would report a sort change that did not happen.
if (ownToken.empty() && matchedColumn && m_sortedColumns.size() == 1)
{
if (auto const lit = m_sortedColumns[0].get();
lit == matchedColumn && lit.SortDirection() == foreignDirection)
{
return;
}
}
// Silent, like ResetSortStateForNewItemsSource: the app already owns the ordering by the time
// we get here, so there is nothing to cancel and no reshape of our own to perform. Sorted is
// still raised so an app tracking sort state sees the chevrons move.
for (auto const& weakColumn : m_sortedColumns)
{
if (auto column = weakColumn.get(); column && column != matchedColumn)
{
winrt::get_self<TableViewColumn>(column)->SetSortStateInternal(winrt::SortDirection::None);
}
}
m_sortedColumns.clear();
// m_tableViewSourceSort stays cleared even when a column is lit: the axis belongs to the app,
// and recording it as ours would make the next control-initiated sort try to replace an axis
// it never installed.
m_tableViewSourceSort.Clear();
if (matchedColumn)
{
winrt::get_self<TableViewColumn>(matchedColumn)->SetSortStateInternal(foreignDirection);
m_sortedColumns.push_back(tracker_ref<winrt::TableViewColumn>{ this, matchedColumn });
}
// Raised directly rather than through RecomputeSortDPsAndRaiseInternal: that path would reshape
// through SyncTableViewSourceSort and re-declare the axis as the control's own, undoing the
// app's ownership - and its stale-clear guard would swallow the event outright now that
// m_sortedColumns can be non-empty here.
if (m_sortedEventSource)
{
auto args = winrt::make_self<TableViewSortedEventArgs>(
matchedColumn,
matchedColumn ? foreignDirection : winrt::SortDirection::None);
m_sortedEventSource(*this, *args);
}
}
void TableView::QueueClearSortAfterColumnRemoval(){
if (m_clearSortAfterColumnRemovalQueued)
{
return;
}
m_clearSortAfterColumnRemovalQueued = true;
auto weakThis = get_weak();
if (auto const queue = winrt::DispatcherQueue::GetForCurrentThread())
{
queue.TryEnqueue([weakThis]()
{
if (auto strongThis = weakThis.get())
{
strongThis->m_clearSortAfterColumnRemovalQueued = false;
// RecomputeSortDPsAndRaiseInternal's stale-clear guard drops this if the app
// applied a new sort on a different column in the meantime.
strongThis->RecomputeSortDPsAndRaiseInternal(nullptr, false);
}
});
}
else
{
m_clearSortAfterColumnRemovalQueued = false;
}
}
void TableView::AnnounceSortChange(const winrt::hstring& announcement){
if (announcement.empty())
{
return;
}
if (!winrt::AutomationPeer::ListenerExists(winrt::AutomationEvents::Notification))
{
return;
}
try
{
// RaiseNotificationEvent rather than a live region: there is no announcement-only element
// in the template, and a notification carries its own text without one.
if (auto const peer = winrt::FrameworkElementAutomationPeer::FromElement(*this))
{
peer.RaiseNotificationEvent(
winrt::AutomationNotificationKind::ActionCompleted,
winrt::AutomationNotificationProcessing::MostRecent,
announcement,
L"TableViewSortChanged");
}
}
catch (...)
{
// No peer, or UIA is unavailable on this host: the sort itself already applied.
}
}