Browse Source

Fix Loaded being incorrectly raised for unloaded controls (#18471)

pull/18315/head
Julien Lebosquain 11 months ago
committed by GitHub
parent
commit
17c3e42a1f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 41
      src/Avalonia.Controls/Control.cs
  2. 34
      tests/Avalonia.Controls.UnitTests/LoadedTests.cs

41
src/Avalonia.Controls/Control.cs

@ -103,7 +103,7 @@ namespace Avalonia.Controls
private static readonly HashSet<Control> _loadedQueue = new HashSet<Control>(); private static readonly HashSet<Control> _loadedQueue = new HashSet<Control>();
private static readonly HashSet<Control> _loadedProcessingQueue = new HashSet<Control>(); private static readonly HashSet<Control> _loadedProcessingQueue = new HashSet<Control>();
private bool _isLoaded = false; private LoadState _loadState = LoadState.Unloaded;
private DataTemplates? _dataTemplates; private DataTemplates? _dataTemplates;
private Control? _focusAdorner; private Control? _focusAdorner;
private AutomationPeer? _automationPeer; private AutomationPeer? _automationPeer;
@ -151,7 +151,7 @@ namespace Avalonia.Controls
/// <remarks> /// <remarks>
/// This is set to true while raising the <see cref="Loaded"/> event. /// This is set to true while raising the <see cref="Loaded"/> event.
/// </remarks> /// </remarks>
public bool IsLoaded => _isLoaded; public bool IsLoaded => _loadState == LoadState.Loaded;
/// <summary> /// <summary>
/// Gets or sets a user-defined object attached to the control. /// Gets or sets a user-defined object attached to the control.
@ -293,9 +293,10 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
internal void ScheduleOnLoadedCore() internal void ScheduleOnLoadedCore()
{ {
if (_isLoaded == false) if (_loadState == LoadState.Unloaded)
{ {
bool isAdded = _loadedQueue.Add(this); bool isAdded = _loadedQueue.Add(this);
_loadState = LoadState.LoadPending;
if (isAdded && if (isAdded &&
_isLoadedProcessing == false) _isLoadedProcessing == false)
@ -312,13 +313,18 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
internal void OnLoadedCore() internal void OnLoadedCore()
{ {
if (_isLoaded == false && if (_loadState == LoadState.LoadPending &&
((ILogical)this).IsAttachedToLogicalTree) ((ILogical)this).IsAttachedToLogicalTree)
{ {
_isLoaded = true; _loadState = LoadState.Loaded;
OnLoaded(new RoutedEventArgs(LoadedEvent, this)); OnLoaded(new RoutedEventArgs(LoadedEvent, this));
} }
else
{
// We somehow got here while being detached?
_loadState = LoadState.Unloaded;
}
} }
/// <summary> /// <summary>
@ -327,15 +333,21 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
internal void OnUnloadedCore() internal void OnUnloadedCore()
{ {
if (_isLoaded) switch (_loadState)
{ {
// Remove from the loaded event queue here as a failsafe in case the control case LoadState.Loaded:
// is detached before the dispatcher runs the Loaded jobs. _loadState = LoadState.Unloaded;
_loadedQueue.Remove(this);
OnUnloaded(new RoutedEventArgs(UnloadedEvent, this));
break;
_isLoaded = false; case LoadState.LoadPending:
// 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);
OnUnloaded(new RoutedEventArgs(UnloadedEvent, this)); _loadState = LoadState.Unloaded;
break;
} }
} }
@ -561,5 +573,12 @@ namespace Avalonia.Controls
_loadedProcessingQueue.Clear(); _loadedProcessingQueue.Clear();
_isLoadedProcessing = false; _isLoadedProcessing = false;
} }
private enum LoadState : byte
{
Unloaded,
Loaded,
LoadPending
}
} }
} }

34
tests/Avalonia.Controls.UnitTests/LoadedTests.cs

@ -72,4 +72,38 @@ public class LoadedTests
Assert.False(target.IsLoaded); Assert.False(target.IsLoaded);
} }
} }
[Fact]
public void Loaded_Should_Not_Be_Raised_If_Detached_From_Visual_Tree()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var loadedCount = 0;
var unloadedCount = 0;
var window = new Window();
window.Show();
var target = new Button();
target.Loaded += (_, _) => loadedCount++;
target.Unloaded += (_, _) => unloadedCount++;
Assert.Equal(0, loadedCount);
Assert.Equal(0, unloadedCount);
// Attach to, then immediately detach from the visual tree.
window.Content = target;
window.Content = null;
// Attach to another logical parent (this can actually happen outside tests with overlay popups)
((ISetLogicalParent) target).SetParent(new Window());
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
// At this point, the control shouldn't have been loaded at all.
Assert.Null(target.VisualParent);
Assert.False(target.IsLoaded);
Assert.Equal(0, loadedCount);
Assert.Equal(0, unloadedCount);
}
} }

Loading…
Cancel
Save