Browse Source

Merge pull request #12178 from timunie/feature/CustomizedNotifications

Refractor WindowNotificationManager
pull/12750/head
Max Katz 3 years ago
committed by GitHub
parent
commit
f23e9571d2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 25
      samples/ControlCatalog/Pages/NotificationsPage.xaml
  2. 8
      samples/ControlCatalog/Pages/NotificationsPage.xaml.cs
  3. 10
      samples/ControlCatalog/ViewModels/NotificationViewModel.cs
  4. 3
      src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs
  5. 55
      src/Avalonia.Controls/Notifications/Notification.cs
  6. 101
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  7. 118
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  8. 2
      src/Avalonia.Controls/Primitives/AdornerLayer.cs

25
samples/ControlCatalog/Pages/NotificationsPage.xaml

@ -1,11 +1,28 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="using:ControlCatalog.ViewModels"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
x:Class="ControlCatalog.Pages.NotificationsPage"
x:DataType="viewModels:NotificationViewModel">
<StackPanel Orientation="Vertical" Spacing="4" HorizontalAlignment="Left">
<Button Content="Show Standard Managed Notification" Command="{Binding ShowManagedNotificationCommand}" />
<Button Content="Show Custom Managed Notification" Command="{Binding ShowCustomManagedNotificationCommand}" />
<Button Content="Show Native Notification" Command="{Binding ShowNativeNotificationCommand}" />
<DockPanel>
<TextBlock DockPanel.Dock="Top"
Margin="2" Classes="h2" TextWrapping="Wrap">TopLevel bound notification manager.</TextBlock>
<StackPanel DockPanel.Dock="Top"
Orientation="Vertical" Spacing="4" HorizontalAlignment="Left">
<Button Content="Show Standard Managed Notification" Command="{Binding ShowManagedNotificationCommand}" />
<Button Content="Show Custom Managed Notification" Command="{Binding ShowCustomManagedNotificationCommand}" />
</StackPanel>
<TextBlock DockPanel.Dock="Top"
Margin="2" Classes="h2" TextWrapping="Wrap">XAML only notification manager.</TextBlock>
<Button DockPanel.Dock="Top"
Content="Show XAML only Notification" Command="{Binding #ControlNotifications.Show}">
<Button.CommandParameter>
<Notification Title="Title" Message="Message" OnClick="NotificationOnClick" />
</Button.CommandParameter>
</Button>
<Border Padding="10" BorderBrush="{DynamicResource SystemAccentColor}">
<WindowNotificationManager x:Name="ControlNotifications" />
</Border>
</DockPanel>
</UserControl>

8
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<WindowNotificationManager>("ControlNotifications").Show("Notification clicked");
}
}
}

10
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; }
}
}

3
src/Avalonia.Controls/Notifications/IManagedNotificationManager.cs

@ -1,4 +1,5 @@
using Avalonia.Metadata;
using System;
using Avalonia.Metadata;
namespace Avalonia.Controls.Notifications
{

55
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
/// <see cref="WindowNotificationManager"/> or by the host operating system (to be implemented).
/// </remarks>
public class Notification : INotification
public class Notification : INotification, INotifyPropertyChanged
{
private string? _title, _message;
/// <summary>
/// Initializes a new instance of the <see cref="Notification"/> class.
/// </summary>
@ -35,23 +40,59 @@ namespace Avalonia.Controls.Notifications
OnClick = onClick;
OnClose = onClose;
}
/// <summary>
/// Initializes a new instance of the <see cref="Notification"/> class.
/// </summary>
public Notification() : this(null, null)
{
}
/// <inheritdoc/>
public string? Title { get; private set; }
public string? Title
{
get => _title;
set
{
if (_title != value)
{
_title = value;
OnPropertyChanged();
}
}
}
/// <inheritdoc/>
public string? Message { get; private set; }
public string? Message
{
get => _message;
set
{
if (_message != value)
{
_message = value;
OnPropertyChanged();
}
}
}
/// <inheritdoc/>
public NotificationType Type { get; private set; }
public NotificationType Type { get; set; }
/// <inheritdoc/>
public TimeSpan Expiration { get; private set; }
public TimeSpan Expiration { get; set; }
/// <inheritdoc/>
public Action? OnClick { get; private set; }
public Action? OnClick { get; set; }
/// <inheritdoc/>
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));
}
}
}

