Skip to content

Commit 20ed002

Browse files
MarchingCubedanwalmsley
authored andcommitted
Merge pull request AvaloniaUI#8189 from AvaloniaUI/fixes/8178-elementname-binding-leak
Make AvaloniaPropertyAccessorPlugin use weak events.
1 parent 9bd1e2a commit 20ed002

File tree

4 files changed

+132
-20
lines changed

4 files changed

+132
-20
lines changed

src/Avalonia.Base/Collections/Pooled/PooledList.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,7 @@ public void Clear()
587587
if (size > 0 && _clearOnFree)
588588
{
589589
// Clear the elements so that the gc can reclaim the references.
590-
Array.Clear(_items, 0, _size);
590+
Array.Clear(_items, 0, size);
591591
}
592592
}
593593

src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Runtime.ExceptionServices;
3+
using Avalonia.Utilities;
34

45
namespace Avalonia.Data.Core.Plugins
56
{
@@ -60,11 +61,10 @@ public bool Match(object obj, string propertyName)
6061
return AvaloniaPropertyRegistry.Instance.FindRegistered(o, propertyName);
6162
}
6263

63-
private class Accessor : PropertyAccessorBase, IObserver<object?>
64+
private class Accessor : PropertyAccessorBase, IWeakEventSubscriber<AvaloniaPropertyChangedEventArgs>
6465
{
6566
private readonly WeakReference<AvaloniaObject> _reference;
6667
private readonly AvaloniaProperty _property;
67-
private IDisposable? _subscription;
6868

6969
public Accessor(WeakReference<AvaloniaObject> reference, AvaloniaProperty property)
7070
{
@@ -95,29 +95,45 @@ public override bool SetValue(object? value, BindingPriority priority)
9595
return false;
9696
}
9797

98-
protected override void SubscribeCore()
98+
void IWeakEventSubscriber<AvaloniaPropertyChangedEventArgs>.
99+
OnEvent(object? notifyPropertyChanged, WeakEvent ev, AvaloniaPropertyChangedEventArgs e)
99100
{
100-
_subscription = Instance?.GetObservable(_property).Subscribe(this);
101+
if (e.Property == _property)
102+
{
103+
SendCurrentValue();
104+
}
101105
}
102106

103-
protected override void UnsubscribeCore()
107+
protected override void SubscribeCore()
104108
{
105-
_subscription?.Dispose();
106-
_subscription = null;
109+
SubscribeToChanges();
110+
SendCurrentValue();
107111
}
108112

109-
void IObserver<object?>.OnCompleted()
113+
protected override void UnsubscribeCore()
110114
{
115+
var instance = Instance;
116+
117+
if (instance != null)
118+
WeakEvents.AvaloniaPropertyChanged.Unsubscribe(instance, this);
111119
}
112120

113-
void IObserver<object?>.OnError(Exception error)
121+
private void SendCurrentValue()
114122
{
115-
ExceptionDispatchInfo.Capture(error).Throw();
123+
try
124+
{
125+
var value = Value;
126+
PublishValue(value);
127+
}
128+
catch { }
116129
}
117130

118-
void IObserver<object?>.OnNext(object? value)
131+
private void SubscribeToChanges()
119132
{
120-
PublishValue(value);
133+
var instance = Instance;
134+
135+
if (instance != null)
136+
WeakEvents.AvaloniaPropertyChanged.Subscribe(instance, this);
121137
}
122138
}
123139
}

src/Avalonia.Base/Utilities/WeakEvents.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,23 @@ public static readonly WeakEvent<INotifyPropertyChanged, PropertyChangedEventArg
3131
return () => s.PropertyChanged -= handler;
3232
});
3333

34+
35+
/// <summary>
36+
/// Represents PropertyChanged event from <see cref="AvaloniaObject"/>
37+
/// </summary>
38+
public static readonly WeakEvent<AvaloniaObject, AvaloniaPropertyChangedEventArgs>
39+
AvaloniaPropertyChanged = WeakEvent.Register<AvaloniaObject, AvaloniaPropertyChangedEventArgs>(
40+
(s, h) =>
41+
{
42+
EventHandler<AvaloniaPropertyChangedEventArgs> handler = (_, e) => h(s, e);
43+
s.PropertyChanged += handler;
44+
return () => s.PropertyChanged -= handler;
45+
});
46+
3447
/// <summary>
3548
/// Represents CanExecuteChanged event from <see cref="ICommand"/>
3649
/// </summary>
3750
public static readonly WeakEvent<ICommand, EventArgs> CommandCanExecuteChanged =
3851
WeakEvent.Register<ICommand>((s, h) => s.CanExecuteChanged += h,
3952
(s, h) => s.CanExecuteChanged -= h);
40-
}
53+
}

