Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Toast Notification -A Deep Dive

0.00/5 (No votes)
22 Apr 2018 1  
Fancy toast notification for WPF applications easy to use and support MVVM pattern

Just download the project demo, restore the missing packages using Nuget and enjoy. Smile | :)

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. Smile | :)

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
    • Notification.cs
  • 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
    • Images
      • notification-icon.png

Now we will explore the structure step by step. So we will have a look at :

Model

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

Assets

This folder contains the notification data template, the notification default styles and close button styles.

  • CloseButton.xaml
<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>
  • NotificationItem.xaml 
<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.

Core

Configuration

It contains :

  1. The NotificationConfiguration class
  2. The NotificationFlowDirection enum.

NotificationConfiguration

 public class NotificationConfiguration
    {
        #region Configuration Default values
        /// <summary>
        /// The default display duration for a notification window.
        /// </summary>
        private static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(2);

        /// <summary>
        /// The default notifications window Width
        /// </summary>
        private const int DefaultWidth = 300;

        /// <summary>
        /// The default notifications window Height
        /// </summary>
        private const int DefaultHeight = 150;

        /// <summary>
        /// The default template of notification window
        /// </summary>
        private const string DefaultTemplateName = "notificationTemplate";
        #endregion

        #region constructor
        /// <summary>
        /// Initialises the configuration object.
        /// </summary>
        /// <param name="displayDuration">The notification display duration. set it TimeSpan.
        ///                               Zero to use default value </param>
        /// <param name="width">The notification width. set it to null to use default value</param>
        /// <param name="height">The notification height. set it to null to use default value</param>
        /// <param name="templateName">The notification template name. 
        ///                            set it to null to use default value</param>
        /// <param name="notificationFlowDirection">The notification flow direction. 
        ///       set it to null to use default value (RightBottom)</param>
        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
        /// <summary>
        /// The default configuration object
        /// </summary>
        public static NotificationConfiguration DefaultConfiguration
        {
            get
            {
                return new NotificationConfiguration(DefaultDisplayDuration,DefaultWidth,
                                                     DefaultHeight, DefaultTemplateName, 
                                                     NotificationFlowDirection.RightBottom);
            }
        }

        /// <summary>
        /// The display duration for a notification window.
        /// </summary>
        public TimeSpan DisplayDuration { get; private set; }

        /// <summary>
        /// Notifications window Width
        /// </summary>
        public int? Width { get; private set; }

        /// <summary>
        /// Notifications window Height
        /// </summary>
        public int? Height { get; private set; }

        /// <summary>
        /// The template of notification window
        /// </summary>
        public string TemplateName { get; private set; }

        /// <summary>
        /// The notification window flow direction
        /// </summary>
        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();
            }
        }

        /// <summary>
        /// Remove all notifications from notification list and buffer list.
        /// </summary>
        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

Services

Contains an interface INotificationDialogService that has two methods:

public interface INotificationDialogService
    {
        /// <summary>
        /// Show notification window.
        /// </summary>
        /// <param name="content">The notification object.</param>
        void ShowNotificationWindow(object content);

        /// <summary>
        /// Show notification window.
        /// </summary>
        /// <param name="content">The notification object.</param>
        /// <param name="configuration">The notification configuration object.</param>
        void ShowNotificationWindow(object content, NotificationConfiguration configuration);
  
        /// <summary>
        /// Remove all notifications from notification list and buffer list.
        /// </summary>
        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
    {
        /// <summary>
        /// Show notification window.
        /// </summary>
        /// <param name="content">The notification object.</param>
        public void ShowNotificationWindow(object content)
        {
            NotifyBox.Show(content);
        }

        /// <summary>
        /// Show notification window.
        /// </summary>
        /// <param name="content">The notification object.</param>
        /// <param name="configuration">The notification configuration object.</param>
        public void ShowNotificationWindow(object content, NotificationConfiguration configuration)
        {
            NotifyBox.Show(content, configuration);
        }

        /// <summary>
        ///  Remove all notifications from notification list and buffer list.
        /// </summary>
        public void ClearNotifications()
        {
            NotifyBox.ClearNotifications();
        }
    }

 

Converters

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


Resources

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

 

 

 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here