Create popup "toaster" notifications in Windows with .NET

.NetWpfWindowsDesktop

.Net Problem Overview


I am using .NET and am creating a desktop app/service that will display notifications in the corner of my Desktop when certain events are triggered. I don't want to use a regular Message Box b/c that would be too intrusive. I want notifications to slide into view and then fade out after a few seconds. I am thinking of something that will act very much like the Outlook alerts that one gets when a new message arrives. The question is: Should I use WPF for this? I've never done anything with WPF but will happily try it if that's best means to the end. Is there a way to accomplish this with regular .NET libraries?

.Net Solutions


Solution 1 - .Net

WPF makes this absolutely trivial: It would proably take ten minutes or less. Here are the steps:

  1. Create a Window, set AllowsTransparency="true" and add a Grid to it
  2. Set the Grid's RenderTransform to a ScaleTransform with origin of 0,1
  3. Create an animation on the grid that animates the ScaleX 0 to 1 then later animates the Opacity from 1 to 0
  4. In the constructor calculate Window.Top and Window.Left to place the window in the lower right-hand corner of the screen.

That's all there is to it.

Using Expression Blend it took about 8 minutes me to generate the following working code:

<Window
    x:Class="NotificationWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Notification Popup" Width="300" SizeToContent="Height"
  WindowStyle="None" AllowsTransparency="True" Background="Transparent">

  <Grid RenderTransformOrigin="0,1" >

    <!-- Notification area -->
    <Border BorderThickness="1" Background="Beige" BorderBrush="Black" CornerRadius="10">
      <StackPanel Margin="20">
        <TextBlock TextWrapping="Wrap" Margin="5">
          <Bold>Notification data</Bold><LineBreak /><LineBreak />
          Something just happened and you are being notified of it.
        </TextBlock>
        <CheckBox Content="Checkable" Margin="5 5 0 5" />
        <Button Content="Clickable" HorizontalAlignment="Center" />
      </StackPanel>
    </Border>

    <!-- Animation -->
    <Grid.Triggers>
      <EventTrigger RoutedEvent="FrameworkElement.Loaded">
        <BeginStoryboard>
          <Storyboard>
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)">
              <SplineDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
              <SplineDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/>
            </DoubleAnimationUsingKeyFrames>
            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)">
              <SplineDoubleKeyFrame KeyTime="0:0:2" Value="1"/>
              <SplineDoubleKeyFrame KeyTime="0:0:4" Value="0"/>
            </DoubleAnimationUsingKeyFrames>
          </Storyboard>
        </BeginStoryboard>
      </EventTrigger>
    </Grid.Triggers>
  
    <Grid.RenderTransform>
      <ScaleTransform ScaleY="1" />
    </Grid.RenderTransform>

  </Grid>
  
</Window>

With code behind:

using System;
using System.Windows;
using System.Windows.Threading;

public partial class NotificationWindow
{
  public NotificationWindow()
  {
    InitializeComponent();

    Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() =>
    {
      var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
      var transform = PresentationSource.FromVisual(this).CompositionTarget.TransformFromDevice;
      var corner = transform.Transform(new Point(workingArea.Right, workingArea.Bottom));

      this.Left = corner.X - this.ActualWidth - 100;
      this.Top = corner.Y - this.ActualHeight;
    }));
  }
}

Since WPF is one of the regular .NET libraries, the answer is yes, it is possible to accomplish this with the "regular .NET libraries".

If you're asking if there is a way to do this without using WPF the answer is still yes, but it is extremely complex and will take more like 5 days than 5 minutes.

Solution 2 - .Net

I went ahead and created a CodePlex site for this that includes "Toast Popups" and control "Help Balloons". These versions have more features than what's described below. https://toastspopuphelpballoon.codeplex.com.

This was a great jumping off point for the solution that I was looking for. I've made a couple of modifications to meet my requirements:

  • I wanted to stop the animation on mouse over.
  • "Reset" animation when mouse leave.
  • Close the Window when opacity reached 0.
  • Stack the Toast (I have not solved the problem if the number of windows exceeds the screen height)
  • Call Load from my ViewModel

Here's my XAML

<Window x:Class="Foundation.FundRaising.DataRequest.Windows.NotificationWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="NotificationWindow" Height="70" Width="300" ShowInTaskbar="False"
    WindowStyle="None" AllowsTransparency="True" 
    Background="Transparent">

