Skip to content

Feature Request: Double-click column separator to auto-fit column width #224

@HannahVernon

Description

@HannahVernon

Is your feature request related to a problem? Please describe.

The Avalonia DataGrid is missing a standard grid behavior: double-clicking the column header resize grip should auto-size the column to fit its content.

This is a well-established UX pattern present in WPF's DataGrid, WinForms' DataGridView, Excel, LibreOffice Calc, and virtually every desktop spreadsheet/grid control. Users coming from any of these environments instinctively double-click the column separator and expect auto-fit — currently nothing happens (or the adjacent column's sort toggles).

I examined DataGridColumnHeader.cs in AvaloniaUI/Avalonia.Controls.DataGrid (commit c0b02c0) and confirmed:

  • No DoubleTapped handler — the column header only handles PointerPressed, PointerMoved, and PointerReleased for drag-resize and column reordering.
  • No auto-fit logic — searched for DoubleTap, DoubleClick, AutoSize, AutoFit, and SizeToCells references — none found in the column header code.
  • No existing issue — searched both AvaloniaUI/Avalonia and AvaloniaUI/Avalonia.Controls.DataGrid issue trackers — no prior feature request found.

The resize grip is implemented via Thumb controls (PART_LeftGrip and PART_RightGrip) in the DataGridColumnHeader template. Thumb.DragDelta drives manual resize, but there is no gesture recognizer for double-tap on these thumbs.

Describe the solution you'd like

Double-clicking the resize grip (the Thumb between two column headers) should auto-size the column to the left of the grip to fit:

  1. The header text (including sort indicator)
  2. The widest visible cell content in that column
  3. Respecting MinWidth and MaxWidth constraints

After auto-fitting, the column should be set to a fixed pixel width (not left as Auto), so subsequent user drag-resizing behaves predictably.

Suggested implementation

The cleanest approach would be inside DataGridColumnHeader itself:

  1. Subscribe to DoubleTapped on the PART_RightGrip and PART_LeftGrip Thumb elements in OnApplyTemplate.

  2. In the handler, identify the target column:

    • Right grip → OwningColumn (already accessible internally)
    • Left grip → the column at OwningColumn.DisplayIndex - 1
  3. Auto-size the column:

    • Temporarily set column.Width = DataGridLength.Auto
    • After the next layout pass, capture column.ActualWidth
    • Set column.Width = new DataGridLength(actualWidth) to lock to pixels
    • Alternatively, measure cells directly for the target column
  4. Respect constraints: clamp to MinWidth / MaxWidth.

  5. Mark the event as handled to prevent the double-tap from also toggling sort.

This approach requires zero public API surface changes — it's purely a behavioral enhancement to the existing control template interaction.

WPF precedent

WPF's DataGridColumnHeader implements this in its OnMouseDoubleClick override with essentially the same approach described above.

Describe alternatives you've considered

For our project (SQL Server AG Monitor), we implemented an external workaround as a static helper that attaches to the DataGrid.DoubleTapped event:

internal static class DataGridAutoFitHelper
{
    private const double GripZonePixels = 8;

    public static void Attach(DataGrid dataGrid)
    {
        dataGrid.DoubleTapped -= OnDoubleTapped;
        dataGrid.DoubleTapped += OnDoubleTapped;
    }

    private static void OnDoubleTapped(object? sender, TappedEventArgs e)
    {
        if (sender is not DataGrid dataGrid) return;

        var source = e.Source as Visual;
        if (source == null) return;

        var header = source.FindAncestorOfType<DataGridColumnHeader>();
        if (header == null) return;

        // Match header to column via header text (OwningColumn is internal)
        var headerText = header.Content?.ToString();
        if (string.IsNullOrEmpty(headerText)) return;

        var matchedColumn = dataGrid.Columns
            .FirstOrDefault(c => c.Header?.ToString() == headerText);
        if (matchedColumn == null) return;

        var position = e.GetPosition(header);
        DataGridColumn? columnToFit = null;

        if (position.X >= header.Bounds.Width - GripZonePixels)
            columnToFit = matchedColumn;
        else if (position.X <= GripZonePixels)
        {
            var prevDisplayIndex = matchedColumn.DisplayIndex - 1;
            if (prevDisplayIndex >= 0)
                columnToFit = dataGrid.Columns
                    .FirstOrDefault(c => c.DisplayIndex == prevDisplayIndex);
        }

        if (columnToFit == null) return;

        columnToFit.Width = DataGridLength.Auto;
        Dispatcher.UIThread.Post(() =>
        {
            var measured = columnToFit.ActualWidth;
            var final = Math.Max(columnToFit.MinWidth, Math.Ceiling(measured));
            if (final > 10)
                columnToFit.Width = new DataGridLength(final);
        }, DispatcherPriority.Render);

        e.Handled = true;
    }
}

This works but has significant limitations compared to a native implementation:

Limitation Why it matters
Column matching by header text DataGridColumnHeader.OwningColumn is internal, forcing a string-based lookup that breaks with duplicate header names
Pixel-based grip zone estimation The 8px zone is a guess; the actual Thumb width depends on the theme/template and could vary
DoubleTapped may not fire on the grip Thumb If the Thumb captures pointer events, the gesture recognizer may not always produce a DoubleTapped event — behavior could be inconsistent across platforms
Layout timing Dispatcher.Post with Render priority is a best-effort workaround; a native implementation has direct access to the layout cycle

Additional context

  • Avalonia version tested: 11.3.9
  • DataGrid package: Avalonia.Controls.DataGrid 11.3.9
  • Platforms affected: All (Windows, macOS, Linux) — this is a missing feature, not a platform-specific bug
  • Source examined: DataGridColumnHeader.cs — confirmed no double-tap handling exists
  • We would be happy to submit a PR if the team is receptive to this enhancement.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions