Browse Source

Add Points collection observability support for Polygon and Polyline (#15030)

* Add Points binding observability support for Polygon and Polyline

* Add simple tests for Polygon and Polyline Points updating

* Revert "Add Points binding observability support for Polygon and Polyline"

This reverts commit e16d987945.

* Move Geometry.Changed handler to Shape class to make it available to all inheritors

* Fixes the event subscriptions

* Fix tests

* Add memory leak tests
pull/15190/head
SKProCH 2 years ago
committed by GitHub
parent
commit
45eb172ec3
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      src/Avalonia.Base/Media/PolylineGeometry.cs
  2. 51
      src/Avalonia.Controls/Shapes/Path.cs
  3. 2
      src/Avalonia.Controls/Shapes/Polygon.cs
  4. 3
      src/Avalonia.Controls/Shapes/Polyline.cs
  5. 44
      src/Avalonia.Controls/Shapes/Shape.cs
  6. 28
      tests/Avalonia.Controls.UnitTests/Shapes/PolygonTests.cs
  7. 28
      tests/Avalonia.Controls.UnitTests/Shapes/PolylineTests.cs
  8. 42
      tests/Avalonia.LeakTests/ControlTests.cs

8
src/Avalonia.Base/Media/PolylineGeometry.cs

@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Reactive;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -100,10 +102,8 @@ namespace Avalonia.Media
private void OnPointsChanged(IList<Point>? newValue) private void OnPointsChanged(IList<Point>? newValue)
{ {
_pointsObserver?.Dispose(); _pointsObserver?.Dispose();
_pointsObserver = (newValue as IAvaloniaList<Point>)?.ForEachItem( _pointsObserver = (newValue as INotifyCollectionChanged)?.GetWeakCollectionChangedObservable()
_ => InvalidateGeometry(), .Subscribe(_ => InvalidateGeometry());
_ => InvalidateGeometry(),
InvalidateGeometry);
} }
} }
} }

51
src/Avalonia.Controls/Shapes/Path.cs

@ -8,12 +8,9 @@ namespace Avalonia.Controls.Shapes
public static readonly StyledProperty<Geometry?> DataProperty = public static readonly StyledProperty<Geometry?> DataProperty =
AvaloniaProperty.Register<Path, Geometry?>(nameof(Data)); AvaloniaProperty.Register<Path, Geometry?>(nameof(Data));
private EventHandler? _geometryChangedHandler;
static Path() static Path()
{ {
AffectsGeometry<Path>(DataProperty); AffectsGeometry<Path>(DataProperty);
DataProperty.Changed.AddClassHandler<Path>((o, e) => o.DataChanged(e));
} }
public Geometry? Data public Geometry? Data
@ -22,54 +19,6 @@ namespace Avalonia.Controls.Shapes
set => SetValue(DataProperty, value); set => SetValue(DataProperty, value);
} }
private EventHandler GeometryChangedHandler => _geometryChangedHandler ??= GeometryChanged;
protected override Geometry? CreateDefiningGeometry() => Data; protected override Geometry? CreateDefiningGeometry() => Data;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (Data is object)
{
Data.Changed += GeometryChangedHandler;
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
if (Data is object)
{
Data.Changed -= GeometryChangedHandler;
}
}
private void DataChanged(AvaloniaPropertyChangedEventArgs e)
{
if (VisualRoot is null)
{
return;
}
var oldGeometry = (Geometry?)e.OldValue;
var newGeometry = (Geometry?)e.NewValue;
if (oldGeometry is object)
{
oldGeometry.Changed -= GeometryChangedHandler;
}
if (newGeometry is object)
{
newGeometry.Changed += GeometryChangedHandler;
}
}
private void GeometryChanged(object? sender, EventArgs e)
{
InvalidateGeometry();
}
} }
} }

2
src/Avalonia.Controls/Shapes/Polygon.cs

@ -27,7 +27,7 @@ namespace Avalonia.Controls.Shapes
protected override Geometry CreateDefiningGeometry() protected override Geometry CreateDefiningGeometry()
{ {
return new PolylineGeometry(Points, true); return new PolylineGeometry { Points = Points, IsFilled = true };
} }
} }
} }

3
src/Avalonia.Controls/Shapes/Polyline.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Data; using Avalonia.Data;
@ -28,7 +29,7 @@ namespace Avalonia.Controls.Shapes
protected override Geometry CreateDefiningGeometry() protected override Geometry CreateDefiningGeometry()
{ {
return new PolylineGeometry(Points, false); return new PolylineGeometry { Points = Points, IsFilled = false };
} }
} }
} }

44
src/Avalonia.Controls/Shapes/Shape.cs

