Browse Source

Add Loaded/Unloaded Events (#8277)

* Add Loaded/Unloaded events

* Don't allow OnLoaded() twice unless OnUnloaded() is called

* Call OnLoadedCore within Render()

* Call OnLoadedCore() from OnAttachedToVisualTreeCore by scheduling it on the dispatcher

* Improve comments

* Queue loaded events

* Make the loaded queue static

* Make more members static per review

* Make sure control wasn't already scheduling for Loaded event

* Add locks around HashSet usage for when enumerating

* Remove from loaded queue in OnUnloadedCore() as failsafe

* Make Window raise its own Loaded/Unloaded events

* Attempt to fix leak tests to work with Loaded events

* Make WindowBase raise its own Loaded/Unloaded events

* Move hotkey leak tests to the LeakTest project

* Address some code review comments

* Attempt at actually queueing Loaded events again

* Fix typo

* Minor improvements

* Update controls benchmark

Co-authored-by: Max Katz <maxkatz6@outlook.com>
Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
pull/8391/head
robloo 4 years ago
committed by GitHub
parent
commit
017788cd8e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      src/Avalonia.Base/Visual.cs
  2. 192
      src/Avalonia.Controls/Control.cs
  3. 21
      src/Avalonia.Controls/WindowBase.cs
  4. 18
      tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs
  5. 110
      tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs
  6. 165
      tests/Avalonia.LeakTests/ControlTests.cs

2
src/Avalonia.Base/Visual.cs

@ -376,7 +376,9 @@ namespace Avalonia
if (e.OldValue is IAffectsRender oldValue)
{
if (sender._affectsRenderWeakSubscriber != null)
{
InvalidatedWeakEvent.Unsubscribe(oldValue, sender._affectsRenderWeakSubscriber);
}
}
if (e.NewValue is IAffectsRender newValue)

192
src/Avalonia.Controls/Control.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents;
@ -10,6 +11,7 @@ using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls
@ -53,21 +55,57 @@ namespace Avalonia.Controls
/// Event raised when an element wishes to be scrolled into view.
/// </summary>
public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble);
RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>(
"RequestBringIntoView",
RoutingStrategies.Bubble);
/// <summary>
/// Provides event data for the <see cref="ContextRequested"/> event.
/// </summary>
public static readonly RoutedEvent<ContextRequestedEventArgs> ContextRequestedEvent =
RoutedEvent.Register<Control, ContextRequestedEventArgs>(nameof(ContextRequested),
RoutedEvent.Register<Control, ContextRequestedEventArgs>(
nameof(ContextRequested),
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="Loaded"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> LoadedEvent =
RoutedEvent.Register<Control, RoutedEventArgs>(
nameof(Loaded),
RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="Unloaded"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> UnloadedEvent =
RoutedEvent.Register<Control, RoutedEventArgs>(
nameof(Unloaded),
RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
public static readonly AttachedProperty<FlowDirection> FlowDirectionProperty =
AvaloniaProperty.RegisterAttached<Control, Control, FlowDirection>(nameof(FlowDirection), inherits: true);
AvaloniaProperty.RegisterAttached<Control, Control, FlowDirection>(
nameof(FlowDirection),
inherits: true);
// Note the following:
// _loadedQueue :
// Is the queue where any control will be added to indicate that its loaded
// event should be scheduled and called later.
// _loadedProcessingQueue :
// Contains a copied snapshot of the _loadedQueue at the time when processing
// starts and individual events are being fired. This was needed to avoid
// exceptions if new controls were added in the Loaded event itself.
private static bool _isLoadedProcessing = false;
private static readonly HashSet<Control> _loadedQueue = new HashSet<Control>();
private static readonly HashSet<Control> _loadedProcessingQueue = new HashSet<Control>();
private bool _isAttachedToVisualTree = false;
private bool _isLoaded = false;
private DataTemplates? _dataTemplates;
private IControl? _focusAdorner;
private AutomationPeer? _automationPeer;
@ -108,6 +146,15 @@ namespace Avalonia.Controls
set => SetValue(ContextFlyoutProperty, value);
}
/// <summary>
/// Gets a value indicating whether the control is fully constructed in the visual tree
/// and both layout and render are complete.
/// </summary>
/// <remarks>
/// This is set to true while raising the <see cref="Loaded"/> event.
/// </remarks>
public bool IsLoaded => _isLoaded;
/// <summary>
/// Gets or sets a user-defined object attached to the control.
/// </summary>
@ -135,6 +182,35 @@ namespace Avalonia.Controls
remove => RemoveHandler(ContextRequestedEvent, value);
}
/// <summary>
/// Occurs when the control has been fully constructed in the visual tree and both
/// layout and render are complete.
/// </summary>
/// <remarks>
/// This event is guaranteed to occur after the control template is applied and references
/// to objects created after the template is applied are available. This makes it different
/// from OnAttachedToVisualTree which doesn't have these references. This event occurs at the
/// latest possible time in the control creation life-cycle.
/// </remarks>
public event EventHandler<RoutedEventArgs>? Loaded
{
add => AddHandler(LoadedEvent, value);
remove => RemoveHandler(LoadedEvent, value);
}
/// <summary>
/// Occurs when the control is removed from the visual tree.
/// </summary>
/// <remarks>
/// This is API symmetrical with <see cref="Loaded"/> and exists for compatibility with other
/// XAML frameworks; however, it behaves the same as OnDetachedFromVisualTree.
/// </remarks>
public event EventHandler<RoutedEventArgs>? Unloaded
{
add => AddHandler(UnloadedEvent, value);
remove => RemoveHandler(UnloadedEvent, value);
}
public new IControl? Parent => (IControl?)base.Parent;
/// <summary>
@ -215,18 +291,124 @@ namespace Avalonia.Controls
/// <returns>The control that receives the focus adorner.</returns>
protected virtual IControl? GetTemplateFocusTarget() => this;
private static Action loadedProcessingAction = () =>
{
// Copy the loaded queue for processing
// There was a possibility of the "Collection was modified; enumeration operation may not execute."
// exception when only a single hash set was used. This could happen when new controls are added
// within the Loaded callback/event itself. To fix this, two hash sets are used and while one is
// being processed the other accepts adding new controls to process next.
_loadedProcessingQueue.Clear();
foreach (Control control in _loadedQueue)
{
_loadedProcessingQueue.Add(control);
}
_loadedQueue.Clear();
foreach (Control control in _loadedProcessingQueue)
{
control.OnLoadedCore();
}
_loadedProcessingQueue.Clear();
_isLoadedProcessing = false;
// Restart if any controls were added to the queue while processing
if (_loadedQueue.Count > 0)
{
_isLoadedProcessing = true;
Dispatcher.UIThread.Post(loadedProcessingAction!, DispatcherPriority.Loaded);
}
};
/// <summary>
/// Schedules <see cref="OnLoadedCore"/> to be called for this control.
/// For performance, it will be queued with other controls.
/// </summary>
internal void ScheduleOnLoadedCore()
{
if (_isLoaded == false)
{
bool isAdded = _loadedQueue.Add(this);
if (isAdded &&
_isLoadedProcessing == false)
{
_isLoadedProcessing = true;
Dispatcher.UIThread.Post(loadedProcessingAction!, DispatcherPriority.Loaded);
}
}
}
/// <summary>
/// Invoked as the first step of marking the control as loaded and raising the
/// <see cref="Loaded"/> event.
/// </summary>
internal void OnLoadedCore()
{
if (_isLoaded == false &&
_isAttachedToVisualTree)
{
_isLoaded = true;
OnLoaded();
}
}
/// <summary>
/// Invoked as the first step of marking the control as unloaded and raising the
/// <see cref="Unloaded"/> event.
/// </summary>
internal void OnUnloadedCore()
{
if (_isLoaded)
{
// Remove from the loaded event queue here as a failsafe in case the control
// is detached before the dispatcher runs the Loaded jobs.
_loadedQueue.Remove(this);
_isLoaded = false;
OnUnloaded();
}
}
/// <summary>
/// Invoked just before the <see cref="Loaded"/> event.
/// </summary>
protected virtual void OnLoaded()
{
var eventArgs = new RoutedEventArgs(LoadedEvent);
eventArgs.Source = null;
RaiseEvent(eventArgs);
}
/// <summary>
/// Invoked just before the <see cref="Unloaded"/> event.
/// </summary>
protected virtual void OnUnloaded()
{
var eventArgs = new RoutedEventArgs(UnloadedEvent);
eventArgs.Source = null;
RaiseEvent(eventArgs);
}
/// <inheritdoc/>
protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTreeCore(e);
_isAttachedToVisualTree = true;
InitializeIfNeeded();
ScheduleOnLoadedCore();
}
/// <inheritdoc/>
protected sealed override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTreeCore(e);
_isAttachedToVisualTree = false;
OnUnloadedCore();
}
/// <inheritdoc/>
@ -324,7 +506,9 @@ namespace Avalonia.Controls
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>()?.OpenContextMenu;
if (keymap is null)
{
return;
}
var matches = false;