<Grid RenderTransformOrigin="0,1" >
    <Border BorderThickness="2" Background="{StaticResource GradientBackground}" BorderBrush="DarkGray" CornerRadius="7">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="60"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="24"/>
            </Grid.ColumnDefinitions>
        
            <Grid.RowDefinitions>
                <RowDefinition Height="30"/>
                <RowDefinition Height="*"/>
            </Grid.RowDefinitions>
            
            <Image Grid.Column="0" 
                   Grid.RowSpan="2" 
                   Source="Resources/data_information.png" 
                   Width="40" Height="40" 
                   VerticalAlignment="Center" 
                   HorizontalAlignment="Center"/>

            <Image Grid.Column="2" 
                   Source="Resources/error20.png"
                   Width="20" 
                   Height="20" 
                   VerticalAlignment="Center" 
                   ToolTip="Close"
                   HorizontalAlignment="Center" 
                   Cursor="Hand" MouseUp="ImageMouseUp"/>

            <TextBlock Grid.Column="1" 
                       Grid.Row="0"
                       VerticalAlignment="Center"
                       HorizontalAlignment="Center"
                       FontWeight="Bold" FontSize="15"
                       Text="A Request has been Added"/>
            
            <Button Grid.Column="1"
                    Grid.Row="1"
                    FontSize="15"
                    Margin="0,-3,0,0"
                    HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Content="Click Here to View" 
                    Style="{StaticResource LinkButton}"/>
        </Grid>            
    </Border>

    <!-- Animation -->
    <Grid.Triggers>
        <EventTrigger RoutedEvent="FrameworkElement.Loaded">
            <BeginStoryboard x:Name="StoryboardLoad">
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" From="0.0" To="1.0" Duration="0:0:2" />
                    <DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" From="1.0" To="0.0" Duration="0:0:8" BeginTime="0:0:5" Completed="DoubleAnimationCompleted"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>

        <EventTrigger RoutedEvent="Mouse.MouseEnter">
            <EventTrigger.Actions>
                <RemoveStoryboard BeginStoryboardName="StoryboardLoad"/>
                <RemoveStoryboard BeginStoryboardName="StoryboardFade"/>
            </EventTrigger.Actions>
        </EventTrigger>

        <EventTrigger RoutedEvent="Mouse.MouseLeave">
            <BeginStoryboard x:Name="StoryboardFade">
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" From="1.0" To="0.0" Duration="0:0:8" BeginTime="0:0:2" Completed="DoubleAnimationCompleted"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>

    </Grid.Triggers>

    <Grid.RenderTransform>
        <ScaleTransform ScaleY="1" />
    </Grid.RenderTransform>
</Grid>

The Code Behind

public partial class NotificationWindow : Window
{
    public NotificationWindow()
        : base()
    {
        this.InitializeComponent();
        this.Closed += this.NotificationWindowClosed;
    }

    public new void Show()
    {
        this.Topmost = true;
        base.Show();

        this.Owner = System.Windows.Application.Current.MainWindow;
        this.Closed += this.NotificationWindowClosed;
        var workingArea = Screen.PrimaryScreen.WorkingArea;

        this.Left = workingArea.Right - this.ActualWidth;
        double top = workingArea.Bottom - this.ActualHeight;

        foreach (Window window in System.Windows.Application.Current.Windows)
        {                
            string windowName = window.GetType().Name;

            if (windowName.Equals("NotificationWindow") && window != this)
            {
                window.Topmost = true;
                top = window.Top - window.ActualHeight;
            }
        }

        this.Top = top;
    }
    private void ImageMouseUp(object sender, 
        System.Windows.Input.MouseButtonEventArgs e)
    {
        this.Close();
    }

    private void DoubleAnimationCompleted(object sender, EventArgs e)
    {
        if (!this.IsMouseOver)
        {
            this.Close();
        }
    }
}

The call from the ViewModel:

    private void ShowNotificationExecute()
    {
        App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(
            () =>
            {
                var notify = new NotificationWindow();
                notify.Show();
            }));
    }

The Styles referenced in the XAML:

     <Style x:Key="LinkButton" TargetType="Button">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <TextBlock>
                        <ContentPresenter />
                    </TextBlock>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="Foreground" Value="Blue"/>
        <Setter Property="Cursor" Value="Hand"/>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="ContentTemplate">
                    <Setter.Value>
                        <DataTemplate>
                            <TextBlock TextDecorations="Underline" Text="{TemplateBinding Content}"/>
                        </DataTemplate>
                    </Setter.Value>
                </Setter>
            </Trigger>
        </Style.Triggers>
    </Style>

    <LinearGradientBrush x:Key="GradientBackground" EndPoint="0.504,1.5" StartPoint="0.504,0.03">
        <GradientStop Color="#FFFDD5A7" Offset="0"/>
        <GradientStop Color="#FFFCE79F" Offset="0.567"/>
    </LinearGradientBrush>

