Browse Source

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
pull/19460/head
Wiesław Šoltés 6 months ago
committed by GitHub
parent
commit
0aa0a09202
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 86
      src/Avalonia.Controls/GridSplitter.cs
  2. 162
      tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs

86
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
/// </summary>
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
}
}
/// <summary>
/// Retrieves the <see cref="Grid"/> that ultimately hosts this
/// <see cref="GridSplitter"/> in the visual/logical tree.
/// </summary>
/// <remarks>
/// A splitter can be placed directly inside a <see cref="Grid"/> or
/// indirectly inside an <see cref="ItemsControl"/> that uses a
/// <see cref="Grid"/> as its <see cref="ItemsControl.ItemsPanel"/>.
/// In the latter case the first logical parent is usually an
/// <see cref="ContentPresenter"/> (or the items control itself),
/// so the method walks these intermediate containers to locate the
/// underlying grid.
/// </remarks>
/// <returns>
/// The containing <see cref="Grid"/> if one is found; otherwise
/// <c>null</c>.
/// </returns>
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;
}
/// <summary>
/// Returns the element that carries the grid-attached properties
/// (<see cref="Grid.RowProperty"/>, <see cref="Grid.ColumnProperty"/>, etc.) relevant
/// to this <see cref="GridSplitter"/>.
/// </summary>
/// <remarks>
/// When the splitter is generated as part of an <see cref="ItemsControl"/>
/// template, the attached properties are set on the surrounding
/// <see cref="ContentPresenter"/> 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 <c>this</c>.
/// </remarks>
/// <returns>
/// The <see cref="StyledElement"/> from which grid-attached properties
/// should be read—either the parent <see cref="ContentPresenter"/> or
/// the splitter instance.
/// </returns>
protected virtual StyledElement GetPropertiesValueSource()
{
return Parent is ContentPresenter
? Parent
: this;
}
protected override void OnPointerEntered(PointerEventArgs e)
{
base.OnPointerEntered(e);

162
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 = @"<ItemsControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Controls.UnitTests'>
<ItemsControl.Resources>
<ControlTheme x:Key='{x:Type ItemsControl}' TargetType='ItemsControl'>
<Setter Property='Template'>
<ControlTemplate>
<Border Background='{TemplateBinding Background}'
BorderBrush='{TemplateBinding BorderBrush}'
BorderThickness='{TemplateBinding BorderThickness}'
CornerRadius='{TemplateBinding CornerRadius}'
Padding='{TemplateBinding Padding}'>
<ItemsPresenter Name='PART_ItemsPresenter'
ItemsPanel='{TemplateBinding ItemsPanel}'/>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
</ItemsControl.Resources>
<ItemsControl.Styles>
<Style Selector='ItemsControl > ContentPresenter'>
<Setter Property='(Grid.Column)' Value='{Binding Column}'/>
</Style>
</ItemsControl.Styles>
<ItemsControl.DataTemplates>
<DataTemplate DataType='local:TextItem'>
<Border><TextBlock Text='{Binding Text}'/></Border>
</DataTemplate>
<DataTemplate DataType='local:SplitterItem'>
<GridSplitter ResizeDirection='Columns'/>
</DataTemplate>
</ItemsControl.DataTemplates>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid ColumnDefinitions='*,10,*'/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>";
var itemsControl = AvaloniaRuntimeXamlLoader.Parse<ItemsControl>(xaml);
itemsControl.ItemsSource = new List<IGridItem>
{
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<Grid>(itemsControl.ItemsPanelRoot);
var cp = Assert.IsType<ContentPresenter>(panel.Children[1]);
cp.UpdateChild();
var splitter = Assert.IsType<GridSplitter>(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 = @"<ItemsControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ItemsControl.Resources>
<ControlTheme x:Key='{x:Type ItemsControl}' TargetType='ItemsControl'>
<Setter Property='Template'>
<ControlTemplate>
<Border Background='{TemplateBinding Background}'
BorderBrush='{TemplateBinding BorderBrush}'
BorderThickness='{TemplateBinding BorderThickness}'
CornerRadius='{TemplateBinding CornerRadius}'
Padding='{TemplateBinding Padding}'>
<ItemsPresenter Name='PART_ItemsPresenter'
ItemsPanel='{TemplateBinding ItemsPanel}'/>
</Border>
</ControlTemplate>
</Setter>
</ControlTheme>
</ItemsControl.Resources>
<ItemsControl.Items>
<Border Grid.Column='0'/>
<GridSplitter Grid.Column='1' ResizeDirection='Columns'/>
<Border Grid.Column='2'/>
</ItemsControl.Items>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid ColumnDefinitions='*,10,*'/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>";
var itemsControl = AvaloniaRuntimeXamlLoader.Parse<ItemsControl>(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<Grid>(itemsControl.ItemsPanelRoot);
var splitter = Assert.IsType<GridSplitter>(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 = @"<Grid xmlns='https://github.com/avaloniaui' ColumnDefinitions='*,10,*'>
<Border Grid.Column='0'/>
<GridSplitter Grid.Column='1' ResizeDirection='Columns'/>
<Border Grid.Column='2'/>
</Grid>";
var grid = AvaloniaRuntimeXamlLoader.Parse<Grid>(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<GridSplitter>(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; }
}
}

Loading…
Cancel
Save