@ -64,6 +64,7 @@ namespace Avalonia.Controls.Shapes
private Geometry? _definingGeometry; private Geometry? _definingGeometry;
private Geometry? _renderedGeometry; private Geometry? _renderedGeometry;
private IPen? _strokePen; private IPen? _strokePen;
private EventHandler? _geometryChangedHandler;
/// <summary> /// <summary>
/// Gets a value that represents the <see cref="Geometry"/> of the shape. /// Gets a value that represents the <see cref="Geometry"/> of the shape.
@ -75,6 +76,10 @@ namespace Avalonia.Controls.Shapes
if (_definingGeometry == null) if (_definingGeometry == null)
{ {
_definingGeometry = CreateDefiningGeometry(); _definingGeometry = CreateDefiningGeometry();
if (_definingGeometry is not null && VisualRoot is not null)
{
_definingGeometry.Changed += GeometryChangedHandler;
}
} }
return _definingGeometry; return _definingGeometry;
@ -186,6 +191,8 @@ namespace Avalonia.Controls.Shapes
get => GetValue(StrokeJoinProperty); get => GetValue(StrokeJoinProperty);
set => SetValue(StrokeJoinProperty, value); set => SetValue(StrokeJoinProperty, value);
} }
private EventHandler GeometryChangedHandler => _geometryChangedHandler ??= OnGeometryChanged;
public sealed override void Render(DrawingContext context) public sealed override void Render(DrawingContext context)
{ {
@ -225,12 +232,29 @@ namespace Avalonia.Controls.Shapes
/// </summary> /// </summary>
/// <returns>Defining <see cref="Geometry"/> of the shape.</returns> /// <returns>Defining <see cref="Geometry"/> of the shape.</returns>
protected abstract Geometry? CreateDefiningGeometry(); protected abstract Geometry? CreateDefiningGeometry();
/// <summary>
/// Called when the underlying <see cref="Geometry"/> changed
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected virtual void OnGeometryChanged(object? sender, EventArgs e)
{
_renderedGeometry = null;
InvalidateMeasure();
}
/// <summary> /// <summary>
/// Invalidates the geometry of this shape. /// Invalidates the geometry of this shape.
/// </summary> /// </summary>
protected void InvalidateGeometry() protected void InvalidateGeometry()
{ {
if (_definingGeometry is not null)
{
_definingGeometry.Changed -= GeometryChangedHandler;
}
_renderedGeometry = null; _renderedGeometry = null;
_definingGeometry = null; _definingGeometry = null;
@ -294,6 +318,26 @@ namespace Avalonia.Controls.Shapes
return default; return default;
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_definingGeometry is not null)
{
_definingGeometry.Changed += GeometryChangedHandler;
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
if (_definingGeometry is not null)
{
_definingGeometry.Changed -= GeometryChangedHandler;
}
}
internal static (Size size, Matrix transform) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch) internal static (Size size, Matrix transform) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch)
{ {

28
tests/Avalonia.Controls.UnitTests/Shapes/PolygonTests.cs

@ -0,0 +1,28 @@
using System.Collections.ObjectModel;
using Avalonia.Controls.Shapes;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests.Shapes;
public class PolygonTests
{
[Fact]
public void Polygon_Will_Update_Geometry_On_Shapes_Collection_Content_Change()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var points = new ObservableCollection<Point>();
var target = new Polygon() { Points = points };
target.Measure(new Size());
Assert.True(target.IsMeasureValid);
var root = new TestRoot(target);
points.Add(new Point());
Assert.False(target.IsMeasureValid);
root.Child = null;
}
}

28
tests/Avalonia.Controls.UnitTests/Shapes/PolylineTests.cs

@ -0,0 +1,28 @@
using System.Collections.ObjectModel;
using Avalonia.Controls.Shapes;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests.Shapes;
public class PolylineTests
{
[Fact]
public void Polyline_Will_Update_Geometry_On_Shapes_Collection_Content_Change()
{
using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
var points = new ObservableCollection<Point>();
var target = new Polyline { Points = points };
target.Measure(new Size());
Assert.True(target.IsMeasureValid);
var root = new TestRoot(target);
points.Add(new Point());
Assert.False(target.IsMeasureValid);
root.Child = null;
}
}

42
tests/Avalonia.LeakTests/ControlTests.cs

@ -713,6 +713,48 @@ namespace Avalonia.LeakTests
GC.KeepAlive(geometry); GC.KeepAlive(geometry);
} }
} }
[Fact]
public void Polyline_WithObservableCollectionPointsBinding_Is_Freed()
{
using (Start())
{
var observableCollection = new ObservableCollection<Point>(){new()};
Func<Window> run = () =>
{
var window = new Window
{
Content = new Polyline()
{
Points = observableCollection
}
};
window.Show();
window.LayoutManager.ExecuteInitialLayoutPass();
Assert.IsType<Polyline>(window.Presenter.Child);
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
};
var result = run();
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Polyline>()).ObjectsCount));
// We are keeping collection alive to simulate a resource that outlives the control.
GC.KeepAlive(observableCollection);
}
}
[Fact] [Fact]
public void ElementName_Binding_In_DataTemplate_Is_Freed() public void ElementName_Binding_In_DataTemplate_Is_Freed()

Loading…
Cancel
Save