diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index f1e94d480d..69f568f9f6 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Avalonia.Reactive; using Avalonia.Controls.Metadata; using Avalonia.Interactivity; using Avalonia.LogicalTree; diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index 4500be6136..741c8d765d 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -2,7 +2,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Threading; @@ -17,8 +16,12 @@ namespace Avalonia.Controls.Notifications [PseudoClasses(":topleft", ":topright", ":bottomleft", ":bottomright", ":topcenter", ":bottomcenter")] public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager { - private IList? _items; - private readonly Dictionary _notificationCards = new (); + private IList _items = new List(); + + /// + /// Currently active notifications. + /// + internal IEnumerable Notifications => _items.OfType(); /// /// Defines the property. @@ -81,9 +84,13 @@ namespace Avalonia.Controls.Notifications protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - + var itemsControl = e.NameScope.Find("PART_Items"); - _items = itemsControl?.Children; + + if (itemsControl?.Children is not null) + { + _items = itemsControl.Children; + } } /// @@ -114,11 +121,11 @@ namespace Avalonia.Controls.Notifications /// an Action to be run when the notification is clicked /// an Action to be run when the notification is closed /// style classes to apply - public async void Show(object content, - NotificationType type, + public void Show(object content, + NotificationType type, TimeSpan? expiration = null, - Action? onClick = null, - Action? onClose = null, + Action? onClick = null, + Action? onClose = null, string[]? classes = null) { Dispatcher.UIThread.VerifyAccess(); @@ -141,9 +148,7 @@ namespace Avalonia.Controls.Notifications notificationControl.NotificationClosed += (sender, args) => { onClose?.Invoke(); - - _items?.Remove(sender); - _notificationCards.Remove(content); + _items.Remove(notificationControl); }; notificationControl.PointerPressed += (sender, args) => @@ -153,49 +158,31 @@ namespace Avalonia.Controls.Notifications (sender as NotificationCard)?.Close(); }; - Dispatcher.UIThread.Post(() => - { - _items?.Add(notificationControl); - _notificationCards.Add(content, notificationControl); + _items.Add(notificationControl); - if (_items?.OfType().Count(i => !i.IsClosing) > MaxItems) - { - _items.OfType().First(i => !i.IsClosing).Close(); - } - }); + if (Notifications.Count(i => !i.IsClosing) > MaxItems) + { + Notifications.First(i => !i.IsClosing).Close(); + } if (expiration == TimeSpan.Zero) { return; } - await Task.Delay(expiration ?? TimeSpan.FromSeconds(5)); - - notificationControl.Close(); - - _notificationCards.Remove(content); + DispatcherTimer.RunOnce(notificationControl.Close, expiration ?? TimeSpan.FromSeconds(5)); } /// - public void Close(INotification notification) - { - Dispatcher.UIThread.VerifyAccess(); - - if (_notificationCards.Remove(notification, out var notificationCard)) - { - notificationCard.Close(); - } - } + public void Close(INotification notification) => Close(notification as object); /// public void Close(object content) { Dispatcher.UIThread.VerifyAccess(); - if (_notificationCards.Remove(content, out var notificationCard)) - { - notificationCard.Close(); - } + var cardsToClose = Notifications.Where(x => x.Content?.Equals(content) ?? false); + cardsToClose?.Do(x => x.Close()); } /// @@ -203,12 +190,7 @@ namespace Avalonia.Controls.Notifications { Dispatcher.UIThread.VerifyAccess(); - foreach (var kvp in _notificationCards) - { - kvp.Value.Close(); - } - - _notificationCards.Clear(); + Notifications.Do(x => x.Close()); } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) @@ -225,7 +207,7 @@ namespace Avalonia.Controls.Notifications { base.OnDetachedFromVisualTree(e); - _notificationCards.Clear(); + _items.Clear(); } /// @@ -249,7 +231,7 @@ namespace Avalonia.Controls.Notifications adornerLayer.Children.Remove(this); AdornerLayer.SetAdornedElement(this, null); } - + // Reinstall notification manager on template reapplied. var topLevel = (TopLevel)sender!; topLevel.TemplateApplied -= TopLevelOnTemplateApplied; diff --git a/tests/Avalonia.Controls.UnitTests/NotificationsTests.cs b/tests/Avalonia.Controls.UnitTests/NotificationsTests.cs new file mode 100644 index 0000000000..e8411f7da1 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/NotificationsTests.cs @@ -0,0 +1,116 @@ +using System.Linq; +using Avalonia.Controls.Notifications; +using Avalonia.UnitTests; +using Xunit; +using Notification = Avalonia.Controls.Notifications.Notification; + +namespace Avalonia.Controls.UnitTests +{ + public class WindowNotificationManagerTests : ScopedTestBase + { + [Fact] + public void Show_Notifications_With_Same_String() + { + WindowNotificationManager manager = new(); + + manager.Show("Notification text"); + manager.Show("Notification text"); + manager.Show("Notification text"); + + Assert.Equal(3, manager.Notifications.Count()); + } + + [Fact] + public void Show_And_Close_Notification() + { + WindowNotificationManager manager = new(); + + manager.Show("Notification text"); + + Assert.Equal(1, manager.Notifications.Count()); + + manager.Close("Notification text"); + + Assert.True(!manager.Notifications.Any(x => !x.IsClosing)); + } + + [Fact] + public void Show_And_Close_All_Notifications() + { + WindowNotificationManager manager = new(); + + manager.Show("Notification 1"); + manager.Show("Notification 2"); + + Assert.Equal(2, manager.Notifications.Count()); + + manager.CloseAll(); + + Assert.True(!manager.Notifications.Any(x => !x.IsClosing)); + } + } + + public class INotificationManagerTests : ScopedTestBase + { + [Fact] + public void Show_Notifications_With_Same_Content() + { + INotificationManager manager = new WindowNotificationManager(); + + Notification notification = new() + { + Message = "Notification text" + }; + + manager.Show(notification); + manager.Show(notification); + manager.Show(notification); + + Assert.Equal(3, ((WindowNotificationManager)manager).Notifications.Count()); + } + + [Fact] + public void Show_And_Close_Notification() + { + INotificationManager manager = new WindowNotificationManager(); + + Notification notification = new() + { + Message = "Notification text" + }; + + manager.Show(notification); + + Assert.Equal(1, ((WindowNotificationManager)manager).Notifications.Count()); + + manager.Close(notification); + + Assert.True(!((WindowNotificationManager)manager).Notifications.Any(x => !x.IsClosing)); + } + + [Fact] + public void Show_And_Close_All_Notifications() + { + INotificationManager manager = new WindowNotificationManager(); + + Notification notification1 = new() + { + Message = "Notification text" + }; + + Notification notification2 = new() + { + Message = "Notification text" + }; + + manager.Show(notification1); + manager.Show(notification2); + + Assert.Equal(2, ((WindowNotificationManager)manager).Notifications.Count()); + + manager.CloseAll(); + + Assert.True(!((WindowNotificationManager)manager).Notifications.Any(x => !x.IsClosing)); + } + } +}