Desktop alerts are a great feature in many applications. Whenever something important (and perhaps urgent) happens a nice little window appears at the bottom right corner of the screen and tells you about it. Everyone is familiar with what they look like in Outlook:
Even though these little notification popup windows can be quite annoying if used incorrectly (I'm thinking Messenger and a multitude of alerts here), they can most certainly be a good tool for making sure users are made aware of pressing issues they should be reacting to.
A few years ago I created a custom control for showing alerts like this in Windows Forms. However, I did find that different applications needed different types of alerts, with different functionality in them. Satisfying this need in Windows Forms always meant doing a whole bunch of coding and deriving a new custom control from my initial one. Now when Visual Studio 2008 was released, with decent support for Windows Presentation Foundation, I figured it would be a good time to revisit the old control and see how it could be implemented in WPF.
Since the thinking around controls in WPF is quite different from Windows Forms, and there is such a strong emphasis on the separation of behavior and appearance. First of all, is it even necessary to create a special control, or would a control template applied to for example a window be sufficient?
After thinking about it I came to the conclusion that there's enough behavior associated with a desktop alert to warrant making a custom control. It needs to be shown only for a short while, unless the user activates it (then it should stay visible until closed by the user). There can also be several alerts visible at the same time, and then they should be laid out in such a way that they don't obscure each other. Trying to achieve this with just a repeatable template would be difficult, but it should be possible to standardize these through a custom control. Some of the thinking that went into this was outlined in my previous post, "Closing a WPF custom control from a storyboard". At the same time I decided to create a small sample of this, and that is what you're reading now. :) The code itself is downloadable, look at the bottom of this post for a link.
Creating a desktop alert sample
Different applications need different looking alerts. The one used by Outlook contains both information about the email (sender, subject and a snippet of the email body) as well as commands you can perform on the email (delete, flag as urgent and so forth). Another application might want to provide links that when clicked open up different views of data in the application, depending on what the alert was about. These all sound like things that would be easy to provide specifically for each application by just showing application-specific XAML content in the alert window.
The next challenge was which existing WPF control should I base my control on, and how the control should be invoked and used. I decided to base it on Window, and allow deriving from the control to create other alert windows with different content in them like this:
(I'm sorry about the uneven lines and shaky text in the image above, but I'm writing this on an airplane...)
DesktopAlertBase is a basic alert window, that you derive from to create specific alert window types. In the diagram above there are two sample derived classes, SimpleAlert (which has a title and a text body) and ImageAlert (which has a title and shows an image as its body). Not very realistic (or good) alerts, but they will have to do for this sample.
I decided to go for derived classes despite the fact that this complicates using the control. But in the end it is actually the derived classes that should be reused, and not the DesktopAlertBase. Its purpose is to simplify the development of desktop alerts, not to be the actual alert that is used in an application. The fact is that the reason for doing this is that I didn't figure out a way to solve this with a control that could be placed inside a Window, since it is the actual window that has the desktop alert behavior. I may change this completely later, and then I'll post a new sample. :)
In Visual Studio I set up a project structure like this:
The solution is split in two projects. The first, DesktopAlert, contains the reusable custom control and its default WPF template. It handles the fading in and out of the alert window, closes the alert when it's been up for a while, and also allows the user to close the alert window.
The second project, SandboxApplication, is a demo application that contains two derived classes (ImageAlert and SimpleAlert) and a little application that launches these.
The DesktopAlertBase class is quite simple. It contains a couple of defaults for the alerts, and the necessary behavior related to opening and closing the alerts. The code for it looks like this:
1: public abstract class DesktopAlertBase : Window
2: {3: private DoubleAnimation _fadeInAnimation;
4: private DoubleAnimation _fadeOutAnimation;
5: private DispatcherTimer _activeTimer;
6: 7: static DesktopAlertBase()
8: {9: DefaultStyleKeyProperty.OverrideMetadata(typeof(DesktopAlertBase), new FrameworkPropertyMetadata(typeof(DesktopAlertBase)));
10: } 11: 12: // We need to add a public constructor so that we can set up our
13: // new window properly
14: public DesktopAlertBase()
15: { 16: Visibility = Visibility.Visible; 17: 18: // Let's set up a dummy window size
19: Width = 350; 20: Height = 75; 21: 22: // Set some default properties for the alerts.
23: // These can be changed by the derived alerts.
24: ShowInTaskbar = false;
25: WindowStyle = WindowStyle.None; 26: ResizeMode = ResizeMode.NoResize;27: Topmost = true;
28: AllowsTransparency = true;
29: Opacity = 0.8;30: BorderThickness = new Thickness(1);
31: BorderBrush = Brushes.Black; 32: Background = Brushes.White; 33: 34: // Set up the fade in and fade out animations
35: _fadeInAnimation = new DoubleAnimation();
36: _fadeInAnimation.From = 0; 37: _fadeInAnimation.To = 0.8;38: _fadeInAnimation.Duration = new Duration(TimeSpan.Parse("0:0:1.5"));
39: 40: // For the fade out we omit the from, so that it can be smoothly initiated
41: // from a fade in that gets interrupted when the user wants to close the window
42: _fadeOutAnimation = new DoubleAnimation();
43: _fadeOutAnimation.To = 0;44: _fadeOutAnimation.Duration = new Duration(TimeSpan.Parse("0:0:1.5"));
45: 46: Loaded += new RoutedEventHandler(DesktopAlertBase_Loaded);
47: } 48: 49: // When the template is applied to the control, look for a button called "PART_CloseButton".
50: // If such a button exists, hook up an event handler so that the alert can be closed when
51: // the user clicks the button.
52: public override void OnApplyTemplate()
53: {54: ButtonBase closeButton = Template.FindName("PART_CloseButton", this) as ButtonBase;
55: if (closeButton != null)
56: closeButton.Click += new RoutedEventHandler(closeButton_Click);
57: } 58: 59: // In the Loaded-event handler we need to figure out where to place the alert window.
60: // Currently they are all placed in the bottom right corner, and can not take into
61: // account other currently open alert windows.
62: void DesktopAlertBase_Loaded(object sender, RoutedEventArgs e)
63: {64: // Figure out where to place the window based on the current screen resolution
65: Rect workAreaRectangle = System.Windows.SystemParameters.WorkArea; 66: Left = workAreaRectangle.Right - Width - BorderThickness.Right; 67: Top = workAreaRectangle.Bottom - Height - BorderThickness.Bottom; 68: 69: _fadeInAnimation.Completed += new EventHandler(_fadeInAnimation_Completed);
70: 71: // Start the fade in animation
72: BeginAnimation(DesktopAlertBase.OpacityProperty, _fadeInAnimation); 73: } 74: 75: // When the fade in animation is completed, start another timer that fires an event when the
76: // window has been visible for 10 seconds
77: void _fadeInAnimation_Completed(object sender, EventArgs e)
78: {79: _activeTimer = new DispatcherTimer();
80: _activeTimer.Interval = TimeSpan.Parse("0:0:10");
81: 82: // Attach an anonymous method to the timer so that we can start fading out the alert
83: // when the timer is done.
84: _activeTimer.Tick += delegate(object obj, EventArgs ea) { FadeOut(); };
85: 86: _activeTimer.Start(); 87: } 88: 89: // Set up the fade out animation, and hook up an event handler to fire when it is completed.
90: private void FadeOut()
91: {92: // Attach an anonymous method to the Completed-event of the fade out animation
93: // so that we can close the alert window when the animation is done.
94: _fadeOutAnimation.Completed += delegate(object sender, EventArgs e) { Close(); };
95: 96: BeginAnimation(DesktopAlertBase.OpacityProperty, _fadeOutAnimation, HandoffBehavior.SnapshotAndReplace); 97: } 98: 99: // If the user clicks the close button, stop the timer that counts how long the alert
100: // window has been open, and start the fading out of the window.
101: void closeButton_Click(object sender, RoutedEventArgs e)
102: { 103: _activeTimer.Stop(); 104: FadeOut(); 105: }The template for the control (in Generic.xaml) is quite simple. It contains a template for the close button, and a template for the actual control. The control has a title area, and a content area in which the content of the derived class is displayed.
1: <ResourceDictionary
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:local="clr-namespace:DesktopAlert">
5: 6: <ControlTemplate x:Key="CloseButtonTemplate" TargetType="{x:Type Button}">
7: <Grid Background="Transparent">
8: <Viewbox>
9: <ContentPresenter/>
10: </Viewbox>
11: </Grid>
12: <ControlTemplate.Triggers>
13: <Trigger Property="IsMouseOver" Value="True">
14: <Setter Property="BitmapEffect">
15: <Setter.Value>
16: <OuterGlowBitmapEffect GlowSize="12" GlowColor="White"/>
17: </Setter.Value>
18: </Setter>
19: </Trigger>
20: <Trigger Property="IsPressed" Value="True">
21: <Setter Property="BitmapEffect">
22: <Setter.Value>
23: <OuterGlowBitmapEffect GlowSize="12" GlowColor="DarkGray"/>
24: </Setter.Value>
25: </Setter>
26: </Trigger>
27: </ControlTemplate.Triggers>
28: </ControlTemplate>
29: 30: <Style TargetType="{x:Type local:DesktopAlertBase}">
31: <Setter Property="Template">
32: <Setter.Value>
33: <ControlTemplate TargetType="{x:Type local:DesktopAlertBase}">
34: <Border Background="{TemplateBinding Background}"
35: BorderBrush="{TemplateBinding BorderBrush}"
36: BorderThickness="{TemplateBinding BorderThickness}"
37: SnapsToDevicePixels="True">
38: <Grid>
39: <Grid.RowDefinitions>
40: <RowDefinition Height="20"/>
41: <RowDefinition/>
42: </Grid.RowDefinitions>
43: <Grid Grid.Row="0" Background="LightGray">
44: <Grid>
45: <Grid.ColumnDefinitions>
46: <ColumnDefinition/>
47: <ColumnDefinition Width="20"/>
48: </Grid.ColumnDefinitions>
49: <TextBlock Grid.Column="0" Margin="5, 0, 0, 0" FontWeight="Bold" FontSize="12"
50: Opacity="1.0" Text="{TemplateBinding Title}"/>
51: <Button x:Name="PART_CloseButton"
52: Grid.Column="1"
53: HorizontalAlignment="Right" VerticalAlignment="Top"
54: Template="{StaticResource CloseButtonTemplate}"
55: Width="18" Height="18"
56: FontWeight="Bold"
57: Foreground="#FF222222"
58: >x</Button>
59: </Grid>
60: </Grid>
61: <ContentControl Grid.Row="1" Content="{TemplateBinding Content}"/>
62: </Grid>
63: </Border>
64: </ControlTemplate>
65: </Setter.Value>
66: </Setter>
67: </Style>
68: </ResourceDictionary>
The derived classes contain very little code. The SimpleAlert type code behind implements a property called Message that contains the body of the alert. The XAML for SimpleAlert looks like this:
1: <da:DesktopAlertBase x:Class="SandboxApplication.Alerts.SimpleAlert"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:da="clr-namespace:DesktopAlert;assembly=DesktopAlert"
5: Title="SimpleAlert">
6: <Grid>
7: <TextBlock FontSize="12" Padding="5" TextWrapping="Wrap"
8: Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=Message}"/>
9: </Grid>
10: </da:DesktopAlertBase>
As you can see, all it does is display a TextBlock that is data bound to the Message property. And why a relative binding? The truth is I was too tired to figure out another way of doing it at the time. :)
Let's have a look at what the alerts actually like. Here's the SimpleAlert:
Not very pretty yet, but we'll get to that in another post. And how much code was needed to launch the alert? This much:
1: SimpleAlert simpleAlert = new SimpleAlert();
2: simpleAlert.Title = "My alert has a sample title";
3: simpleAlert.Message = "This is my message! If it's long enough it will wrap. Is it long enough now?";
The ImageAlert is equally simple, so I won't post the code here. You can download it and have a look if you want.
Summing it all up
The code in this sample is by no means perfect. It's intended for exploring this way of implementing a desktop alert.
At this point I'm still not convinced that the approach outlined here is the right one, but at least it seems good enough for me now. I'll have to work on it a bit more, and try it out in a real project, before I know for sure. I'll also try to put together some proper alerts and make some nice designs for them to see what this will look like in real life. Also, this implementation doesn't yet support simultaneously appearing windows (they appear on top of each other), I hope to have time to fix that in a few days.
Please download the source code for this sample and have a look if you're interested. Please note that it comes with no warranties at all. If you like it (or do something with it), please let me know. You need Visual Studio 2008 to compile the sample (but compiled executables are included in the zip).
Hi,
Some time ago, while I was implementing Desktop Alert for my Public Folder Watcher for Outlook, I found out that there is one important feature that should be implemented in almost any kind of such alerts - such alert should not steal keyboar focus when it becomes visible... and it seems to me that you will have the same trouble with your alert...
So, just in case you will need the solution for that - feel free to email me (ImmortalRat at gmail.com) - I will try to help you with the solution...
Regards,
Dima
Posted by: Dmitrij Zaharov | Friday, December 21, 2007 at 02:10
Well, just set the window property ShowActivated to false and the problem is solved.
Posted by: Mange | Tuesday, August 26, 2008 at 09:55
Awesome work!
Posted by: fahad khalil | Friday, March 06, 2009 at 11:56
Thanks for this, very helpful and looks great!
Posted by: Daniel | Tuesday, March 31, 2009 at 06:38
Very cool stuff!!
But, it cant make more than 6 alerts windows. When I try to create the 7th, I've got an Out of Memory Exception... I tried on a 4Go ram PC after a fresh restart, same problem occured
I don't see where's the problem so far... any idea?
Posted by: Greg | Thursday, June 04, 2009 at 13:46
it looks like tha Transparency is using to much memory that my system can handle more than 6 in the same time. I tried without transparency and made 18 windows without an exception (didn't try further)
Posted by: Greg again | Friday, June 05, 2009 at 07:51
Hey nicke, is there anyway for u 2 implement this in silverlight 3,
i've been trying but some controls dont exist in WPF, and triggers are implemented differently :(
Posted by: Neil | Wednesday, July 08, 2009 at 10:47