101
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
/// </summary>
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();
}
/// <summary>
@ -93,6 +58,21 @@ namespace Avalonia.Controls.Notifications
public static readonly StyledProperty<bool> IsClosedProperty =
AvaloniaProperty.Register<NotificationCard, bool>(nameof(IsClosed));
/// <summary>
/// Gets or sets the type of the notification
/// </summary>
public NotificationType NotificationType
{
get { return GetValue(NotificationTypeProperty); }
set { SetValue(NotificationTypeProperty, value); }
}
/// <summary>
/// Defines the <see cref="NotificationType" /> property
/// </summary>
public static readonly StyledProperty<NotificationType> NotificationTypeProperty =
AvaloniaProperty.Register<NotificationCard, NotificationType>(nameof(NotificationType));
/// <summary>
/// Defines the <see cref="NotificationClosed"/> event.
/// </summary>
@ -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;
}
}
}
}

118
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;
/// <summary>
/// Defines the <see cref="Position"/> property.
/// </summary>
@ -49,18 +49,24 @@ namespace Avalonia.Controls.Notifications
get { return GetValue(MaxItemsProperty); }
set { SetValue(MaxItemsProperty, value); }
}
/// <summary>
/// Initializes a new instance of the <see cref="WindowNotificationManager"/> class.
/// </summary>
/// <param name="host">The window that will host the control.</param>
public WindowNotificationManager(TopLevel? host)
/// <param name="host">The TopLevel that will host the control.</param>
public WindowNotificationManager(TopLevel? host) : this()
{
if (host != null)
if (host is not null)
{
Install(host);
InstallFromTopLevel(host);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="WindowNotificationManager"/> class.
/// </summary>
public WindowNotificationManager()
{
UpdatePseudoClasses(Position);
}
@ -73,6 +79,8 @@ namespace Avalonia.Controls.Notifications
/// <inheritdoc/>
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
var itemsControl = e.NameScope.Find<Panel>("PART_Items");
_items = itemsControl?.Children;
}
@ -80,49 +88,85 @@ namespace Avalonia.Controls.Notifications
/// <inheritdoc/>
public void Show(INotification content)
{
Show(content as object);
Show(content, content.Type, content.Expiration, content.OnClick, content.OnClose);
}
/// <inheritdoc/>
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);
}
}
/// <summary>
/// Shows a Notification
/// </summary>
/// <param name="content">the content of the notification</param>
/// <param name="type">the type of the notification</param>
/// <param name="expiration">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</param>
/// <param name="onClick">an Action to be run when the notification is clicked</param>
/// <param name="onClose">an Action to be run when the notification is closed</param>
/// <param name="classes">style classes to apply</param>
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<NotificationCard>().Count(i => !i.IsClosing) > MaxItems)
Dispatcher.UIThread.Post(() =>
{
_items.OfType<NotificationCard>().First(i => !i.IsClosing).Close();
}
_items?.Add(notificationControl);
if (notification != null && notification.Expiration == TimeSpan.Zero)
if (_items?.OfType<NotificationCard>().Count(i => !i.IsClosing) > MaxItems)
{
_items.OfType<NotificationCard>().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
/// <summary>
/// Installs the <see cref="WindowNotificationManager"/> within the <see cref="AdornerLayer"/>
/// of the host <see cref="Window"/>.
/// </summary>
/// <param name="host">The <see cref="Window"/> that will be the host.</param>
private void Install(TemplatedControl host)
private void InstallFromTopLevel(TopLevel topLevel)
{
var adornerLayer = host.FindDescendantOfType<VisualLayerManager>()?.AdornerLayer;
topLevel.TemplateApplied += TopLevelOnTemplateApplied;
var adorner = topLevel.FindDescendantOfType<VisualLayerManager>()?.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)

2
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);
}

Loading…
Cancel
Save