UPDATE: I added this event handler when the form is closed to "drop" the other windows.

    private void NotificationWindowClosed(object sender, EventArgs e)
    {
        foreach (Window window in System.Windows.Application.Current.Windows)
        {
            string windowName = window.GetType().Name;

            if (windowName.Equals("NotificationWindow") && window != this)
            {
                // Adjust any windows that were above this one to drop down
                if (window.Top < this.Top)
                {
                    window.Top = window.Top + this.ActualHeight;
                }
            }
        }
    }

Solution 3 - .Net

public partial class NotificationWindow : Window
{
    DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer();
    public NotificationWindow()
        : base()
    {
        this.InitializeComponent();
        
        Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() =>
        {
            var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
            var transform = PresentationSource.FromVisual(this).CompositionTarget.TransformFromDevice;
            var corner = transform.Transform(new Point(workingArea.Right, workingArea.Bottom));

            this.Left = corner.X - this.ActualWidth;
            this.Top = corner.Y - this.ActualHeight;
        }));
        timer.Interval = TimeSpan.FromSeconds(4d);
        timer.Tick += new EventHandler(timer_Tick);
    }
    public new void Show()
    {
        base.Show();
        timer.Start();
    }

    void timer_Tick(object sender, EventArgs e)
    {
        //set default result if necessary

        timer.Stop();
        this.Close();
    }

}

The above code is refined version @Ray Burns approach. Added with time interval code. So that Notification window would close after 4 seconds..

Call the Window as,

NotificationWindow nfw = new NotificationWindow();
nfw.Show();

Solution 4 - .Net

I used the answer above to design my own notification window that is a bit more user friendly, in my opinion, and uses a few techniques that took me a little while to figure out. Sharing in case it helps anyone else out there.:

  1. Added MouseEnter event trigger to immediately set window opacity to 1 so a user does not have to wait for the window to fade into full view.
  2. Added MouseLeave event trigger to fade the window opacity to 0 when the user moves the mouse out of the window.
  3. Added MouseUp (mouse click) event trigger to immediately set window opacity to 0 to hide the notification window.
  4. If you have to Show() and Hide() the notification window multiple times, then below method also resets the Storyboard at the end so that the next Show() operation starts the operation from the start and there is no glitch in the animation.

XAML:

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Class="ToastNotificationWindow"
    Title="Notification Popup"
    Width="480"
    Height="140"
    WindowStyle="None"
    AllowsTransparency="True"
    Background="Transparent"
    BorderThickness="0"
    Topmost="True"
>


    <Grid Background="Transparent" Name="ToastWindowGrid" RenderTransformOrigin="0,1">
        
        <Border Name="ToastWindowBorder" BorderThickness="0" Background="#333333">
            
            <StackPanel Name="ToastWindowStackPanel" Margin="10" Orientation="Horizontal">

                <Image Name="ToastLogo" Width="100" Height="100" Source="D:\Development\ToastNotification\resources\Logo-x100.png"/>

                <StackPanel Name="ToastMessageStackPanel" Width="359">
                
                    <TextBox Name="ToastTitleTextBox" Margin="5" MaxWidth="340" Background="#333333" BorderThickness="0" IsReadOnly="True" Foreground="White" FontSize="20" Text="Toast Title" FontWeight="Bold" HorizontalContentAlignment="Center" Width="Auto" HorizontalAlignment="Stretch" IsHitTestVisible="False"/>
                
                    <TextBox Name="ToastMessageTextBox" Margin="5" MaxWidth="340" Background="#333333" BorderThickness="0" IsReadOnly="True" Foreground="LightGray" FontSize="16" Text="Toast title message. Click to start something." HorizontalContentAlignment="Left" TextWrapping="Wrap" IsHitTestVisible="False"/>
            
                </StackPanel>
            
            </StackPanel>
        
        </Border>

        <Grid.Triggers>

            <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                <EventTrigger.Actions>
                    <BeginStoryboard Name="StoryboardLoad">
                        <Storyboard Name="ToastAnimationStoryboard">
                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)" FillBehavior="HoldEnd">
                                <SplineDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
                                <SplineDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/>
                            </DoubleAnimationUsingKeyFrames>
                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" FillBehavior="HoldEnd">
                                <SplineDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
                                <SplineDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/>
                            </DoubleAnimationUsingKeyFrames>
                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" FillBehavior="HoldEnd">
                                <SplineDoubleKeyFrame KeyTime="0:0:20" Value="1"/>
                                <SplineDoubleKeyFrame KeyTime="0:0:23" Value="0"/>
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger.Actions>
            </EventTrigger>

            <EventTrigger RoutedEvent="Mouse.MouseEnter">
                <EventTrigger.Actions>
                    <StopStoryboard BeginStoryboardName="StoryboardMouseLeaveFadeOut"/>
                    <BeginStoryboard Name="StoryboardMouseEnterFadeIn">
                        <Storyboard Name="ToastAnimationStoryboardMouseEnterFadeIn">
                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" FillBehavior="HoldEnd">
                                <DiscreteDoubleKeyFrame KeyTime="0:0:0.1" Value="1"/>
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger.Actions>
            </EventTrigger>

            <EventTrigger RoutedEvent="Mouse.MouseLeave">
                <EventTrigger.Actions>
                    <RemoveStoryboard BeginStoryboardName="StoryboardMouseLeaveFadeOut"/>
                    <BeginStoryboard Name="StoryboardMouseLeaveFadeOut">
                        <Storyboard Name="ToastAnimationStoryboardMouseLeaveFadeOut">
                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" FillBehavior="HoldEnd">
                                <SplineDoubleKeyFrame KeyTime="0:0:0" Value="1"/>
                                <SplineDoubleKeyFrame KeyTime="0:0:3" Value="0"/>
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger.Actions>
            </EventTrigger>

            <EventTrigger RoutedEvent="Mouse.MouseUp">
                <EventTrigger.Actions>
                    <StopStoryboard BeginStoryboardName="StoryboardMouseEnterFadeIn"/>
                    <RemoveStoryboard BeginStoryboardName="StoryboardMouseEnterFadeIn"/>
                    <StopStoryboard BeginStoryboardName="StoryboardMouseLeaveFadeOut"/>
                    <RemoveStoryboard BeginStoryboardName="StoryboardMouseLeaveFadeOut"/>
                    <BeginStoryboard Name="StoryboardMouseClickFadeOut">
                        <Storyboard Name="ToastAnimationStoryboardMouseClickFadeOut">
                            <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" FillBehavior="HoldEnd">
                                <DiscreteDoubleKeyFrame KeyTime="0:0:0.1" Value="0"/>
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                    <SeekStoryboard BeginStoryboardName="StoryboardLoad"/>
                    <PauseStoryboard BeginStoryboardName="StoryboardLoad"/>
                    </EventTrigger.Actions>
            </EventTrigger>
        </Grid.Triggers>

        <Grid.RenderTransform>
            <ScaleTransform ScaleY="1" />
        </Grid.RenderTransform>
    
    </Grid>

