An Outlook-style Desktop Alert control in WPF (part 1)


One frequently useful way of notifying users of information for an application is via some sort of transient popup, similar to the Microsoft Outlook Desktop Alert. There are a variety of paid-for controls out there which contain similar functionality, but I decided to build one in WPF.

Requirements

Before we start, its always best to work out exactly what we need the alerts to do.

  • Display an alert, on demand which contains, as a minimum a title and some content
  • The alert should automatically appear in the correct location – just inside the Windows system tray, irrespective of the location.
  • The alert should have a configurable opacity
  • The alert should stay visible for a configurable period of time, then disappear automatically
  • Multiple alerts can be displayed at the same time without overlapping (within reason – where there are more alerts than screen real-estate, they can overlap as necessary).
  • When the mouse cursor hovers over the alert, it should, as a minimum have an opacity of 1 to be fully visible and should not disappear
  • When the mouse moves away from the alert, the opacity should return to the configured value and any timing related to the alert disappearing should restart
  • The title can be set
  • The content can be configured and should as a minimum be some text and an icon
  • Users can close the alert prior to the planned close.
  • A configurable action can be fired when the user clicks on an alert, configurable per alert.
  • <<CONTEXT>> some way of giving context and setting options, etc.

NOTE: One of my original requirements for this alert was that it should fade-in and fade-out in the same way that the default Outlook one does. However, after doing some testing on this (and comparing it to other solutions), it appears that there are frequent OutOfMemoryExceptions thrown. Removing the fading resolves the problem, so I have removed the requirement for now until I can find a way round this.

Designing the Alert

Basic layout for the Desktop Alert

The Desktop Alert simple mockup

It makes sense to keep the content of the alert as flexible as possible. However, all alerts will we require a title area, a close button and the content area. The obvious layout is shown here.

To emphasise the title it will be shown in bold. The close button is at the far right of the title area. Taking up the rest of the control is the content area, which will be configurable.

Configuring the Content

Whilst keeping the content configurable to allow many different types of alert, for the purposes of this post I will concentrate on using a simple alert with an icon and message text. The icon will be displayed to the left of the content area (with a fixed size), with the text taking up the rest.

To build this up in WPF, it makes sense to split the control into 2 parts:

  1. A Custom Control containing the basic layout: title area, close box & content area
  2. A User Control inheriting this which has the content (icon & message).

In this way, we can just use a different User Control to display different content, keeping the same behaviour of the overall alert.

DesktopAlertBase

Add a new Custom Control to the project called DesktopAlertBase. Enter the following xaml into the Themes\Generic.xaml file:

<Style TargetType="{x:Type DesktopAlert:DesktopAlertBase}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type DesktopAlert:DesktopAlertBase}">
                    <Border>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="15"/>
                                <RowDefinition/>
                            </Grid.RowDefinitions>
                            <Grid Grid.Row="0">
                                <Grid>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition/>
                                        <ColumnDefinition Width="15"/>
                                    </Grid.ColumnDefinitions>
                                    <TextBlock Grid.Column="0" 
                                               Margin="5, 0, 0, 0" 
                                               FontWeight="Bold" 
                                               FontSize="12" 
                                               Opacity="1.0" 
                                               Text="{TemplateBinding Title}"/>
                                    <Button Grid.Column="1"
                                            HorizontalAlignment="Right"
                                            VerticalAlignment="Top"
                                            Width="15" 
                                            Height="15" 
                                            Foreground="Red">x</Button>
                                </Grid>
                            </Grid>
                            <ContentControl Grid.Row="1" Content="{TemplateBinding Content}"/>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Update the new custom control as follows:

/// <summary>Desktop alert class</summary>
public abstract class DesktopAlertBase : Window
{
	/// <summary>Initializes a new instance of the <see cref="DesktopAlertBase"/> class.</summary>
	public DesktopAlertBase()
	{
		Visibility = Visibility.Visible;

		// Set some default properties for the alerts.
		// These can be changed by the derived alerts.
		this.ShowInTaskbar = false;
		WindowStyle = WindowStyle.None;
		ResizeMode = ResizeMode.NoResize;
		Topmost = true;
		AllowsTransparency = true;
		Opacity = 0.9;
		BorderThickness = new Thickness(1);
		BorderBrush = Brushes.Black;
		Background = Brushes.White;
	}