tests/Avalonia.LeakTests/ControlTests.cs

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Runtime.Remoting.Contexts;
4+
using System.Reactive.Disposables;
55
using Avalonia.Controls;
66
using Avalonia.Controls.Shapes;
77
using Avalonia.Controls.Templates;
8+
using Avalonia.Data;
89
using Avalonia.Diagnostics;
910
using Avalonia.Input;
10-
using Avalonia.Layout;
1111
using Avalonia.Media;
1212
using Avalonia.Platform;
1313
using Avalonia.Rendering;
1414
using Avalonia.Styling;
15+
using Avalonia.Threading;
1516
using Avalonia.UnitTests;
1617
using Avalonia.VisualTree;
1718
using JetBrains.dotMemoryUnit;
@@ -619,14 +620,96 @@ public void ItemsRepeater_Is_Freed()
619620
}
620621
}
621622

623+
[Fact]
624+
public void ElementName_Binding_In_DataTemplate_Is_Freed()
625+
{
626+
using (Start())
627+
{
628+
var items = new ObservableCollection<int>(Enumerable.Range(0, 10));
629+
NameScope ns;
630+
TextBox tb;
631+
ListBox lb;
632+
var window = new Window
633+
{
634+
[NameScope.NameScopeProperty] = ns = new NameScope(),
635+
Width = 100,
636+
Height = 100,
637+
Content = new StackPanel
638+
{
639+
Children =
640+
{
641+
(tb = new TextBox
642+
{
643+
Name = "tb",
644+
Text = "foo",
645+
}),
646+
(lb = new ListBox
647+
{
648+
Items = items,
649+
ItemTemplate = new FuncDataTemplate<int>((_, _) =>
650+
new Canvas
651+
{
652+
Width = 10,
653+
Height = 10,
654+
[!Control.TagProperty] = new Binding
655+
{
656+
ElementName = "tb",
657+
Path = "Text",
658+
NameScope = new WeakReference<INameScope>(ns),
659+
}
660+
})
661+
}),
662+
}
663+
}
664+
};
665+
666+
tb.RegisterInNameScope(ns);
667+
668+
window.Show();
669+
window.LayoutManager.ExecuteInitialLayoutPass();
670+
671+
void AssertInitialItemState()
672+
{
673+
var item0 = (ListBoxItem)lb.ItemContainerGenerator.Containers.First().ContainerControl;
674+
var canvas0 = (Canvas)item0.Presenter.Child;
675+
Assert.Equal("foo", canvas0.Tag);
676+
}
677+
678+
Assert.Equal(10, lb.ItemContainerGenerator.Containers.Count());
679+
AssertInitialItemState();
680+
681+
items.Clear();
682+
window.LayoutManager.ExecuteLayoutPass();
683+
684+
Assert.Empty(lb.ItemContainerGenerator.Containers);
685+
686+
dotMemory.Check(memory =>
687+
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
688+
}
689+
}
690+
622691
private IDisposable Start()
623692
{
624-
return UnitTestApplication.Start(TestServices.StyledWindow.With(
625-
focusManager: new FocusManager(),
626-
keyboardDevice: () => new KeyboardDevice(),
627-
inputManager: new InputManager()));
693+
void Cleanup()
694+
{
695+
// KeyboardDevice holds a reference to the focused item.
696+
KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);
697+
698+
// Empty the dispatcher queue.
699+
Dispatcher.UIThread.RunJobs();
700+
}
701+
702+
return new CompositeDisposable
703+
{
704+
Disposable.Create(Cleanup),
705+
UnitTestApplication.Start(TestServices.StyledWindow.With(
706+
focusManager: new FocusManager(),
707+
keyboardDevice: () => new KeyboardDevice(),
708+
inputManager: new InputManager()))
709+
};
628710
}
629711

712+
630713
private class Node
631714
{
632715
public string Name { get; set; }

0 commit comments

Comments
 (0)