</Window>

Code behind:

using System;
using System.Windows;
using System.Windows.Threading;

public partial class ToastNotificationWindow
{
    public ToastNotificationWindow()
    {
        InitializeComponent();

        Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() =>
        {
            var workingArea = System.Windows.SystemParameters.WorkArea;
            var transform = PresentationSource.FromVisual(this).CompositionTarget.TransformFromDevice;
            var corner = transform.Transform(new Point(workingArea.Right, workingArea.Bottom));

            this.Left = corner.X - this.ActualWidth - 10;
            this.Top = corner.Y - this.ActualHeight;
        }));
    }
}

Solution 5 - .Net

NotifyIcon notifyIcon = new NotifyIcon();
Stream iconStream = System.Windows.Application.GetResourceStream(new Uri("pack://application:,,,/Assets/ic_instant_note_tray.ico")).Stream;
notifyIcon.Icon = new System.Drawing.Icon(iconStream);
notifyIcon.Text = string.Format(Properties.Resources.InstantNoteAppName, Constants.Application_Name);
notifyIcon.Visible = true;
notifyIcon.ShowBalloonTip(5000, "tooltiptitle", "tipMessage", ToolTipIcon.Info);
notifyIcon.Visible = false;
notifyIcon.Dispose();

Solution 6 - .Net

Note that the calling thread must be sta because many ui components require this, while writing following code under system.timers.timer elapsed event

Window1 notifyWin = new Window1();
bool? isOpen = notifyWin.ShowDialog();
if (isOpen != null && isOpen == true)
{
     notifyWin.Close();
}
System.Threading.Thread.Sleep(1000);
notifyWin.ShowDialog();

under window1 constructor:

public Window1()
{
    InitializeComponent();

    Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() => { 
        var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea; 
        var transform = PresentationSource.FromVisual(this).CompositionTarget.TransformFromDevice; 
        var corner = transform.Transform(new Point(workingArea.Right, workingArea.Bottom)); 
        this.Left = corner.X - this.ActualWidth - 100; 
        this.Top = corner.Y - this.ActualHeight; 
    })); 
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAntonyView Question on Stackoverflow
Solution 1 - .NetRay BurnsView Answer on Stackoverflow
Solution 2 - .NetLawManView Answer on Stackoverflow
Solution 3 - .NetAnees DeenView Answer on Stackoverflow
Solution 4 - .Netuser1367200View Answer on Stackoverflow
Solution 5 - .NetMehul SantView Answer on Stackoverflow
Solution 6 - .NetPKBhaiyaView Answer on Stackoverflow