Introduction
Recently I needed to add Growl like notifications to a WPF project. Just a notification system that only this program will be using. I Googled a bit and found Growl for Windows, it's fine but seemed too much for just this functionality (it has an independent component and common message bus, which I don't need). Also you're adding new dependencies (Growl components, Forms, etc.) and new libraries to
the project, but you can just have a few classes to have the same behavior of notifications to handle this.
Functionality
This implementation provides the following functionality:
- Notifications could be added and will be placed on the screen
- Specific notifications can be deleted
- Notification fades in when added (2 sec)
-
Notification stays for 6 seconds after fade in and then it will fade out (2
sec) and collapse
- If user places mouse
pointer above a notification it becomes fully visible and doesn't fade out
- There's
a maximum number of notifications, if there's more than the max number, they
are placed in a queue and will be shown when the place is available
-
The looks of the notification is defined by a DataTemplate
- Notification class is used to store
data, which is bound to the DataTemplate
Using the code
GrowlNotifications class
The GrowlNotifiactions
class contains logic to add or remove notifications. It is very simple. First of all
a DataContext is set on creation to Notifications, which is an ObservableCollection<Notification>
. This collection is
the source for the ItemControl
in XAML.
using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace WPFGrowlNotification
{
public partial class GrowlNotifiactions
{
private const byte MAX_NOTIFICATIONS = 4;
private int count;
public Notifications Notifications = new Notifications();
private readonly Notifications buffer = new Notifications();
public GrowlNotifiactions()
{
InitializeComponent();
NotificationsControl.DataContext = Notifications;
}
public void AddNotification(Notification notification)
{
notification.Id = count++;
if (Notifications.Count + 1 > MAX_NOTIFICATIONS)
buffer.Add(notification);
else
Notifications.Add(notification);
if (Notifications.Count > 0 && !IsActive)
Show();
}
public void RemoveNotification(Notification notification)
{
if (Notifications.Contains(notification))
Notifications.Remove(notification);
if (buffer.Count > 0)
{
Notifications.Add(buffer[0]);
buffer.RemoveAt(0);
}
if (Notifications.Count < 1)
Hide();
}
private void NotificationWindowSizeChanged(object sender, SizeChangedEventArgs e)
{
if (e.NewSize.Height != 0.0)
return;
var element = sender as Grid;
RemoveNotification(Notifications.First(
n => n.Id == Int32.Parse(element.Tag.ToString())));
}
}
}
When a new notification is added a unique ID is assigned. This is needed when we want to remove
an element from a collection (when it is collapsed in this case), see NotificationWindowSizeChanged
. Then if there's space,
the
ItemsControl
element notification is added directly to the Notifications
collection, otherwise it will be stored in
a buffer variable. The final step is to show the window.
When a notification
is removed, the buffer is checked and if there's something in it, it's pushed to
the Notifications
collection. If there's nothing to show,
the window is closed.
Notification class
The notification class implements INotifyPropertyChanged
, so you can bind to its values in DataTemplate. You can customize it to show whatever you like.
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace WPFGrowlNotification
{
public class Notification : INotifyPropertyChanged
{
private string message;
public string Message
{
get { return message; }
set
{
if (message == value) return;
message = value;
OnPropertyChanged("Message");
}
}
private int id;
public int Id
{
get { return id; }
set
{
if (id == value) return;
id = value;
OnPropertyChanged("Id");
}
}
private string imageUrl;
public string ImageUrl
{
get { return imageUrl; }
set
{
if (imageUrl == value) return;
imageUrl = value;
OnPropertyChanged("ImageUrl");
}
}
private string title;
public string Title
{
get { return title; }
set
{
if (title == value) return;
title = value;
OnPropertyChanged("Title");
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Notifications : ObservableCollection<Notification> { }
}
XAML
All the animations are in XAML and it is very handy. It is easy to go through them, there're only four triggers. Also you can see
the
DataTemplate
which can be customized however you like to show what's in
the
Notification
class.
<Window x:Class="WPFGrowlNotification.GrowlNotifiactions"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:Model="clr-namespace:WPFGrowlNotification"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
Title="GrowlNotifiactions" Height="530" Width="300" ShowActivated="False"
AllowsTransparency="True" WindowStyle="None" ShowInTaskbar="False"
Background="Transparent" Topmost="True" UseLayoutRounding="True">
<Window.Resources>
<Storyboard x:Key="CollapseStoryboard">
<DoubleAnimation From="100" To="0" Storyboard.TargetProperty="Height" Duration="0:0:1"/>
</Storyboard>
<DataTemplate x:Key="MessageTemplate" DataType="Model:Notification">
<Grid x:Name="NotificationWindow" Tag="{Binding Path=Id}"
Background="Transparent" SizeChanged="NotificationWindowSizeChanged">
<Border Name="border" Background="#2a3345"
BorderThickness="0" CornerRadius="10" Margin="10">
<Border.Effect>
<DropShadowEffect ShadowDepth="0" Opacity="0.8" BlurRadius="10"/>
</Border.Effect>
<Grid Height="100" Width="280" Margin="6">
<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 Path=ImageUrl}" 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" Grid.Column="1" Width="16" Height="16"
HorizontalAlignment="Right" Margin="0,0,12,0" Style="{StaticResource CloseButton}" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=Message}"
TextOptions.TextRenderingMode="ClearType"
TextOptions.TextFormattingMode="Display" Foreground="White"
FontFamily="Arial" VerticalAlignment="Center"
Margin="2,2,4,4" TextWrapping="Wrap" TextTrimming="CharacterEllipsis"/>
</Grid>
</Border>
</Grid>
<DataTemplate.Triggers>
<EventTrigger RoutedEvent="Window.Loaded" SourceName="NotificationWindow">
<BeginStoryboard x:Name="FadeInStoryBoard">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="NotificationWindow"
From="0.01" To="1" Storyboard.TargetProperty="Opacity" Duration="0:0:2"/>
<DoubleAnimation Storyboard.TargetName="NotificationWindow"
From="1" To="0" Storyboard.TargetProperty="Opacity"
Duration="0:0:2" BeginTime="0:0:6"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<Trigger Property="IsMouseOver" Value="True">
<Trigger.EnterActions>
<SeekStoryboard Offset="0:0:3" BeginStoryboardName="FadeInStoryBoard" />
<PauseStoryboard BeginStoryboardName="FadeInStoryBoard" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<SeekStoryboard Offset="0:0:3" BeginStoryboardName="FadeInStoryBoard" />
<ResumeStoryboard BeginStoryboardName="FadeInStoryBoard"></ResumeStoryboard>
</Trigger.ExitActions>
</Trigger>
<EventTrigger RoutedEvent="Button.Click" SourceName="CloseButton">
<BeginStoryboard>
<Storyboard >
<DoubleAnimation Storyboard.TargetName="NotificationWindow"
From="1" To="0" Storyboard.TargetProperty="(Grid.Opacity)" Duration="0:0:0"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<Trigger SourceName="NotificationWindow" Property="Opacity" Value="0">
<Setter TargetName="NotificationWindow" Property="Visibility" Value="Hidden"></Setter>
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource CollapseStoryboard}"/>
</Trigger.EnterActions>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<ItemsControl x:Name="NotificationsControl" FocusVisualStyle="{x:Null}"
d:DataContext="{d:DesignData Source=DesignTimeNotificationData.xaml}"
ItemsSource="{Binding .}" ItemTemplate="{StaticResource MessageTemplate}" />
</Window>
Sample application
The sample application has a window that adds different notifications to the
notification window.
History
- 02/18/2013 - Bug fix: notification window should be hidden, not closed. Closing should be external or on some event.
- 02/17/2013 - Bug fix: Collection window stays open if the last notification is closed. Thanks ChrDressler (see comments).
- 12/19/2012 -
ItemsControl
focusable style is set to null. Notifications window is not activated. Thanks John Schroedl (see comments).
- 12/10/2012 - Executable added to downloads.
- 11/26/2012 - Initial version.