Bugfix(MemoryLeak): fix IconPresenter memory leak

This commit is contained in:
chinboy
2026-04-09 18:31:36 +08:00
parent b22671b6b9
commit 5e3c3e5efa
12 changed files with 270 additions and 145 deletions
@@ -141,6 +141,7 @@ public partial class FormShowCase : ReactiveUserControl<FormViewModel>
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_messageManager?.Dispose();
_messageManager = null;
}
@@ -192,14 +192,18 @@ public partial class UploadShowCase : ReactiveUserControl<UploadViewModel>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_messageManager == null)
var topLevel = TopLevel.GetTopLevel(this);
_messageManager = new WindowMessageManager(topLevel)
{
var topLevel = TopLevel.GetTopLevel(this);
_messageManager = new WindowMessageManager(topLevel)
{
MaxItems = 10
};
}
MaxItems = 10
};
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_messageManager?.Dispose();
_messageManager = null;
}
private void HandleAboutToScheduling(object? sender, UploadTaskAboutToSchedulingEventArgs e)
@@ -28,6 +28,13 @@ public partial class MessageShowCase : ReactiveUserControl<MessageViewModel>
};
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_messageManager?.Dispose();
_messageManager = null;
}
private void ShowSimpleMessage(object? sender, RoutedEventArgs e)
{
_messageManager?.Show(new Message(
@@ -98,6 +98,18 @@ public partial class NotificationShowCase : ReactiveUserControl<NotificationView
};
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_basicManager?.Dispose();
_topLeftManager?.Dispose();
_topManager?.Dispose();
_topRightManager?.Dispose();
_bottomLeftManager?.Dispose();
_bottomManager?.Dispose();
_bottomRightManager?.Dispose();
}
private void ShowSimpleNotification(object? sender, RoutedEventArgs e)
{
_basicManager?.Show(new Notification(
@@ -62,6 +62,13 @@ public partial class BreadcrumbShowCase : ReactiveUserControl<BreadcrumbViewMode
};
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_messageManager?.Dispose();
_messageManager = null;
}
private void HandleNavigateRequest(object? sender, BreadcrumbNavigateEventArgs eventArgs)
{
_messageManager?.Show(new Message(
+21 -36
View File
@@ -13,9 +13,6 @@ using IconControl = Icon;
public class IconPresenter : Control, IMotionAwareControl
{
#region 公共属性定义
public static readonly StyledProperty<IconTemplate?> IconTemplateProperty =
AvaloniaProperty.Register<IconPresenter, IconTemplate?>(nameof(IconTemplate));
public static readonly StyledProperty<PathIcon?> IconProperty =
AvaloniaProperty.Register<IconPresenter, PathIcon?>(nameof(Icon));
@@ -25,12 +22,6 @@ public class IconPresenter : Control, IMotionAwareControl
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<IconPresenter>();
public IconTemplate? IconTemplate
{
get => GetValue(IconTemplateProperty);
set => SetValue(IconTemplateProperty, value);
}
[Content]
public PathIcon? Icon
{
@@ -56,10 +47,9 @@ public class IconPresenter : Control, IMotionAwareControl
static IconPresenter()
{
AffectsMeasure<IconPresenter>(IconProperty, IconTemplateProperty);
AffectsMeasure<IconPresenter>(IconProperty);
AffectsRender<IconPresenter>(IconBrushProperty);
IconProperty.Changed.AddClassHandler<IconPresenter>((x, e) => x.HandleIconChanged(e));
IconTemplateProperty.Changed.AddClassHandler<IconPresenter>((x, e) => x.HandleIconTemplateChanged(e));
}
private void HandleIconChanged(AvaloniaPropertyChangedEventArgs change)
@@ -77,34 +67,33 @@ public class IconPresenter : Control, IMotionAwareControl
if (newChild is PathIcon pathIcon)
{
ConfigureIcon(pathIcon);
AddIcon(pathIcon);
}
}
private void HandleIconTemplateChanged(AvaloniaPropertyChangedEventArgs change)
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
var oldIconTemplate = (IconTemplate?)change.OldValue;
var newIconTemplate = (IconTemplate?)change.NewValue;
if (oldIconTemplate != null)
base.OnAttachedToVisualTree(e);
if (Icon != null)
{
_disposables?.Dispose();
_disposables = null;
if (Icon != null)
{
((ISetLogicalParent)Icon).SetParent(null);
}
LogicalChildren.Clear();
VisualChildren.Clear();
ConfigureIcon(Icon);
}
}
if (newIconTemplate != null)
{
var pathIcon = newIconTemplate.Build();
if (pathIcon != null)
{
ConfigureIcon(pathIcon);
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_disposables?.Dispose();
_disposables = null;
}
private void AddIcon(PathIcon pathIcon)
{
ConfigureIcon(pathIcon);
((ISetLogicalParent)pathIcon).SetParent(null);
pathIcon.SetVisualParent(null);
VisualChildren.Add(pathIcon);
LogicalChildren.Add(pathIcon);
}
private void ConfigureIcon(PathIcon pathIcon)
@@ -123,9 +112,5 @@ public class IconPresenter : Control, IMotionAwareControl
{
_disposables.Add(BindUtils.RelayBind(this, IconBrushProperty, pathIcon, PathIcon.ForegroundProperty, BindingMode.Default, BindingPriority.Template));
}
((ISetLogicalParent)pathIcon).SetParent(null);
pathIcon.SetVisualParent(null);
VisualChildren.Add(pathIcon);
LogicalChildren.Add(pathIcon);
}
}
@@ -0,0 +1,78 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Metadata;
namespace AtomUI.Controls;
public class IconTemplatePresenter : Control, IMotionAwareControl
{
#region 公共属性定义
public static readonly StyledProperty<IconTemplate?> IconTemplateProperty =
AvaloniaProperty.Register<IconTemplatePresenter, IconTemplate?>(nameof(IconTemplate));
public static readonly StyledProperty<IBrush?> IconBrushProperty =
AvaloniaProperty.Register<IconTemplatePresenter, IBrush?>(nameof(IconBrush));
public static readonly StyledProperty<bool> IsMotionEnabledProperty =
MotionAwareControlProperty.IsMotionEnabledProperty.AddOwner<IconTemplatePresenter>();
[Content]
public IconTemplate? IconTemplate
{
get => GetValue(IconTemplateProperty);
set => SetValue(IconTemplateProperty, value);
}
public IBrush? IconBrush
{
get => GetValue(IconBrushProperty);
set => SetValue(IconBrushProperty, value);
}
public bool IsMotionEnabled
{
get => GetValue(IsMotionEnabledProperty);
set => SetValue(IsMotionEnabledProperty, value);
}
#endregion
static IconTemplatePresenter()
{
AffectsMeasure<IconTemplatePresenter>( IconTemplateProperty);
AffectsRender<IconTemplatePresenter>(IconBrushProperty);
IconTemplateProperty.Changed.AddClassHandler<IconTemplatePresenter>((x, e) => x.HandleIconTemplateChanged(e));
}
private void HandleIconTemplateChanged(AvaloniaPropertyChangedEventArgs change)
{
var oldIconTemplate = (IconTemplate?)change.OldValue;
var newIconTemplate = (IconTemplate?)change.NewValue;
if (oldIconTemplate != null)
{
LogicalChildren.Clear();
VisualChildren.Clear();
}
if (newIconTemplate != null)
{
var pathIcon = newIconTemplate.Build();
if (pathIcon != null)
{
ConfigureIcon(pathIcon);
}
}
}
private void ConfigureIcon(PathIcon pathIcon)
{
pathIcon[!WidthProperty] = this[!WidthProperty];
pathIcon[!HeightProperty] = this[!HeightProperty];
pathIcon[!IsMotionEnabledProperty] = this[!IsMotionEnabledProperty];
pathIcon[!Icon.StrokeBrushProperty] = this[!IconBrushProperty];
pathIcon[!Icon.FillBrushProperty] = this[!IconBrushProperty];
VisualChildren.Add(pathIcon);
LogicalChildren.Add(pathIcon);
}
}
@@ -47,7 +47,7 @@
VerticalContentAlignment="Center"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}" />
<atom:IconPresenter Name="ExpandIconPresenter" Grid.Column="3"
<atom:IconTemplatePresenter Name="ExpandIconPresenter" Grid.Column="3"
IconTemplate="{TemplateBinding ExpandIcon}"
IsMotionEnabled="{TemplateBinding IsMotionEnabled}">
<atom:IconPresenter.IsVisible>
@@ -58,10 +58,10 @@
Path="IsLoading" />
</MultiBinding>
</atom:IconPresenter.IsVisible>
</atom:IconPresenter>
<atom:IconPresenter Name="LoadingIconPresenter" Grid.Column="3"
IsVisible="{TemplateBinding IsLoading}"
IconTemplate="{TemplateBinding LoadingIcon}" />
</atom:IconTemplatePresenter>
<atom:IconTemplatePresenter Name="LoadingIconPresenter" Grid.Column="3"
IsVisible="{TemplateBinding IsLoading}"
IconTemplate="{TemplateBinding LoadingIcon}" />
</Grid>
</Border>
</ControlTemplate>
@@ -11,7 +11,7 @@
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<DockPanel LastChildFill="True">
<atom:IconPresenter
<atom:IconTemplatePresenter
Name="SelectedIndicator"
IconTemplate="{TemplateBinding SelectedIndicator}"
IsVisible="{TemplateBinding IsSelectedIndicatorVisible}"
@@ -11,7 +11,7 @@
CornerRadius="{TemplateBinding CornerRadius}"
Padding="{TemplateBinding Padding}">
<DockPanel LastChildFill="True">
<atom:IconPresenter
<atom:IconTemplatePresenter
Name="SelectedIndicator"
IconTemplate="{TemplateBinding SelectedIndicator}"
IsVisible="{TemplateBinding IsSelectedIndicatorVisible}"
@@ -5,7 +5,6 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Threading;
using Avalonia.VisualTree;
@@ -101,8 +100,11 @@ public class WindowMessageManager : TemplatedControl, IMessageManager, IMotionAw
/// <param name="classes">style classes to apply</param>
public void Show(IMessage message, string[]? classes = null)
{
var expiration = message.Expiration;
var onClose = message.OnClose;
if (_isDisposed || _items is null)
{
return;
}
Dispatcher.UIThread.VerifyAccess();
var messageControl = new MessageCard
@@ -114,7 +116,7 @@ public class WindowMessageManager : TemplatedControl, IMessageManager, IMotionAw
messageControl[!MessageCard.IsMotionEnabledProperty] = this[!IsMotionEnabledProperty];
// Add style classes if any
if (classes != null)
if (classes?.Length > 0)
{
foreach (var cls in classes)
{
@@ -122,28 +124,40 @@ public class WindowMessageManager : TemplatedControl, IMessageManager, IMotionAw
}
}
messageControl.MessageClosed += (sender, args) =>
messageControl.MessageClosed += (sender, _) =>
{
onClose?.Invoke();
_items?.Remove(sender);
message.OnClose?.Invoke();
_items.Remove(sender);
};
Dispatcher.UIThread.Post(() =>
{
_items?.Add(messageControl);
if (_items?.OfType<MessageCard>().Count(i => !i.IsClosing) > MaxItems)
{
_items.OfType<MessageCard>().First(i => !i.IsClosing).Close();
}
_items.Add(messageControl);
RemoveExcessMessages();
});
if (expiration == TimeSpan.Zero)
// Auto-close after expiration time
if (message.Expiration != TimeSpan.Zero)
{
return;
DispatcherTimer.RunOnce(messageControl.Close, message.Expiration);
}
}
DispatcherTimer.RunOnce(messageControl.Close, expiration);
/// <summary>
/// Removes excess messages when the count exceeds MaxItems.
/// </summary>
private void RemoveExcessMessages()
{
var visibleMessages = _items!.OfType<MessageCard>().Where(m => !m.IsClosing).ToList();
var excessCount = visibleMessages.Count - MaxItems;
if (excessCount > 0)
{
for (int i = 0; i < excessCount; i++)
{
visibleMessages[i].Close();
}
}
}
private void InstallFromTopLevel(TopLevel topLevel)
@@ -157,7 +171,7 @@ public class WindowMessageManager : TemplatedControl, IMessageManager, IMotionAw
}
}
private void UninstallFromTopLevel()
public void Dispose()
{
if (_topLevel is null || _isDisposed)
{
@@ -168,7 +182,7 @@ public class WindowMessageManager : TemplatedControl, IMessageManager, IMotionAw
{
// 卸载事件订阅
_topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
_items?.Clear();
// 从 AdornerLayer 中移除
if (_adornerLayer is not null)
{
@@ -176,7 +190,6 @@ public class WindowMessageManager : TemplatedControl, IMessageManager, IMotionAw
AdornerLayer.SetAdornedElement(this, null);
_adornerLayer = null;
}
_topLevel = null;
_items = null;
_isDisposed = true;
@@ -187,41 +200,23 @@ public class WindowMessageManager : TemplatedControl, IMessageManager, IMotionAw
}
}
private void TopLevelOnTemplateApplied(object? sender, TemplateAppliedEventArgs e)
private void RemoveFromAdornerLayer()
{
if (_adornerLayer is not null)
{
_adornerLayer.Children.Remove(this);
AdornerLayer.SetAdornedElement(this, null);
_adornerLayer = null;
}
_adornerLayer = null;
}
private void TopLevelOnTemplateApplied(object? sender, TemplateAppliedEventArgs _)
{
RemoveFromAdornerLayer();
// Reinstall notification manager on template reapplied.
var topLevel = (TopLevel)sender!;
topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
InstallFromTopLevel(topLevel);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
UninstallFromTopLevel();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_topLevel is not null && !_isDisposed)
{
_topLevel.TemplateApplied += TopLevelOnTemplateApplied;
}
}
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
UninstallFromTopLevel();
}
public void Dispose() => UninstallFromTopLevel();
}
@@ -1,6 +1,5 @@
using System.Collections;
using AtomUI.Controls;
using AtomUI.Data;
using AtomUI.Theme;
using Avalonia;
using Avalonia.Controls;
@@ -18,14 +17,8 @@ namespace AtomUI.Desktop.Controls;
NotificationPseudoClass.BottomRight,
NotificationPseudoClass.TopCenter,
NotificationPseudoClass.BottomCenter)]
public class WindowNotificationManager : TemplatedControl, INotificationManager, IMotionAwareControl
public class WindowNotificationManager : TemplatedControl, INotificationManager, IMotionAwareControl, IDisposable
{
private IList? _items;
private readonly Queue<NotificationCard> _cleanupQueue;
private readonly DispatcherTimer _cardExpiredTimer;
private readonly DispatcherTimer _cleanupTimer;
private IDisposable? _bindingDisposable;
#region 公共属性定义
public static readonly StyledProperty<NotificationPosition> PositionProperty =
AvaloniaProperty.Register<WindowNotificationManager, NotificationPosition>(
@@ -67,6 +60,12 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager,
#endregion
private TopLevel? _topLevel;
private bool _isDisposed;
private AdornerLayer? _adornerLayer;
private IList? _items;
private readonly Queue<NotificationCard> _cleanupQueue;
private readonly DispatcherTimer _cardExpiredTimer;
private readonly DispatcherTimer _cleanupTimer;
public WindowNotificationManager(TopLevel? host) : this()
{
@@ -153,6 +152,10 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager,
public void Show(INotification notification, string[]? classes = null)
{
if (_items is null)
{
return;
}
var expiration = notification.Expiration;
var onClick = notification.OnClick;
var onClose = notification.OnClose;
@@ -167,11 +170,10 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager,
Expiration = expiration == TimeSpan.Zero ? null : expiration,
IsShowProgress = notification.ShowProgress
};
_bindingDisposable?.Dispose();
_bindingDisposable = BindUtils.RelayBind(this, PositionProperty, notificationControl, NotificationCard.PositionProperty);
notificationControl[!NotificationCard.PositionProperty] = this[!PositionProperty];
// Add style classes if any
if (classes != null)
if (classes?.Length > 0)
{
foreach (var cls in classes)
{
@@ -179,11 +181,10 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager,
}
}
notificationControl.PointerPressed += (sender, args) => { onClick?.Invoke(); };
notificationControl.NotificationClosed += (sender, args) =>
notificationControl.PointerPressed += (_, _) => { onClick?.Invoke(); };
notificationControl.NotificationClosed += (sender, _) =>
{
onClose?.Invoke();
_bindingDisposable?.Dispose();
if (sender is NotificationCard card)
{
_items?.Remove(card);
@@ -195,12 +196,27 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager,
{
_items?.Add(notificationControl);
ConfigureExpiredTimer();
if (_items?.OfType<NotificationCard>().Count(i => !i.IsClosing) > MaxItems)
{
_items.OfType<NotificationCard>().First(i => !i.IsClosing).Close();
}
RemoveExcessNotifications();
});
}
/// <summary>
/// Removes excess notifications when the count exceeds MaxItems.
/// </summary>
private void RemoveExcessNotifications()
{
var visibleNotifications = _items!.OfType<NotificationCard>().Where(n => !n.IsClosing).ToList();
var excessCount = visibleNotifications.Count - MaxItems;
if (excessCount > 0)
{
// 支持关闭多个超限的通知,而不仅仅是一个
for (int i = 0; i < excessCount; i++)
{
visibleNotifications[i].Close();
}
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
@@ -211,53 +227,72 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager,
UpdatePseudoClasses(change.GetNewValue<NotificationPosition>());
}
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
ConfigureExpiredTimer();
if (_topLevel is not null)
{
_topLevel.TemplateApplied += TopLevelOnTemplateApplied;
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_items?.Clear();
_cardExpiredTimer.Stop();
_cleanupTimer.Stop();
if (_topLevel is not null)
{
_topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
}
}
private void InstallFromTopLevel(TopLevel topLevel)
{
topLevel.TemplateApplied += TopLevelOnTemplateApplied;
var adorner = topLevel.FindDescendantOfType<VisualLayerManager>()?.AdornerLayer;
if (adorner is not null)
_adornerLayer = topLevel.FindDescendantOfType<VisualLayerManager>()?.AdornerLayer;
if (_adornerLayer is not null)
{
adorner.Children.Add(this);
AdornerLayer.SetAdornedElement(this, adorner);
_adornerLayer.Children.Add(this);
AdornerLayer.SetAdornedElement(this, _adornerLayer);
}
}
private void TopLevelOnTemplateApplied(object? sender, TemplateAppliedEventArgs e)
{
if (Parent is AdornerLayer adornerLayer)
{
adornerLayer.Children.Remove(this);
AdornerLayer.SetAdornedElement(this, null);
}
RemoveFromAdornerLayer();
// Reinstall notification manager on template reapplied.
var topLevel = (TopLevel)sender!;
topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
InstallFromTopLevel(topLevel);
}
private void RemoveFromAdornerLayer()
{
if (_adornerLayer is not null)
{
_adornerLayer.Children.Remove(this);
AdornerLayer.SetAdornedElement(this, null);
_adornerLayer = null;
}
}
public void Dispose()
{
if (_topLevel is null || _isDisposed)
{
return;
}
try
{
_cleanupTimer.Stop();
_cardExpiredTimer.Stop();
_items?.Clear();
_cleanupQueue.Clear();
// 卸载事件订阅
_topLevel.TemplateApplied -= TopLevelOnTemplateApplied;
// 从 AdornerLayer 中移除
if (_adornerLayer is not null)
{
_adornerLayer.Children.Remove(this);
AdornerLayer.SetAdornedElement(this, null);
_adornerLayer = null;
}
_topLevel = null;
_items = null;
_isDisposed = true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Error uninstalling from TopLevel: {ex.Message}");
}
}
private void UpdatePseudoClasses(NotificationPosition position)
{
@@ -278,4 +313,5 @@ public class WindowNotificationManager : TemplatedControl, INotificationManager,
{
_cardExpiredTimer.Start();
}
}