Just download the project demo, restore the missing packages using Nuget and enjoy.
Introduction
In this article I will complete my previous article WPF Toast Notification. In that article we covered how to use the toast notification package in your WPF application. In this article we will explain the problem and the code behind the WPFNotification package.
Github
You can check out the project on Github from here, download the source code and the App demo and enjoy.
The demo demonstrates how to use the toast notification to display it with the default implementation or with your own custom implementation.
Nuget
WPF Toast Notification is available on NuGet, you can install it using nuget manager or run the following command in the package manager console.
PM> Install-Package WPFNotification
The Problem
Recently I needed to use a toast notification in a WPF application that displays some information to the user. So I googled for the WPF notification to check if there was any results matching what i needed and to know from where I could start. I found a good open source project called Elysium. It had the notify box feature. But there were some problems.
- The notification UI didn't match my application UI.
- I needed to display many notifications, each one with a different UI.
- It was a large library, and I didn't need the other controls.
- I needed to display one notification per time and any other notifications will be placed in a queue.
So I started from here, and adapted the notify box trying to make a lightweight and a fancy reusable component, easy to use and match what I needed.
Using the code
Ω‹The WPFNotification project structure consist of six folders :
- Model
- Assets
- CloseButton.xaml
- NotificationItem.xaml
- NotificationUI.xaml
- Core
- Configuration
- NotificationConfiguration.cs
- NotificationFlowDirection.cs
- Interactivity
- FadeBehavior.cs
- SlideBehavior.cs
- NotifyBox.cs
- Services
- INotificationDialogService.cs
- NotificationDialogService.cs
- Converters
- BaseConverter.cs
- EmptyStringConverter.cs
- Resources
Now we will explore the structure step by step. So we will have a look at :
Contains the notification model. It simply has Title , Message and Image URL
public class Notification
{
public string Title { get; set; }
public string Message { get; set; }
public string ImgURL { get; set; }
}
This folder contains the notification data template, the notification default styles and close button styles.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style x:Key="SystemButtonBase" TargetType="ButtonBase">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ButtonBase}">
<Border Name="Chrome"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
SnapsToDevicePixels="true">
<ContentPresenter Margin="{TemplateBinding Padding}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SystemButton" TargetType="ButtonBase" BasedOn="{StaticResource SystemButtonBase}">
<Setter Property="Width" Value="32" />
<Setter Property="Height" Value="24" />
<Setter Property="Foreground" Value="#d1d1d1"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3e3e42" />
<Setter Property="Foreground" Value="#d1d1d1"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#1ba1e2" />
<Setter Property="Foreground" Value="#d1d1d1" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="#515151" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="SystemCloseButton" TargetType="ButtonBase" BasedOn="{StaticResource SystemButton}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#3e3e42" />
<Setter Property="Foreground" Value="#d1d1d1"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#e51400" />
<Setter Property="Foreground" Value="White" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="#515151" />
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
<UserControl x:Class="WPFNotification.Assets.NotificationItem"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:converters="clr-namespace:WPFNotification.Converters"
mc:Ignorable="d"
d:DesignHeight="150" d:DesignWidth="300"
x:Name="NotificationWindow"
Background="Transparent">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/WPFNotification;component/Assets/CloseButton.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid Background="Transparent">
<Border Name="border" Background="#2a3345" BorderThickness="0" CornerRadius="10" Margin="10">
<Border.Effect>
<DropShadowEffect ShadowDepth="0" Opacity="0.8" BlurRadius="10"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Image Grid.RowSpan="2"
Source="{Binding ImgURL, Converter={converters:EmptyStringConverter}, ConverterParameter='pack://application:,,,/WPFNotification;component/Resources/Images/notification-icon.png'}"
Margin="4" Width="80"></Image>
<TextBlock Grid.Column="1" Text="{Binding Path=Title}" TextOptions.TextRenderingMode="ClearType" TextOptions.TextFormattingMode="Display" Foreground="White"
FontFamily="Arial" FontSize="14" FontWeight="Bold" VerticalAlignment="Center" Margin="2,4,4,2" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" />
<Button x:Name="CloseButton"
Width="16"
Height="16"
Grid.Column="1"
HorizontalAlignment="Right"
Margin="0,0,12,0"
Click="CloseButton_Click"
Style="{StaticResource SystemCloseButton}">
<Button.Content>
<Grid Width="10" Height="12" RenderTransform="1,0,0,1,0,1">
<Path Data="M0,0 L8,7 M8,0 L0,7 Z" Width="8" Height="7"
VerticalAlignment="Center" HorizontalAlignment="Center"
Stroke="{Binding Foreground, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button}}"
StrokeThickness="1.5" />
</Grid>
</Button.Content>
</Button>
<TextBlock Grid.Row="1"
Grid.Column="1"
Text="{Binding Path=Message}"
TextOptions.TextRenderingMode="ClearType"
TextOptions.TextFormattingMode="Display"
Foreground="White"
FontFamily="Arial"
VerticalAlignment="Stretch"
Margin="5"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"/>
</Grid>
</Border>
</Grid>
</UserControl>
In this file, we just create the default notification window and bind it to the notification model. The window contains :
- An image to display the notification image. If notification ImageURL is empty, it will display the default predefined image in the images folder thanks to the
EmptyStringConverter. - Two text boxes, one to display the notification title and another one to display the notification message.
- Close button to immediately close the notification window. To make that work, we need to implement the
CloseButton_Click
event.
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
Window parentWindow = Window.GetWindow(this);
this.Visibility = Visibility.Hidden;
parentWindow.Close();
}
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Model="clr-namespace:WPFNotification.Model"
xmlns:NotificationView="clr-namespace:WPFNotification.Assets">
<DataTemplate x:Key="notificationTemplate" DataType="{x:Type Model:Notification}">
<NotificationView:NotificationItem/>
</DataTemplate>
</ResourceDictionary>
A resource dictionary that contains dataTemplate to define the presentation of the notification model. We will use its name as a default value for the configuration object. You must reference this file in the App.xaml
file. You can see this in the WPF toast notification article - Getting started section.
Configuration
It contains :
- The
NotificationConfiguration
class - The
NotificationFlowDirection
enum.
NotificationConfiguration
public class NotificationConfiguration
{
#region Configuration Default values
private static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(2);
private const int DefaultWidth = 300;
private const int DefaultHeight = 150;
private const string DefaultTemplateName = "notificationTemplate";
#endregion
#region constructor
public NotificationConfiguration(TimeSpan displayDuration, int? width, int? height,
string templateName,
NotificationFlowDirection? notificationFlowDirection)
{
DisplayDuration = displayDuration > TimeSpan.Zero ? displayDuration : DefaultDisplayDuration;
Width = width.HasValue ? width : DefaultWidth;
Height = height.HasValue ? height : DefaultHeight;
TemplateName = !string.IsNullOrEmpty(templateName) ? templateName : DefaultTemplateName;
NotificationFlowDirection = notificationFlowDirection ?? NotificationFlowDirection.RightBottom;
}
#endregion
#region public Properties
public static NotificationConfiguration DefaultConfiguration
{
get
{
return new NotificationConfiguration(DefaultDisplayDuration,DefaultWidth,
DefaultHeight, DefaultTemplateName,
NotificationFlowDirection.RightBottom);
}
}
public TimeSpan DisplayDuration { get; private set; }
public int? Width { get; private set; }
public int? Height { get; private set; }
public string TemplateName { get; private set; }
public NotificationFlowDirection NotificationFlowDirection { get; set; }
#endregion
}
Using this class you can configure the notification:
- Width. The default value is 300
- Height. The default value is 150
- Display duration. The default value is 2 Seconds.
- Template Name. The default value is "notificationTemplate", this value is the name of the data template in the NotificationUI.xaml file.
- NotificationFlowDirection. That set direction in which new notification window will appear. The default value is RightBottom.
NotificationFlowDirection
Enum that represent the notification window Directions
public enum NotificationFlowDirection
{
RightBottom,
LeftBottom,
LeftUp,
RightUp,
}
Interactivity
Contains two Behaviours
- Fade Behavior for fading in/out notification window
- Slide Behavior for slide in/out notification window
Notify Box
Before we start to explain NotifyBox
class, let me introduce the WindowInfo
class. This class will hold the window Metadata.
private sealed class WindowInfo
{
public int ID { get; set; }
public TimeSpan DisplayDuration { get; set; }
public Window Window { get; set; }
}
The NotifyBox
is a static class depends on Reactive Extensions. It contains some private fields :
public static class NotifyBox
{
private const int MAX_NOTIFICATIONS = 1;
private static int notificationWindowsCount;
private const double Margin = 5;
private static List<WindowInfo> notificationWindows;
private static List<WindowInfo> notificationsBuffer;
static NotifyBox()
{
notificationWindows = new List<WindowInfo>();
notificationsBuffer = new List<WindowInfo>();
notificationWindowsCount = 0;
}
.......
.......
}
MAX_NOTIFICATIONS
is the number of notifications you want to display at the same time.
Note: In the current release, the MAX_NOTIFICATIONS
number is set to one and it is not configurable, we just display one notification per time.
-
notificationWindowsCount
an accumulated number, that represent the notifications numbers
-
notificationWindows
List of the notifications to be displayed
-
notificationsBuffer
A buffer list where the pending notifications will be placed.
Now lets explain the public methods
public static class NotifyBox
{
.....
.....
public static void Show(object content, NotificationConfiguration configuration)
{
DataTemplate notificationTemplate = (DataTemplate)Application.Current.Resources[configuration.TemplateName];
Window window = new Window()
{
Title = "",
Width = configuration.Width.Value,
Height = configuration.Height.Value,
Content = content,
ShowActivated = false,
AllowsTransparency = true,
WindowStyle = WindowStyle.None,
ShowInTaskbar = false,
Topmost = true,
Background = Brushes.Transparent,
UseLayoutRounding = true,
ContentTemplate = notificationTemplate
};
Show(window, configuration.DisplayDuration);
}
public static void Show(object content)
{
Show(content, NotificationConfiguration.DefaultConfiguration);
}
public static void Show(Window window, TimeSpan displayDuration,
NotificationFlowDirection notificationFlowDirection )
{
BehaviorCollection behaviors = Interaction.GetBehaviors(window);
behaviors.Add(new FadeBehavior());
behaviors.Add(new SlideBehavior());
SetWindowDirection(window, notificationFlowDirection);
notificationWindowsCount += 1;
WindowInfo windowInfo = new WindowInfo()
{
ID = notificationWindowsCount,
DisplayDuration = displayDuration,
Window = window
};
if (notificationWindows.Count + 1 > MAX_NOTIFICATIONS)
{
notificationsBuffer.Add(windowInfo);
}
else
{
Observable
.Timer(displayDuration)
.ObserveOnDispatcher()
.Subscribe(x => OnTimerElapsed(windowInfo));
notificationWindows.Add(windowInfo);
window.Show();
}
}
public static void ClearNotifications()
{
notificationWindows.Clear();
notificationsBuffer.Clear();
notificationWindowsCount = 0;
}
......
......
We have ClearNotifications
method :
In this method we just reset everything to zero state
- We set notificationWindowsCount to zero.
- We clear notificationWindows and notificationsBuffer lists.
Then we have three overloads of the Show
method :
public static void Show(object content)
public static void Show(object content, NotificationConfiguration configuration)
In this methods we take the following steps :
- Get the
notificationTemplate
, using the templateName
in the configuration
object. - Create the
window
object with this template and the other configuration object values. - Call the
Show
method with the window
object and the configured display duration value.
public static void Show(Window window, TimeSpan displayDuration, NotificationFlowDirection notificationFlowDirection)
In this method we take the following steps :
- We get the
BehaviorCollection
associated with the window
object and add the slide
and fade
behaviours to it. - We call
SetWindowDirection
method to set the notification window in the right coordinates based on notificationFlowDirection
value the default value is (RightBottom) corner of the screen. - We create
WindowInfo
object and we check if the number of the currently displayed notifications is greater than the MAX_NOTIFICATIONS
.if yes, we add this notification to the buffer list, otherwise we display the notification. After the display duration has elapsed, We call the callback function OnTimerElapsed
.
Now let's discuss the OnTimerElapsed
method:
private static void OnTimerElapsed(WindowInfo windowInfo)
{
if (windowInfo.Window.IsMouseOver)
{
Observable
.Timer(windowInfo.DisplayDuration)
.ObserveOnDispatcher()
.Subscribe(x => OnTimerElapsed(windowInfo));
}
else
{
BehaviorCollection behaviors = Interaction.GetBehaviors(windowInfo.Window);
FadeBehavior fadeBehavior = behaviors.OfType<FadeBehavior>().First();
SlideBehavior slideBehavior = behaviors.OfType<SlideBehavior>().First();
fadeBehavior.FadeOut();
slideBehavior.SlideOut();
EventHandler eventHandler = null;
eventHandler = (sender2, e2) =>
{
fadeBehavior.FadeOutCompleted -= eventHandler;
notificationWindows.Remove(windowInfo);
windowInfo.Window.Close();
if (notificationsBuffer != null && notificationsBuffer.Count > 0)
{
var BufferWindowInfo = notificationsBuffer.First();
Observable
.Timer(BufferWindowInfo.DisplayDuration)
.ObserveOnDispatcher()
.Subscribe(x => OnTimerElapsed(BufferWindowInfo));
notificationWindows.Add(BufferWindowInfo);
BufferWindowInfo.Window.Show();
notificationsBuffer.Remove(BufferWindowInfo);
}
};
fadeBehavior.FadeOutCompleted += eventHandler;
}
}
This method will be called after the notification display duration has elapsed. In this method we check whether the mouse pointer is still located over the notification window, In case:
- YES, We continue displaying the notification window for another display duration.
- NO, We get the associated behaviours of the window, then call
FadeOut()
and SlideOut
methods and subscribe to FadeOutCompleted
callback function. In this method:
- We close the notification window and remove it from the displayed notification list.
- If the buffer list (queue) is not empty, We pick the first notification from the queue and display it on the screen.
Now last thing in the notifyBox class let's discuss the SetWindowDirection
method:
private static void SetWindowDirection(Window window, NotificationFlowDirection notificationFlowDirection)
{
var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
var transform = PresentationSource.FromVisual(Application.Current.MainWindow).CompositionTarget.TransformFromDevice;
var corner = transform.Transform(new Point(workingArea.Right, workingArea.Bottom));
switch (notificationFlowDirection)
{
case NotificationFlowDirection.RightBottom:
window.Left = corner.X - window.Width - window.Margin.Right - Margin;
window.Top = corner.Y - window.Height - window.Margin.Top;
break;
case NotificationFlowDirection.LeftBottom:
window.Left = 0;
window.Top = corner.Y - window.Height - window.Margin.Top;
break;
case NotificationFlowDirection.LeftUp:
window.Left = 0;
window.Top = 0;
break;
case NotificationFlowDirection.RightUp:
window.Left = corner.X - window.Width - window.Margin.Right - Margin;
window.Top = 0;
break;
default:
window.Left = corner.X - window.Width - window.Margin.Right - Margin;
window.Top = corner.Y - window.Height - window.Margin.Top;
break;
}
}
In this method we just calculate the notification window coordinates to display the notification in the right coordinates based on notificationFlowDirection
value, regardless the monitor resolution or the computer is using the default DPI setting or not for more info about the DPI value in wpf check this article
Contains an interface INotificationDialogService
that has two methods:
public interface INotificationDialogService
{
void ShowNotificationWindow(object content);
void ShowNotificationWindow(object content, NotificationConfiguration configuration);
void ClearNotifications();
}
To display the notification window you must implement this interface in your project. Or use the default implementation. Let's take a look at NotificationDialogService
; in this class we just implement the ShowNotificationWindow
methods. I think the code is pretty self-explanatory.
public class NotificationDialogService : INotificationDialogService
{
public void ShowNotificationWindow(object content)
{
NotifyBox.Show(content);
}
public void ShowNotificationWindow(object content, NotificationConfiguration configuration)
{
NotifyBox.Show(content, configuration);
}
public void ClearNotifications()
{
NotifyBox.ClearNotifications();
}
}
Contains the EmptyStringConverter
we use this converter when binding the notification image to the ImgURL
property in the notification object. So the converter will assign the default image URL if it was empty.
public class EmptyStringConverter : BaseConverter, IValueConverter
{
public EmptyStringConverter()
{ }
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
return string.IsNullOrEmpty(value as string) ? parameter : value;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
it conatins Images folder that has the default notification image. This image will be used in the notification window, as long as you didn't override the ImgURL
property in the <span class="sac" id="spans0e3">notification</span>
object.
History
- 12 Feb 2016 published the original post.
- 3 Jul 2016 add new features:
- Adds support for windows 7 or later.
- Adds support for displaying notification on different screen resolutions.
- Add notification Flow Direction.
- Allow Remove All notifications in the buffer list