21
src/Avalonia.Controls/WindowBase.cs

@ -169,7 +169,6 @@ namespace Avalonia.Controls
}
}
[Obsolete("No longer used. Has no effect.")]
protected IDisposable BeginAutoSizing() => Disposable.Empty;
@ -186,6 +185,26 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override void OnClosed(EventArgs e)
{
// Window must manually raise Loaded/Unloaded events as it is a visual root and
// does not raise OnAttachedToVisualTreeCore/OnDetachedFromVisualTreeCore events
OnUnloadedCore();
base.OnClosed(e);
}
/// <inheritdoc/>
protected override void OnOpened(EventArgs e)
{
// Window must manually raise Loaded/Unloaded events as it is a visual root and
// does not raise OnAttachedToVisualTreeCore/OnDetachedFromVisualTreeCore events
ScheduleOnLoadedCore();
base.OnOpened(e);
}
protected override void HandleClosed()
{
_ignoreVisibilityChange = true;

18
tests/Avalonia.Benchmarks/Layout/ControlsBenchmark.cs

@ -1,6 +1,7 @@
using System;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Threading;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
@ -37,6 +38,21 @@ namespace Avalonia.Benchmarks.Layout
_root.Child = calendar;
_root.LayoutManager.ExecuteLayoutPass();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
}
[Benchmark]
[MethodImpl(MethodImplOptions.NoInlining)]
public void CreateCalendarWithLoaded()
{
using var subscription = Control.LoadedEvent.AddClassHandler<Control>((c, s) => { });
var calendar = new Calendar();
_root.Child = calendar;
_root.LayoutManager.ExecuteLayoutPass();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
}
[Benchmark]
@ -48,6 +64,7 @@ namespace Avalonia.Benchmarks.Layout
_root.Child = button;
_root.LayoutManager.ExecuteLayoutPass();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
}
[Benchmark]
@ -59,6 +76,7 @@ namespace Avalonia.Benchmarks.Layout
_root.Child = textBox;
_root.LayoutManager.ExecuteLayoutPass();
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
}
public void Dispose()

