Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions docs/docs/column-sizing.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,21 @@ Disable resizing for a specific column:
<tv:TableViewTextColumn Header="ID" Binding="{Binding Id}" CanResize="False" />
```

### ColumnResizeMode

[`ColumnResizeMode`](xref:WinUI.TableView.TableView.ColumnResizeMode) controls what happens to cells *while* the user is dragging a column divider, as distinct from the final committed width:

| Value | Description |
|---|---|
| [`Live`](xref:WinUI.TableView.TableViewColumnResizeMode.Live) (default) | Every visible row's cells resize for real on every pointer-move frame. Fully accurate at all times, but on grids with many visible rows the drag can feel less smooth. |
| [`Preview`](xref:WinUI.TableView.TableViewColumnResizeMode.Preview) | Cells appear to resize live via a lightweight visual preview; no row layout runs until the drag ends, so the drag stays smooth regardless of row count. The real width is committed in a single layout pass when the pointer is released. |

```xml
<tv:TableView ColumnResizeMode="Preview" />
```

Prefer `Preview` if users report a slow or stuttery resize drag on grids with many visible rows; keep the default `Live` if you need cells to reflect the in-progress width at every instant (for example, a template column that reacts to its own width while dragging).

## Row and header row heights

Control row heights with the following `TableView` properties:
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ tableView.RefreshFilter();

> **Tip**: Prefer `ObservableCollection<T>` with `INotifyPropertyChanged` models over manual refresh calls whenever possible, as it is more efficient and requires less code.

## Column resize drag performance

By default, dragging a column divider ([`ColumnResizeMode="Live"`](xref:WinUI.TableView.TableView.ColumnResizeMode)) relayouts every visible row's cells on every pointer-move frame. On grids with many visible rows this can make the drag itself feel less smooth, even though the final committed width is unaffected. Set `ColumnResizeMode="Preview"` to use a lightweight visual preview during the drag instead — no row layout runs until the pointer is released, so the drag stays smooth regardless of row count. See [Column sizing](column-sizing.md#columnresizemode).

## Horizontal scrolling and column count

Unlike rows, columns are not virtualized — all column headers are instantiated regardless of whether they are visible. A very large number of columns (100+) may affect horizontal scroll performance. In practice, most data grids have far fewer columns than rows.
Expand Down
10 changes: 9 additions & 1 deletion samples/WinUI.TableView.SampleApp/Pages/ColumnSizingPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,25 @@
Header="Column Auto Width Mode"
SelectedItem="{Binding ColumnAutoWidthMode, Mode=TwoWay, ElementName=tableView}"
ItemsSource="{ui:EnumValues Type=tv:TableViewColumnAutoWidthMode}" />
<ComboBox x:Name="columnResizeMode"
HorizontalAlignment="Stretch"
Header="Column Resize Mode"
SelectedItem="{Binding ColumnResizeMode, Mode=TwoWay, ElementName=tableView}"
ItemsSource="{ui:EnumValues Type=tv:TableViewColumnResizeMode}" />
</StackPanel>
</controls:SamplePresenter.Options>
<controls:SamplePresenter.Xaml>
<x:String xml:space="preserve">
&lt;tv:TableView ItemsSource="{Binding Items}"
ColumnAutoWidthMode="$(ColumnAutoWidthMode)"/>
ColumnAutoWidthMode="$(ColumnAutoWidthMode)"
ColumnResizeMode="$(ColumnResizeMode)"/>
</x:String>
</controls:SamplePresenter.Xaml>
<controls:SamplePresenter.Substitutions>
<controls:CodeSubstitution Key="ColumnAutoWidthMode"
Value="{x:Bind columnAutoWidthMode.SelectedItem, Mode=OneWay}" />
<controls:CodeSubstitution Key="ColumnResizeMode"
Value="{x:Bind columnResizeMode.SelectedItem, Mode=OneWay}" />
</controls:SamplePresenter.Substitutions>
</controls:SamplePresenter>
</Grid>
Expand Down
6 changes: 6 additions & 0 deletions src/Columns/TableViewColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ public bool CanResize
set => SetValue(CanResizeProperty, value);
}

/// <summary>
/// Gets or sets a value indicating whether the column is currently being resized by the user.
/// Used to skip expensive auto-width measurement while a manual pixel resize preview is active.
/// </summary>
internal bool IsResizing { get; set; }

/// <summary>
/// Gets or sets a value indicating whether the column is read-only.
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions src/TableView.Properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,11 @@ public partial class TableView
/// </summary>
public static readonly DependencyProperty ColumnAutoWidthModeProperty = DependencyProperty.Register(nameof(ColumnAutoWidthMode), typeof(TableViewColumnAutoWidthMode), typeof(TableView), new PropertyMetadata(TableViewColumnAutoWidthMode.Both, OnColumnAutoWidthModeChanged));

/// <summary>
/// Identifies the ColumnResizeMode dependency property.
/// </summary>
public static readonly DependencyProperty ColumnResizeModeProperty = DependencyProperty.Register(nameof(ColumnResizeMode), typeof(TableViewColumnResizeMode), typeof(TableView), new PropertyMetadata(TableViewColumnResizeMode.Live));

/// <summary>
/// Identifies the FrozenColumnCount dependency property.
/// </summary>
Expand Down Expand Up @@ -806,6 +811,16 @@ public TableViewColumnAutoWidthMode ColumnAutoWidthMode
set => SetValue(ColumnAutoWidthModeProperty, value);
}

/// <summary>
/// Gets or sets how a column behaves while the user drags to resize it — a real-time resize where
/// every visible row's cell relayouts every frame (default), or a fast composition-only preview.
/// </summary>
public TableViewColumnResizeMode ColumnResizeMode
{
get => (TableViewColumnResizeMode)GetValue(ColumnResizeModeProperty);
set => SetValue(ColumnResizeModeProperty, value);
}

/// <summary>
/// Gets or sets the number of columns that stays in view on horizontal scroll.
/// </summary>
Expand Down
209 changes: 208 additions & 1 deletion src/TableView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
Expand Down Expand Up @@ -49,6 +50,11 @@
private ItemIndexRange? _lastDragSelectionRowRange;
private bool _cellStateDispatchPending;
private readonly HashSet<int> _pendingCellStateRows = [];
private TableViewColumn? _resizingColumn;
private double _resizingOriginalWidth;
private readonly List<TableViewCell> _resizingPreviewCells = [];
private readonly List<TableViewCell> _resizingDownstreamCells = [];
private readonly List<(Panel Panel, TranslateTransform Shift)> _resizingScrollableShifts = [];

/// <summary>
/// Initializes a new instance of the TableView class.
Expand Down Expand Up @@ -204,6 +210,206 @@
return row;
}

/// <summary>
/// Gets a value indicating whether a column is currently being resized by the user via a live
/// drag preview (see <see cref="BeginColumnResizePreview"/>).
/// </summary>
internal bool IsColumnResizing { get; private set; }

/// <summary>
/// Starts a live resize-drag preview for <paramref name="column"/>: generously (re)measures the
/// column's currently-realized cells once, and creates the per-cell composition-only clip/shift
/// state that <see cref="UpdateColumnResizePreview"/> will mutate on every subsequent pointer-move
/// frame. No real layout (Width/ActualWidth) is touched here or during the drag — that only
/// happens once, in <see cref="EndColumnResizePreview"/>.
/// </summary>
internal void BeginColumnResizePreview(TableViewColumn column)
{
var visibleColumns = Columns.VisibleColumns;
var columnIndex = visibleColumns.IndexOf(column);

if (columnIndex < 0)
{
return;
}

_resizingColumn = column;
_resizingOriginalWidth = column.ActualWidth;
IsColumnResizing = true;
column.IsResizing = true;

var effectiveMax = column.MaxWidth ?? MaxColumnWidth;
if (double.IsPositiveInfinity(effectiveMax))
{
effectiveMax = 4000d;
}

_resizingPreviewCells.Clear();
_resizingDownstreamCells.Clear();
_resizingScrollableShifts.Clear();

foreach (var row in _rows)
{
foreach (var cell in row.Cells)
{
if (cell.Column is null || cell.Column.IsFrozen != column.IsFrozen)
{
continue;
}

if (cell.Column == column)
{
cell.BeginResizePreview(effectiveMax);
_resizingPreviewCells.Add(cell);
}
else if (visibleColumns.IndexOf(cell.Column) > columnIndex)
{
cell.ApplyDownstreamShift();
_resizingDownstreamCells.Add(cell);
}
}

if (column.IsFrozen && row.RowPresenter?.ScrollableCellsPanel is { } scrollablePanel)
{
var shift = new TranslateTransform();
scrollablePanel.RenderTransform = shift;
_resizingScrollableShifts.Add((scrollablePanel, shift));
}
}
}

/// <summary>
/// Updates the live resize-drag preview to <paramref name="liveWidth"/>. The only work done per
/// pointer-move frame: mutate each touched cell's own (not shared — see
/// <see cref="TableViewCell.BeginResizePreview"/>) clip/shift in place. No Measure/Arrange, just
/// cheap composition-only property writes, still nowhere near the cost of the real layout cascade
/// this preview mechanism replaces.
/// </summary>
internal void UpdateColumnResizePreview(double liveWidth)
{
if (_resizingColumn is null)
{
return;
}

var delta = liveWidth - _resizingOriginalWidth;

foreach (var cell in _resizingPreviewCells)
{
cell.UpdateResizePreviewClip(liveWidth, cell.ActualHeight);
cell.UpdateGridLineShift(delta);
}

foreach (var cell in _resizingDownstreamCells)
{
cell.UpdateDownstreamShift(delta);
}

foreach (var (_, shift) in _resizingScrollableShifts)
{
shift.X = delta;
}
}

/// <summary>
/// Ends the live resize-drag preview, synchronously: clears every clip/transform the preview
/// touched, then — if <paramref name="commitWidth"/> is not null — performs the single real width
/// commit (<see cref="TableViewColumn.ActualWidth"/> then <see cref="TableViewColumn.Width"/>),
/// cascading into a normal, one-time layout pass. Doing this all in one synchronous call (no
/// await/DispatcherQueue in between) means the compositor never presents an intermediate frame —
/// the preview's last shown state and the committed real state are numerically identical.
/// </summary>
internal void EndColumnResizePreview(double? commitWidth)
{
if (_resizingColumn is null)
{
return;
}

var column = _resizingColumn;
_resizingColumn = null;
IsColumnResizing = false;
column.IsResizing = false;

foreach (var cell in _resizingPreviewCells)
{
cell.EndResizePreview();
}

foreach (var cell in _resizingDownstreamCells)
{
cell.EndResizePreview();
}

foreach (var (panel, _) in _resizingScrollableShifts)
{
panel.RenderTransform = null;
}

_resizingPreviewCells.Clear();
_resizingDownstreamCells.Clear();
_resizingScrollableShifts.Clear();

if (commitWidth is double width)
{
column.ActualWidth = width;
column.Width = new GridLength(width, GridUnitType.Pixel);
}
}

/// <summary>
/// Starts a <see cref="TableViewColumnResizeMode.Live"/> resize drag for <paramref name="column"/>:
/// unlike <see cref="BeginColumnResizePreview"/>, no cell state is touched here — every frame's
/// width change goes through the normal, real <see cref="TableViewColumn.ActualWidth"/> cascade
/// instead (see <see cref="UpdateColumnResizeLive"/>).
/// </summary>
internal void BeginColumnResizeLive(TableViewColumn column)
{
_resizingColumn = column;
IsColumnResizing = true;
column.IsResizing = true;
}

/// <summary>
/// Updates a <see cref="TableViewColumnResizeMode.Live"/> resize drag to <paramref name="liveWidth"/>
/// by setting <see cref="TableViewColumn.ActualWidth"/> directly — every visible row's cell
/// relayouts for real on every call. Deliberately does not touch <see cref="TableViewColumn.Width"/>
/// (which would additionally re-run <c>CalculateHeaderWidths</c> for every column on every frame);
/// that commit happens once, in <see cref="EndColumnResizeLive"/>.
/// </summary>
internal void UpdateColumnResizeLive(double liveWidth)
{
if (_resizingColumn is null)
{
return;
}

_resizingColumn.ActualWidth = liveWidth;
}

/// <summary>
/// Ends a <see cref="TableViewColumnResizeMode.Live"/> resize drag. <see cref="TableViewColumn.ActualWidth"/>
/// already reflects the live width from <see cref="UpdateColumnResizeLive"/>, so only the
/// <see cref="TableViewColumn.Width"/> GridLength commit is left to do here.
/// </summary>
internal void EndColumnResizeLive(double? commitWidth)
{
if (_resizingColumn is null)
{
return;
}

var column = _resizingColumn;
_resizingColumn = null;
IsColumnResizing = false;
column.IsResizing = false;

if (commitWidth is double width)
{
column.Width = new GridLength(width, GridUnitType.Pixel);
}
}

/// <inheritdoc/>
protected override void OnKeyDown(KeyRoutedEventArgs e)
{
Expand Down Expand Up @@ -1187,7 +1393,7 @@
}
else
{
foreach (var propertyInfo in dataType.GetProperties())

Check warning on line 1396 in src/TableView.cs

View workflow job for this annotation

GitHub Actions / build-winui-sample (x64)

'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperties()'. The return value of method 'WinUI.TableView.Extensions.ObjectExtensions.GetItemType(IEnumerable)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to.
{
var displayAttribute = propertyInfo.GetCustomAttributes().OfType<DisplayAttribute>().FirstOrDefault();
var autoGenerateField = displayAttribute?.GetAutoGenerateField();
Expand Down Expand Up @@ -2604,7 +2810,8 @@
{
if (_scrollViewer is null) return;

var offset = CellsHorizontalOffset + Columns.VisibleColumns.Where(c => c.IsFrozen).Sum(c => c.ActualWidth);
var frozenColumns = Columns.VisibleColumns.Where(c => c.IsFrozen);
var offset = CellsHorizontalOffset + frozenColumns.Sum(c => c.ActualWidth);
AttachedPropertiesHelper.SetFrozenColumnScrollBarSpace(_scrollViewer, offset);
}
}
Loading
Loading