	/// <summary>Initializes static members of the <see cref="DesktopAlertBase"/> class.</summary>
	static DesktopAlertBase()
	{
		DefaultStyleKeyProperty.OverrideMetadata(
			typeof (DesktopAlertBase), new FrameworkPropertyMetadata(typeof (DesktopAlertBase)));
	}
}

This will give us a new control, but without hooking up either the timer or the close button to close the window. To do this:

  • Amend the close button in the template and give it a name: “PART_CloseButton”
  • Add “Loaded += this.OnLoaded;” to the constructor.
  • Add the line Loaded += this.OnLoaded; to the constructor
  • Add the following code:
  • /// <summary>The timer that determines the length of time that the control is visible for</summary>
    private DispatcherTimer activeTimer;
    
    /// <summary>Hook up the button click handlers, etc</summary>
    public override void OnApplyTemplate()
    {
    	ButtonBase closeButton = Template.FindName("PART_CloseButton", this) as ButtonBase;
    	if (closeButton != null)
    	{
    		closeButton.Click += this.OnCloseButtonClick;
    	}
    }
    
    /// <summary>
    /// If the user clicks the close button, stop the timer that counts how long the alert and simply close it
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
    private void OnCloseButtonClick(object sender, RoutedEventArgs e)
    {
    	this.activeTimer.Stop();
    	Close();
    }
    
    /// <summary>
    /// Called when the alert is loaded. Kicks off the time to hide the control after the requisite time
    /// </summary>
    /// <param name="sender">The sender.</param>
    /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> instance containing the event data.</param>
    private void OnLoaded(object sender, RoutedEventArgs e)
    {
    	this.activeTimer = new DispatcherTimer { Interval = TimeSpan.Parse("0:0:05") };
    
    	// Attach an anonymous method to the timer so that we can start fading out the alert when the timer is done.
    	this.activeTimer.Tick += delegate { Close(); };
    	this.activeTimer.Start();
    }
    

    Text Alerts

    Now we have the basis of the alert, we can create a new User Control to implement a simple text style alert. To do this, add a new User Control called SimpleControl. Add the following code for the xaml & c#:

        /// <summary>
        /// Interaction logic for SimpleAlert.xaml
        /// </summary>
        public partial class SimpleAlert
        {
            public static DependencyProperty MessageProperty = DependencyProperty.Register(
                "Message",
                typeof(string),
                typeof(SimpleAlert));
    
            public static DependencyProperty ImageProperty = DependencyProperty.Register(
                "Image",
                typeof(ImageSource),
                typeof(SimpleAlert));
    
            [Bindable(true)]
            public string Message
            {
                get { return (string)GetValue(MessageProperty); }
                set { SetValue(MessageProperty, value); }
            }
    
            [Bindable(true)]
            public ImageSource Image
            {
                get { return (ImageSource) GetValue(ImageProperty); }
                set { SetValue(ImageProperty, value); }
            }
    
            public SimpleAlert()
            {
                InitializeComponent();
            }
        }
    
    <!-- XAML for the user control content --> 
        <Grid Background="White">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="40"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Image Source="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=Image}"/>
            <TextBlock FontSize="12" 
                       Padding="5" 
                       TextWrapping="Wrap"
                       Grid.Column="1"
                       Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=Message}"/>
        </Grid>
    
    

    You will also need to change the XAML definition from a UserControl to a DesktopAlertBase. Now we have this, we can create the alert from the MainWindow.cs and position it in the relevant place on the screen:

        public partial class MainWindow : Window
        {
            private SimpleAlert alert;
            public MainWindow()
            {
                InitializeComponent();
                alert = new SimpleAlert
                            {
                                Title = "Simple Alert",
                                Message =
                                    "This is the text for the simple alert popup, which should wrap if too long to display on a single line",
                                Width = 300,
                                Height = 75,
                };
                alert.Left = SystemParameters.WorkArea.Right - alert.Width - alert.BorderThickness.Right;
                alert.Top = SystemParameters.WorkArea.Bottom - (alert.Height + alert.BorderThickness.Bottom);
            }
        }
    
    

    And there we have it – the first desktop alert. In the next posts on the subject I will go into more detail on the images, styling of the control, positioning, displaying multiple alerts simultaneously and user interactions & effects (ie mouseover, context menus, etc).

, , ,

  1. #1 by user on April 23, 2012 - 10:01 am

    Can you share the complete source code for this.. I am have bit problem with above code

    • #2 by davefielding76 on April 23, 2012 - 10:54 pm

      I intend to include the full source code once I get to the end of the series. I will try to get everything published as soon as possible.

Leave a comment