110
tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs

@ -12,6 +12,7 @@ using Moq;
using Xunit;
using Avalonia.Input.Raw;
using Factory = System.Func<int, System.Action<object>, Avalonia.Controls.Window, Avalonia.AvaloniaObject>;
using Avalonia.Threading;
namespace Avalonia.Controls.UnitTests.Utils
{
@ -60,115 +61,6 @@ namespace Avalonia.Controls.UnitTests.Utils
}
}
[Fact]
public void HotKeyManager_Should_Release_Reference_When_Control_Detached()
{
using (AvaloniaLocator.EnterScope())
{
var styler = new Mock<Styler>();
AvaloniaLocator.CurrentMutable
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformMock())
.Bind<IStyler>().ToConstant(styler.Object);
var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control);
WeakReference reference = null;
var tl = new Window();
new Action(() =>
{
var button = new Button();
reference = new WeakReference(button, true);
tl.Content = button;
tl.Template = CreateWindowTemplate();
tl.ApplyTemplate();
tl.Presenter.ApplyTemplate();
HotKeyManager.SetHotKey(button, gesture1);
// Detach the button from the logical tree, so there is no reference to it
tl.Content = null;
tl.ApplyTemplate();
})();
// The button should be collected since it's detached from the listbox
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.Null(reference?.Target);
}
}
[Fact]
public void HotKeyManager_Should_Release_Reference_When_Control_In_Item_Template_Detached()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var styler = new Mock<Styler>();
AvaloniaLocator.CurrentMutable
.Bind<IWindowingPlatform>().ToConstant(new WindowingPlatformMock())
.Bind<IStyler>().ToConstant(styler.Object);
var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control);
var weakReferences = new List<WeakReference>();
var tl = new Window { SizeToContent = SizeToContent.WidthAndHeight, IsVisible = true };
var lm = tl.LayoutManager;
var keyGestures = new AvaloniaList<KeyGesture> { gesture1 };
var listBox = new ListBox
{
Width = 100,
Height = 100,
VirtualizationMode = ItemVirtualizationMode.None,
// Create a button with binding to the KeyGesture in the template and add it to references list
ItemTemplate = new FuncDataTemplate(typeof(KeyGesture), (o, scope) =>
{
var keyGesture = o as KeyGesture;
var button = new Button
{
DataContext = keyGesture, [!Button.HotKeyProperty] = new Binding("")
};
weakReferences.Add(new WeakReference(button, true));
return button;
})
};
// Add the listbox and render it
tl.Content = listBox;
lm.ExecuteInitialLayoutPass();
listBox.Items = keyGestures;
lm.ExecuteLayoutPass();
// Let the button detach when clearing the source items
keyGestures.Clear();
lm.ExecuteLayoutPass();
// Add it again to double check,and render
keyGestures.Add(gesture1);
lm.ExecuteLayoutPass();
keyGestures.Clear();
lm.ExecuteLayoutPass();
// The button should be collected since it's detached from the listbox
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.True(weakReferences.Count > 0);
foreach (var weakReference in weakReferences)
{
Assert.Null(weakReference.Target);
}
}
}
[Theory]
[MemberData(nameof(ElementsFactory), parameters: true)]
public void HotKeyManager_Should_Use_CommandParameter(string factoryName, Factory factory)

