Browse Source

Raise LayoutUpdated at the end of layout pass.

This attempts to match the behavior of `LayoutUpdated` in WPF and UWP.
pull/4080/head
Steven Kirk 6 years ago
parent
commit
d46265233d
  1. 2
      build/Moq.props
  2. 5
      src/Avalonia.Layout/ILayoutManager.cs
  3. 3
      src/Avalonia.Layout/LayoutManager.cs
  4. 54
      src/Avalonia.Layout/Layoutable.cs
  5. 18
      tests/Avalonia.Controls.UnitTests/GridTests.cs
  6. 121
      tests/Avalonia.Layout.UnitTests/LayoutableTests.cs

2
build/Moq.props

@ -1,5 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="Moq" Version="4.7.99" />
<PackageReference Include="Moq" Version="4.14.1" />
</ItemGroup>
</Project>

5
src/Avalonia.Layout/ILayoutManager.cs

@ -9,6 +9,11 @@ namespace Avalonia.Layout
/// </summary>
public interface ILayoutManager
{
/// <summary>
/// Raised when the layout manager completes a layout pass.
/// </summary>
event EventHandler LayoutUpdated;
/// <summary>
/// Notifies the layout manager that a control requires a measure.
/// </summary>

3
src/Avalonia.Layout/LayoutManager.cs

@ -23,6 +23,8 @@ namespace Avalonia.Layout
_executeLayoutPass = ExecuteLayoutPass;
}
public event EventHandler? LayoutUpdated;
/// <inheritdoc/>
public void InvalidateMeasure(ILayoutable control)
{
@ -126,6 +128,7 @@ namespace Avalonia.Layout
}
_queued = false;
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc/>

54
src/Avalonia.Layout/Layoutable.cs

@ -132,6 +132,7 @@ namespace Avalonia.Layout
private bool _measuring;
private Size? _previousMeasure;
private Rect? _previousArrange;
private EventHandler? _layoutUpdated;
/// <summary>
/// Initializes static members of the <see cref="Layoutable"/> class.
@ -154,7 +155,28 @@ namespace Avalonia.Layout
/// <summary>
/// Occurs when a layout pass completes for the control.
/// </summary>
public event EventHandler? LayoutUpdated;
public event EventHandler? LayoutUpdated
{
add
{
if (_layoutUpdated is null && VisualRoot is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated;
}
_layoutUpdated += value;
}
remove
{
_layoutUpdated -= value;
if (_layoutUpdated is null && VisualRoot is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated;
}
}
}
/// <summary>
/// Gets or sets the width of the element.
@ -359,12 +381,9 @@ namespace Avalonia.Layout
IsArrangeValid = true;
ArrangeCore(rect);
_previousArrange = rect;
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Called by InvalidateMeasure
/// </summary>
@ -694,6 +713,26 @@ namespace Avalonia.Layout
InvalidateMeasure();
}
protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTreeCore(e);
if (_layoutUpdated is object && e.Root is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated;
}
}
protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTreeCore(e);
if (_layoutUpdated is object && e.Root is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated;
}
}
/// <inheritdoc/>
protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{
@ -702,6 +741,13 @@ namespace Avalonia.Layout
base.OnVisualParentChanged(oldParent, newParent);
}
/// <summary>
/// Called when the layout manager raises a LayoutUpdated event.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void LayoutManagedLayoutUpdated(object sender, EventArgs e) => _layoutUpdated?.Invoke(this, e);
/// <summary>
/// Tests whether any of a <see cref="Rect"/>'s properties include negative values,
/// a NaN or Infinity.

18
tests/Avalonia.Controls.UnitTests/GridTests.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.UnitTests;
using Xunit;
using Xunit.Abstractions;
@ -1182,13 +1183,18 @@ namespace Avalonia.Controls.UnitTests
foreach (var xgrids in grids)
scope.Children.Add(xgrids);
var root = new Grid();
root.UseLayoutRounding = false;
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Children.Add(scope);
var rootGrid = new Grid();
rootGrid.UseLayoutRounding = false;
rootGrid.SetValue(Grid.IsSharedSizeScopeProperty, true);
rootGrid.Children.Add(scope);
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
var root = new TestRoot(rootGrid)
{
Width = 50,
Height = 50,
};
root.LayoutManager.ExecuteInitialLayoutPass(root);
PrintColumnDefinitions(grids[0]);
Assert.Equal(5, grids[0].ColumnDefinitions[0].ActualWidth);

