diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml b/samples/ControlCatalog/Pages/NotificationsPage.xaml index 46c1fe52de..b4094d8a2e 100644 --- a/samples/ControlCatalog/Pages/NotificationsPage.xaml +++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml @@ -1,11 +1,28 @@ - - + + + + diff --git a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs index 6f83e5c366..fac1989976 100644 --- a/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs +++ b/samples/ControlCatalog/Pages/NotificationsPage.xaml.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Notifications; using Avalonia.Markup.Xaml; using ControlCatalog.ViewModels; @@ -27,7 +28,12 @@ namespace ControlCatalog.Pages { base.OnAttachedToVisualTree(e); - _viewModel.NotificationManager = new Avalonia.Controls.Notifications.WindowNotificationManager(TopLevel.GetTopLevel(this)); + _viewModel.NotificationManager = new WindowNotificationManager(TopLevel.GetTopLevel(this)!); + } + + public void NotificationOnClick() + { + this.Get("ControlNotifications").Show("Notification clicked"); } } } diff --git a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs index bcbcb345ef..40af6e033a 100644 --- a/samples/ControlCatalog/ViewModels/NotificationViewModel.cs +++ b/samples/ControlCatalog/ViewModels/NotificationViewModel.cs @@ -11,7 +11,7 @@ namespace ControlCatalog.ViewModels { ShowCustomManagedNotificationCommand = MiniCommand.Create(() => { - NotificationManager?.Show(new NotificationViewModel() { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" , NotificationManager = NotificationManager}); + NotificationManager?.Show(new NotificationViewModel() { Title = "Hey There!", Message = "Did you know that Avalonia now supports Custom In-Window Notifications?" , NotificationManager = NotificationManager}, NotificationType.Warning); }); ShowManagedNotificationCommand = MiniCommand.Create(() => @@ -19,11 +19,6 @@ namespace ControlCatalog.ViewModels NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Welcome", "Avalonia now supports Notifications.", NotificationType.Information)); }); - ShowNativeNotificationCommand = MiniCommand.Create(() => - { - NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Error", "Native Notifications are not quite ready. Coming soon.", NotificationType.Error)); - }); - YesCommand = MiniCommand.Create(() => { NotificationManager?.Show(new Avalonia.Controls.Notifications.Notification("Avalonia Notifications", "Start adding notifications to your app today.")); @@ -45,8 +40,5 @@ namespace ControlCatalog.ViewModels public MiniCommand ShowCustomManagedNotificationCommand { get; } public MiniCommand ShowManagedNotificationCommand { get; } - - public MiniCommand ShowNativeNotificationCommand { get; } - } } diff --git a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs index b2e6e9e80b..bd57f3a86f 100644 --- a/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs @@ -1,4 +1,5 @@ -using Avalonia.Metadata; +using System; +using Avalonia.Metadata; namespace Avalonia.Controls.Notifications { diff --git a/src/Avalonia.Controls/Notifications/Notification.cs b/src/Avalonia.Controls/Notifications/Notification.cs index 376df175f3..ace8cba243 100644 --- a/src/Avalonia.Controls/Notifications/Notification.cs +++ b/src/Avalonia.Controls/Notifications/Notification.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; namespace Avalonia.Controls.Notifications { @@ -9,8 +12,10 @@ namespace Avalonia.Controls.Notifications /// This class represents a notification that can be displayed either in a window using /// or by the host operating system (to be implemented). /// - public class Notification : INotification + public class Notification : INotification, INotifyPropertyChanged { + private string? _title, _message; + /// /// Initializes a new instance of the class. /// @@ -35,23 +40,59 @@ namespace Avalonia.Controls.Notifications OnClick = onClick; OnClose = onClose; } + + /// + /// Initializes a new instance of the class. + /// + public Notification() : this(null, null) + { + } /// - public string? Title { get; private set; } + public string? Title + { + get => _title; + set + { + if (_title != value) + { + _title = value; + OnPropertyChanged(); + } + } + } /// - public string? Message { get; private set; } + public string? Message + { + get => _message; + set + { + if (_message != value) + { + _message = value; + OnPropertyChanged(); + } + } + } /// - public NotificationType Type { get; private set; } + public NotificationType Type { get; set; } /// - public TimeSpan Expiration { get; private set; } + public TimeSpan Expiration { get; set; } /// - public Action? OnClick { get; private set; } + public Action? OnClick { get; set; } /// - public Action? OnClose { get; private set; } + public Action? OnClose { get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } } } diff --git a/src/Avalonia.Controls/Notifications/NotificationCard.cs b/src/Avalonia.Controls/Notifications/NotificationCard.cs index 7d5b6cc0ca..d233525056 100644 --- a/src/Avalonia.Controls/Notifications/NotificationCard.cs +++ b/src/Avalonia.Controls/Notifications/NotificationCard.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using Avalonia.Reactive; using Avalonia.Controls.Metadata; @@ -25,42 +25,7 @@ namespace Avalonia.Controls.Notifications /// public NotificationCard() { - this.GetObservable(IsClosedProperty) - .Subscribe(x => - { - if (!IsClosing && !IsClosed) - { - return; - } - - RaiseEvent(new RoutedEventArgs(NotificationClosedEvent)); - }); - - this.GetObservable(ContentProperty) - .Subscribe(x => - { - if (x is INotification notification) - { - switch (notification.Type) - { - case NotificationType.Error: - PseudoClasses.Add(":error"); - break; - - case NotificationType.Information: - PseudoClasses.Add(":information"); - break; - - case NotificationType.Success: - PseudoClasses.Add(":success"); - break; - - case NotificationType.Warning: - PseudoClasses.Add(":warning"); - break; - } - } - }); + UpdateNotificationType(); } /// @@ -93,6 +58,21 @@ namespace Avalonia.Controls.Notifications public static readonly StyledProperty IsClosedProperty = AvaloniaProperty.Register(nameof(IsClosed)); + /// + /// Gets or sets the type of the notification + /// + public NotificationType NotificationType + { + get { return GetValue(NotificationTypeProperty); } + set { SetValue(NotificationTypeProperty, value); } + } + + /// + /// Defines the property + /// + public static readonly StyledProperty NotificationTypeProperty = + AvaloniaProperty.Register(nameof(NotificationType)); + /// /// Defines the event. /// @@ -163,5 +143,52 @@ namespace Avalonia.Controls.Notifications IsClosing = true; } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.Property == ContentProperty && e.NewValue is INotification notification) + { + SetValue(NotificationTypeProperty, notification.Type); + } + + if (e.Property == NotificationTypeProperty) + { + UpdateNotificationType(); + } + + if (e.Property == IsClosedProperty) + { + if (!IsClosing && !IsClosed) + { + return; + } + + RaiseEvent(new RoutedEventArgs(NotificationClosedEvent)); + } + } + + private void UpdateNotificationType() + { + switch (NotificationType) + { + case NotificationType.Error: + PseudoClasses.Add(":error"); + break; + + case NotificationType.Information: + PseudoClasses.Add(":information"); + break; + + case NotificationType.Success: + PseudoClasses.Add(":success"); + break; + + case NotificationType.Warning: + PseudoClasses.Add(":warning"); + break; + } + } } } diff --git a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs index b03099f750..6d9a030ead 100644 --- a/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs +++ b/src/Avalonia.Controls/Notifications/WindowNotificationManager.cs @@ -1,11 +1,12 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; -using Avalonia.Reactive; using System.Threading.Tasks; +using Avalonia.Collections; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; -using Avalonia.Rendering; +using Avalonia.Threading; using Avalonia.VisualTree; namespace Avalonia.Controls.Notifications @@ -18,7 +19,6 @@ namespace Avalonia.Controls.Notifications public class WindowNotificationManager : TemplatedControl, IManagedNotificationManager { private IList? _items; - /// /// Defines the property. /// @@ -49,18 +49,24 @@ namespace Avalonia.Controls.Notifications get { return GetValue(MaxItemsProperty); } set { SetValue(MaxItemsProperty, value); } } - + /// /// Initializes a new instance of the class. /// - /// The window that will host the control. - public WindowNotificationManager(TopLevel? host) + /// The TopLevel that will host the control. + public WindowNotificationManager(TopLevel? host) : this() { - if (host != null) + if (host is not null) { - Install(host); + InstallFromTopLevel(host); } + } + /// + /// Initializes a new instance of the class. + /// + public WindowNotificationManager() + { UpdatePseudoClasses(Position); } @@ -73,6 +79,8 @@ namespace Avalonia.Controls.Notifications /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { + base.OnApplyTemplate(e); + var itemsControl = e.NameScope.Find("PART_Items"); _items = itemsControl?.Children; } @@ -80,49 +88,85 @@ namespace Avalonia.Controls.Notifications /// public void Show(INotification content) { - Show(content as object); + Show(content, content.Type, content.Expiration, content.OnClick, content.OnClose); } /// public async void Show(object content) { - var notification = content as INotification; - + if (content is INotification notification) + { + Show(notification, notification.Type, notification.Expiration, notification.OnClick, notification.OnClose); + } + else + { + Show(content, NotificationType.Information); + } + } + + /// + /// Shows a Notification + /// + /// the content of the notification + /// the type of the notification + /// the expiration time of the notification after which it will automatically close. If the value is Zero then the notification will remain open until the user closes it + /// 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, + TimeSpan? expiration = null, + Action? onClick = null, + Action? onClose = null, + string[]? classes = null) + { + Dispatcher.UIThread.VerifyAccess(); + var notificationControl = new NotificationCard { - Content = content + Content = content, + NotificationType = type }; + // Add style classes if any + if (classes != null) + { + foreach (var @class in classes) + { + notificationControl.Classes.Add(@class); + } + } + notificationControl.NotificationClosed += (sender, args) => { - notification?.OnClose?.Invoke(); + onClose?.Invoke(); _items?.Remove(sender); }; notificationControl.PointerPressed += (sender, args) => { - if (notification != null && notification.OnClick != null) - { - notification.OnClick.Invoke(); - } + onClick?.Invoke(); (sender as NotificationCard)?.Close(); }; - _items?.Add(notificationControl); - - if (_items?.OfType().Count(i => !i.IsClosing) > MaxItems) + Dispatcher.UIThread.Post(() => { - _items.OfType().First(i => !i.IsClosing).Close(); - } + _items?.Add(notificationControl); - if (notification != null && notification.Expiration == TimeSpan.Zero) + if (_items?.OfType().Count(i => !i.IsClosing) > MaxItems) + { + _items.OfType().First(i => !i.IsClosing).Close(); + } + }); + + if (expiration == TimeSpan.Zero) { return; } - await Task.Delay(notification?.Expiration ?? TimeSpan.FromSeconds(5)); + await Task.Delay(expiration ?? TimeSpan.FromSeconds(5)); notificationControl.Close(); } @@ -139,18 +183,30 @@ namespace Avalonia.Controls.Notifications /// /// Installs the within the - /// of the host . /// - /// The that will be the host. - private void Install(TemplatedControl host) + private void InstallFromTopLevel(TopLevel topLevel) { - var adornerLayer = host.FindDescendantOfType()?.AdornerLayer; + topLevel.TemplateApplied += TopLevelOnTemplateApplied; + var adorner = topLevel.FindDescendantOfType()?.AdornerLayer; + if (adorner is not null) + { + adorner.Children.Add(this); + AdornerLayer.SetAdornedElement(this, adorner); + } + } - if (adornerLayer is not null) + private void TopLevelOnTemplateApplied(object? sender, TemplateAppliedEventArgs e) + { + if (Parent is AdornerLayer adornerLayer) { - adornerLayer.Children.Add(this); - AdornerLayer.SetAdornedElement(this, adornerLayer); + adornerLayer.Children.Remove(this); + AdornerLayer.SetAdornedElement(this, null); } + + // Reinstall notification manager on template reapplied. + var topLevel = (TopLevel)sender!; + topLevel.TemplateApplied -= TopLevelOnTemplateApplied; + InstallFromTopLevel(topLevel); } private void UpdatePseudoClasses(NotificationPosition position) diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 37c46e1e7d..f21b608667 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Primitives return adorner.GetValue(AdornedElementProperty); } - public static void SetAdornedElement(Visual adorner, Visual adorned) + public static void SetAdornedElement(Visual adorner, Visual? adorned) { adorner.SetValue(AdornedElementProperty, adorned); }