165
tests/Avalonia.LeakTests/ControlTests.cs

@ -3,7 +3,10 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Data;
@ -67,6 +70,9 @@ namespace Avalonia.LeakTests
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<DataGrid>()).ObjectsCount));
}
@ -100,6 +106,9 @@ namespace Avalonia.LeakTests
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<Canvas>()).ObjectsCount));
}
@ -141,6 +150,9 @@ namespace Avalonia.LeakTests
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<Canvas>()).ObjectsCount));
}
@ -179,6 +191,9 @@ namespace Avalonia.LeakTests
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<TextBox>()).ObjectsCount));
dotMemory.Check(memory =>
@ -216,6 +231,9 @@ namespace Avalonia.LeakTests
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<TextBox>()).ObjectsCount));
}
@ -261,6 +279,9 @@ namespace Avalonia.LeakTests
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<TextBox>()).ObjectsCount));
dotMemory.Check(memory =>
@ -351,6 +372,9 @@ namespace Avalonia.LeakTests
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<TreeView>()).ObjectsCount));
}
@ -384,6 +408,9 @@ namespace Avalonia.LeakTests
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<Slider>()).ObjectsCount));
}
@ -421,6 +448,9 @@ namespace Avalonia.LeakTests
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<TabItem>()).ObjectsCount));
}
@ -496,6 +526,9 @@ namespace Avalonia.LeakTests
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<Canvas>()).ObjectsCount));
}
@ -536,9 +569,12 @@ namespace Avalonia.LeakTests
initialMenuCount = memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount;
initialMenuItemCount = memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount;
});
AttachShowAndDetachContextMenu(window);
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
Mock.Get(window.PlatformImpl).Invocations.Clear();
dotMemory.Check(memory =>
Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
@ -580,10 +616,13 @@ namespace Avalonia.LeakTests
initialMenuCount = memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount;
initialMenuItemCount = memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount;
});
BuildAndShowContextMenu(window);
BuildAndShowContextMenu(window);
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
Mock.Get(window.PlatformImpl).Invocations.Clear();
dotMemory.Check(memory =>
Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is<ContextMenu>()).ObjectsCount));
@ -623,6 +662,9 @@ namespace Avalonia.LeakTests
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<Path>()).ObjectsCount));
@ -657,6 +699,9 @@ namespace Avalonia.LeakTests
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<ItemsRepeater>()).ObjectsCount));
}
@ -725,14 +770,128 @@ namespace Avalonia.LeakTests
Assert.Empty(lb.ItemContainerGenerator.Containers);
// 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<Canvas>()).ObjectsCount));
}
}
[Fact]
public void HotKeyManager_Should_Release_Reference_When_Control_Detached()
{
using (Start())
{
Func<Window> run = () =>
{
var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control);
var tl = new Window
{
Content = new ItemsRepeater(),
};
tl.Show();
var button = new Button();
tl.Content = button;
tl.Template = CreateWindowTemplate();
tl.ApplyTemplate();
tl.Presenter.ApplyTemplate();
HotKeyManager.SetHotKey(button, gesture1);
// Detach the button from the logical tree, so there is no reference to it
tl.Content = null;
tl.ApplyTemplate();
return tl;
};
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<Button>()).ObjectsCount));
}
}
[Fact]
public void HotKeyManager_Should_Release_Reference_When_Control_In_Item_Template_Detached()
{
using (Start())
{
Func<Window> run = () =>
{
var gesture1 = new KeyGesture(Key.A, KeyModifiers.Control);
var tl = new Window { SizeToContent = SizeToContent.WidthAndHeight, IsVisible = true };
var lm = tl.LayoutManager;
tl.Show();
var keyGestures = new AvaloniaList<KeyGesture> { gesture1 };
var listBox = new ListBox
{
Width = 100,
Height = 100,
VirtualizationMode = ItemVirtualizationMode.None,
// Create a button with binding to the KeyGesture in the template and add it to references list
ItemTemplate = new FuncDataTemplate(typeof(KeyGesture), (o, scope) =>
{
var keyGesture = o as KeyGesture;
return new Button
{
DataContext = keyGesture,
[!Button.HotKeyProperty] = new Binding("")
};
})
};
// Add the listbox and render it
tl.Content = listBox;
lm.ExecuteInitialLayoutPass();
listBox.Items = keyGestures;
lm.ExecuteLayoutPass();
// Let the button detach when clearing the source items
keyGestures.Clear();
lm.ExecuteLayoutPass();
// Add it again to double check,and render
keyGestures.Add(gesture1);
lm.ExecuteLayoutPass();
keyGestures.Clear();
lm.ExecuteLayoutPass();
return tl;
};
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<Button>()).ObjectsCount));
}
}
private FuncControlTemplate CreateWindowTemplate()
{
return new FuncControlTemplate<Window>((parent, scope) =>
{
return new ContentPresenter
{
Name = "PART_ContentPresenter",
[~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty],
}.RegisterInNameScope(scope);
});
}
private IDisposable Start()
{
void Cleanup()
static void Cleanup()
{
// KeyboardDevice holds a reference to the focused item.
KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None);

Loading…
Cancel
Save