121
tests/Avalonia.Layout.UnitTests/LayoutableTests.cs

@ -203,6 +203,127 @@ namespace Avalonia.Layout.UnitTests
Assert.Equal(new Rect(expectedX, 0, childWidth, 100), target.Bounds);
}
[Fact]
public void LayoutUpdated_Is_Called_At_End_Of_Layout_Pass()
{
Border border1;
Border border2;
var layoutManager = new LayoutManager();
var root = new TestRoot
{
Child = border1 = new Border
{
Child = border2 = new Border
{
}
},
LayoutManager = layoutManager,
};
var raised = 0;
void ValidateBounds(object sender, EventArgs e)
{
Assert.Equal(new Rect(0, 0, 100, 100), border1.Bounds);
Assert.Equal(new Rect(0, 0, 100, 100), border2.Bounds);
++raised;
}
root.LayoutUpdated += ValidateBounds;
border1.LayoutUpdated += ValidateBounds;
border2.LayoutUpdated += ValidateBounds;
root.Measure(new Size(100, 100));
root.Arrange(new Rect(0, 0, 100, 100));
layoutManager.ExecuteLayoutPass();
Assert.Equal(3, raised);
Assert.Equal(new Rect(0, 0, 100, 100), border1.Bounds);
Assert.Equal(new Rect(0, 0, 100, 100), border2.Bounds);
}
[Fact]
public void LayoutUpdated_Subscribes_To_LayoutManager()
{
Border target;
var layoutManager = new Mock<ILayoutManager>();
layoutManager.SetupAdd(m => m.LayoutUpdated += (sender, args) => { });
var root = new TestRoot
{
Child = new Border
{
Child = target = new Border(),
},
LayoutManager = layoutManager.Object,
};
void Handler(object sender, EventArgs e) {}
layoutManager.Invocations.Clear();
target.LayoutUpdated += Handler;
layoutManager.VerifyAdd(
x => x.LayoutUpdated += It.IsAny<EventHandler>(),
Times.Once);
layoutManager.Invocations.Clear();
target.LayoutUpdated -= Handler;
layoutManager.VerifyRemove(
x => x.LayoutUpdated -= It.IsAny<EventHandler>(),
Times.Once);
}
[Fact]
public void LayoutManager_LayoutUpdated_Is_Subscribed_When_Attached_To_Tree()
{
Border border1;
var layoutManager = new Mock<ILayoutManager>();
layoutManager.SetupAdd(m => m.LayoutUpdated += (sender, args) => { });
var root = new TestRoot
{
Child = border1 = new Border(),
LayoutManager = layoutManager.Object,
};
var border2 = new Border();
border2.LayoutUpdated += (s, e) => { };
layoutManager.Invocations.Clear();
border1.Child = border2;
layoutManager.VerifyAdd(
x => x.LayoutUpdated += It.IsAny<EventHandler>(),
Times.Once);
}
[Fact]
public void LayoutManager_LayoutUpdated_Is_Unsubscribed_When_Detached_From_Tree()
{
Border border1;
var layoutManager = new Mock<ILayoutManager>();
layoutManager.SetupAdd(m => m.LayoutUpdated += (sender, args) => { });
var root = new TestRoot
{
Child = border1 = new Border(),
LayoutManager = layoutManager.Object,
};
var border2 = new Border();
border2.LayoutUpdated += (s, e) => { };
border1.Child = border2;
layoutManager.Invocations.Clear();
border1.Child = null;
layoutManager.VerifyRemove(
x => x.LayoutUpdated -= It.IsAny<EventHandler>(),
Times.Once);
}
private class TestLayoutable : Layoutable
{
public Size ArrangeSize { get; private set; }

Loading…
Cancel
Save