Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Points collection observability support for Polygon and Polyline #15030

Merged
merged 9 commits into from
Apr 1, 2024
8 changes: 4 additions & 4 deletions src/Avalonia.Base/Media/PolylineGeometry.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.Reactive;

namespace Avalonia.Media
{
Expand Down Expand Up @@ -100,10 +102,8 @@ public override Geometry Clone()
private void OnPointsChanged(IList<Point>? newValue)
{
_pointsObserver?.Dispose();
_pointsObserver = (newValue as IAvaloniaList<Point>)?.ForEachItem(
_ => InvalidateGeometry(),
_ => InvalidateGeometry(),
InvalidateGeometry);
_pointsObserver = (newValue as INotifyCollectionChanged)?.GetWeakCollectionChangedObservable()
.Subscribe(_ => InvalidateGeometry());
}
}
}
51 changes: 0 additions & 51 deletions src/Avalonia.Controls/Shapes/Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@ public class Path : Shape
public static readonly StyledProperty<Geometry?> DataProperty =
AvaloniaProperty.Register<Path, Geometry?>(nameof(Data));

private EventHandler? _geometryChangedHandler;

static Path()
{
AffectsGeometry<Path>(DataProperty);
DataProperty.Changed.AddClassHandler<Path>((o, e) => o.DataChanged(e));
}

public Geometry? Data
Expand All @@ -22,54 +19,6 @@ public Geometry? Data
set => SetValue(DataProperty, value);
}

private EventHandler GeometryChangedHandler => _geometryChangedHandler ??= GeometryChanged;

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 changes: 1 addition & 1 deletion src/Avalonia.Controls/Shapes/Polygon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public IList<Point> Points

protected override Geometry CreateDefiningGeometry()
{
return new PolylineGeometry(Points, true);
return new PolylineGeometry { Points = Points, IsFilled = true };
}
}
}
3 changes: 2 additions & 1 deletion src/Avalonia.Controls/Shapes/Polyline.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.Data;
Expand Down Expand Up @@ -28,7 +29,7 @@ public IList<Point> Points

protected override Geometry CreateDefiningGeometry()
{
return new PolylineGeometry(Points, false);
return new PolylineGeometry { Points = Points, IsFilled = false };
}
}
}
44 changes: 44 additions & 0 deletions src/Avalonia.Controls/Shapes/Shape.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public abstract class Shape : Control
private Geometry? _definingGeometry;
private Geometry? _renderedGeometry;
private IPen? _strokePen;
private EventHandler? _geometryChangedHandler;

/// <summary>
/// Gets a value that represents the <see cref="Geometry"/> of the shape.
Expand All @@ -75,6 +76,10 @@ public Geometry? DefiningGeometry
if (_definingGeometry == null)
{
_definingGeometry = CreateDefiningGeometry();
if (_definingGeometry is not null && VisualRoot is not null)
{
_definingGeometry.Changed += GeometryChangedHandler;
}
}

return _definingGeometry;
Expand Down Expand Up @@ -186,6 +191,8 @@ public PenLineJoin StrokeJoin
get => GetValue(StrokeJoinProperty);
set => SetValue(StrokeJoinProperty, value);
}

private EventHandler GeometryChangedHandler => _geometryChangedHandler ??= OnGeometryChanged;

public sealed override void Render(DrawingContext context)
{
Expand Down Expand Up @@ -225,12 +232,29 @@ protected static void AffectsGeometry<TShape>(params AvaloniaProperty[] properti
/// </summary>
/// <returns>Defining <see cref="Geometry"/> of the shape.</returns>
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>
/// Invalidates the geometry of this shape.
/// </summary>
protected void InvalidateGeometry()
{
if (_definingGeometry is not null)
{
_definingGeometry.Changed -= GeometryChangedHandler;
}

_renderedGeometry = null;
_definingGeometry = null;

Expand Down Expand Up @@ -294,6 +318,26 @@ protected override Size ArrangeOverride(Size finalSize)

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)
{
Expand Down
28 changes: 28 additions & 0 deletions tests/Avalonia.Controls.UnitTests/Shapes/PolygonTests.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 28 additions & 0 deletions tests/Avalonia.Controls.UnitTests/Shapes/PolylineTests.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 42 additions & 0 deletions tests/Avalonia.LeakTests/ControlTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,48 @@ public void Path_Is_Freed()
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]
public void ElementName_Binding_In_DataTemplate_Is_Freed()
Expand Down
Loading