From 0aa0a092025df9377f31ce31bc197536cfe49792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 12 Aug 2025 11:28:53 +0200 Subject: [PATCH] Fix usage of GridSplitter inside ItemsControl as ItemsPanel (#19200) * Get parent Grid when its used in ItemsControl as ItemsPanel * Get properties values from ContentPresenter if its used in ItemsControl * Add unit tests * Move test types * Adjust GetParentGrid switch and expose helper methods for override --- src/Avalonia.Controls/GridSplitter.cs | 86 +++++++++- .../GridSplitterTests.cs | 162 ++++++++++++++++++ 2 files changed, 245 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index ee615a26f8..ba857687f6 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -7,6 +7,7 @@ using System; using System.Diagnostics; using Avalonia.Collections; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Presenters; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; @@ -211,7 +212,9 @@ namespace Avalonia.Controls private void InitializeData(bool showsPreview) { // If not in a grid or can't resize, do nothing. - if (Parent is Grid grid) + var grid = GetParentGrid(); + + if (grid != null) { GridResizeDirection resizeDirection = GetEffectiveResizeDirection(); @@ -244,13 +247,15 @@ namespace Avalonia.Controls /// private bool SetupDefinitionsToResize() { - int gridSpan = GetValue(_resizeData!.ResizeDirection == GridResizeDirection.Columns ? + // Get properties values from ContentPresenter if Grid it's used in ItemsControl as ItemsPanel otherwise directly from GridSplitter. + var sourceControl = GetPropertiesValueSource(); + int gridSpan = sourceControl.GetValue(_resizeData!.ResizeDirection == GridResizeDirection.Columns ? Grid.ColumnSpanProperty : Grid.RowSpanProperty); if (gridSpan == 1) { - var splitterIndex = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ? + var splitterIndex = sourceControl.GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ? Grid.ColumnProperty : Grid.RowProperty); @@ -351,6 +356,81 @@ namespace Avalonia.Controls } } + /// + /// Retrieves the that ultimately hosts this + /// in the visual/logical tree. + /// + /// + /// A splitter can be placed directly inside a or + /// indirectly inside an that uses a + /// as its . + /// In the latter case the first logical parent is usually an + /// (or the items control itself), + /// so the method walks these intermediate containers to locate the + /// underlying grid. + /// + /// + /// The containing if one is found; otherwise + /// null. + /// + protected virtual Grid? GetParentGrid() + { + // When GridSplitter is used inside an ItemsControl with Grid as + // its ItemsPanel, its immediate parent is usually a ItemsControl or ContentPresenter. + switch (Parent) + { + case Grid grid: + { + return grid; + } + case ItemsControl itemsControl: + { + if (itemsControl.ItemsPanelRoot is Grid grid) + { + return grid; + } + + break; + } + case ContentPresenter { Parent: ItemsControl presenterItemsControl }: + { + if (presenterItemsControl.ItemsPanelRoot is Grid grid) + { + return grid; + } + + break; + } + } + + return null; + } + + /// + /// Returns the element that carries the grid-attached properties + /// (, , etc.) relevant + /// to this . + /// + /// + /// When the splitter is generated as part of an + /// template, the attached properties are set on the surrounding + /// rather than on the splitter itself. + /// This helper selects that presenter when appropriate so subsequent + /// property look-ups read the correct values; otherwise it simply + /// returns this. + /// + /// + /// The from which grid-attached properties + /// should be read—either the parent or + /// the splitter instance. + /// + protected virtual StyledElement GetPropertiesValueSource() + { + return Parent is ContentPresenter + ? Parent + : this; + } + protected override void OnPointerEntered(PointerEventArgs e) { base.OnPointerEntered(e); diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index cdb8fb21fe..c0974ea97b 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -1,5 +1,8 @@ +using System.Collections.Generic; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Markup.Xaml; using Avalonia.Platform; using Avalonia.UnitTests; using Moq; @@ -380,5 +383,164 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star)); Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star)); } + + [Fact] + public void Works_In_ItemsControl_ItemsSource() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var xaml = @" + + + + + + + + + + + + + + + + + + + + + + + + + + + +"; + + var itemsControl = AvaloniaRuntimeXamlLoader.Parse(xaml); + itemsControl.ItemsSource = new List + { + new TextItem { Column = 0, Text = "A" }, + new SplitterItem { Column = 1 }, + new TextItem { Column = 2, Text = "B" }, + }; + + var root = new TestRoot { Child = itemsControl }; + root.Measure(new Size(200, 100)); + root.Arrange(new Rect(0, 0, 200, 100)); + + var panel = Assert.IsType(itemsControl.ItemsPanelRoot); + var cp = Assert.IsType(panel.Children[1]); + cp.UpdateChild(); + var splitter = Assert.IsType(cp.Child); + + splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragDeltaEvent, Vector = new Vector(-20, 0) }); + splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragCompletedEvent }); + + Assert.NotEqual(panel.ColumnDefinitions[0].Width, panel.ColumnDefinitions[2].Width); + } + + [Fact] + public void Works_In_ItemsControl_Items() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var xaml = @" + + + + + + + + + + + + + + + + + + + + + +"; + + var itemsControl = AvaloniaRuntimeXamlLoader.Parse(xaml); + var root = new TestRoot { Child = itemsControl }; + root.Measure(new Size(200, 100)); + root.Arrange(new Rect(0, 0, 200, 100)); + + var panel = Assert.IsType(itemsControl.ItemsPanelRoot); + var splitter = Assert.IsType(panel.Children[1]); + + splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragDeltaEvent, Vector = new Vector(-20, 0) }); + splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragCompletedEvent }); + + Assert.NotEqual(panel.ColumnDefinitions[0].Width, panel.ColumnDefinitions[2].Width); + } + + [Fact] + public void Works_In_Grid() + { + using var app = UnitTestApplication.Start(TestServices.StyledWindow); + + var xaml = @" + + + +"; + + var grid = AvaloniaRuntimeXamlLoader.Parse(xaml); + var root = new TestRoot { Child = grid }; + root.Measure(new Size(200, 100)); + root.Arrange(new Rect(0, 0, 200, 100)); + + var splitter = Assert.IsType(grid.Children[1]); + + splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent }); + splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragDeltaEvent, Vector = new Vector(-20, 0) }); + splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragCompletedEvent }); + + Assert.NotEqual(grid.ColumnDefinitions[0].Width, grid.ColumnDefinitions[2].Width); + } + } + + public interface IGridItem + { + int Column { get; set; } + } + + public class TextItem : IGridItem + { + public int Column { get; set; } + public string? Text { get; set; } + } + + public class SplitterItem : IGridItem + { + public int Column { get; set; } } }