Silverlight 4 Rough Notes: Notification Windows

Note – these posts are put together after a short time with Silverlight 4 as a way of providing pointers to some of the new features that Silverlight 4 has to offer. I’m posting these from the PDC as Silverlight 4 is announced for the first time so please bear that in mind when working through these posts.

Quite often when you’re working in an application its window gets pushed to the back of the Z-order somewhere and yet it wants to notify you that something has happened in that app. One of the ways of doing that is by displaying a notification window ( commonly in the bottom right hand corner of the screen ). Outlook can optionally do this when mail arrives and Live Messenger can do it when contacts sign in. I’ve often heard these notification windows referred to as “Toast” 🙂

In Silverlight 4, applications that are running Out-Of-Browser have the ability to raise these kinds of notifications via a new NotificationWindow class.

The new class is pretty simple to use. It has a single Content property of type FrameworkElement which means you can assign pretty much any content you like to it. You then ask the notification to display and it pops up, displays its content for a little while and then fades out. For example;

      NotificationWindow notify = new NotificationWindow();

      // Note - I don't seem to be able to get the child Content to stretch unless
      // I explicitly set Width and Height on it right now.
      notify.Content = new NotifyContent()
      {
        DisplayText = "Hello World",
        Width = 400,                  // Both default and max value
        Height = 100                  // Both default and max value
      };
      notify.Show(2000);

If I run that code in the browser then it’ll fail but if I run it out of browser it works fine. Note that the 2000 is the duration of the notification on-screen and that NotifyContent is just a simple UserControl of my own made up of a little bit of XAML

<UserControl x:Class="SilverlightApplication19.NotifyContent"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    HorizontalAlignment="Stretch"
    VerticalAlignment="Stretch"
    d:DesignHeight="300" d:DesignWidth="400">    
    <Grid x:Name="LayoutRoot" Background="Azure"
          HorizontalAlignment="Stretch"
          VerticalAlignment="Stretch">
        <Viewbox>
            <TextBlock
                Text="{Binding DisplayText}" />
        </Viewbox>
    </Grid>
</UserControl>

and a little bit of code;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;

namespace SilverlightApplication19
{
  public partial class NotifyContent : UserControl, INotifyPropertyChanged
  {
    public NotifyContent()
    {
      InitializeComponent();

      this.Loaded += (s, e) =>
        {
          this.DataContext = this;
        };
    }
    public string DisplayText
    {
      get
      {
        return (displayText);
      }
      set
      {
        displayText = value;
        FirePropertyChanged("DisplayText");
      }
    }
    void FirePropertyChanged(string property)
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs(property));
      }
    }

    string displayText;
    public event PropertyChangedEventHandler PropertyChanged;
  }
}

but it could be any arbitrary content. What shows up in my UI when I run this code is a little toast window;

image

Now, I think there are a bunch of restrictions around what you can do in that toast window so I wouldn’t try and get too fancy around things like;

  • trying to display a transparent notification ( or semi-transparent )
  • trying to get away from the square corners on the notification window
  • displaying more than one notification at a time ( last one wins AFAIK )

and there are no doubt a bunch more.

However, you can get a basic notification and it can display UI and respond to events on it. Imagine that I’ve got some Silverlight application that is generating events every 10 seconds or so and wants to notify the user of that. I’ll simulate that by using a timer and I’ll make it a navigation application.

Here’s the interaction that I want to create. I have an OOB application displaying a page like this one;

image

and every 10 seconds a notification window pops up;

image

showing that there’s some new item of interest. Clicking on the Hyperlink at the bottom of the notification window should cause the main window to navigate to that item of interest as in;

image

and then clicking on the Return button in the main UI will navigate back to the home page again;

image

Putting that together. I made a simple out-of-browser application with a navigation frame inside of MainPage.xaml as below;

<UserControl x:Class="SilverlightApplication19.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400"
    xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation">
    <navigation:Frame
        Name="navFrame" 
        Source="/Pages/BlankPage.xaml"/>

</UserControl>

with some code behind;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Threading;
using System.Windows.Navigation;

namespace SilverlightApplication19
{
  public partial class MainPage : UserControl
  {
    public MainPage()
    {
      InitializeComponent();

      if (Application.Current.IsRunningOutOfBrowser)
      {
        this.Loaded += (s, e) =>
          {
            Timer timer = new Timer(OnTimerTick);
            timer.Change(10000, 10000);
          };
      }
    }
    void OnTimerTick(object parameter)
    {
      Dispatcher.BeginInvoke(() =>
        {
          ++itemNumber;

          NotificationWindow notify = new NotificationWindow();

          // Note - I don't seem to be able to get the child Content to stretch unless
          // I explicitly set Width and Height on it right now.
          notify.Content = new NotifyContent()
          {
            DisplayText = string.Format("Jump to item {0}", itemNumber),

            Command = new AlwaysExecuteDelegateCommand((o) =>
              {
                navFrame.Navigate(new Uri(string.Format("/Pages/ItemPage.xaml?pageNo={0}",
                  itemNumber), UriKind.Relative));

                notify.Close();
              }),

            Width = 400,                  // Both default and max value
            Height = 100                  // Both default and max value
          };
          notify.Show(2000);
        });
    }
    int itemNumber;
  }
}

for an out-of-browser situation, this code is setting up a Timer to fire every 10 seconds. When that Timer ticks, we increment an itemNumber and then we go and show a NotificationWindow with a fairly simple UserControl in it called NotifyContent. There are 2 key properties that we set on that control – DisplayText which is the text it displays and Command which is basically a piece of code to run when someone clicks on a HyperlinkButton in that UI. Here’s the XAML for the NotifyContent control;

<UserControl x:Class="SilverlightApplication19.NotifyContent"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    HorizontalAlignment="Stretch"
    VerticalAlignment="Stretch"
    d:DesignHeight="300" d:DesignWidth="400">    
    <Grid x:Name="LayoutRoot"
          HorizontalAlignment="Stretch"
          VerticalAlignment="Stretch">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Viewbox>
            <TextBlock
                Text="{Binding DisplayText}" />
        </Viewbox>
        <HyperlinkButton    
            Command="{Binding Command}"
            Margin="5"
            Grid.Row="1"
            NavigateUri="{Binding NavUri}"
            Content="Jump to Notified Item" />
    </Grid>
</UserControl>

and here’s the code behind;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Windows.Navigation;

namespace SilverlightApplication19
{
  public partial class NotifyContent : UserControl, INotifyPropertyChanged
  {
    public NotifyContent()
    {
      InitializeComponent();

      this.Loaded += (s, e) =>
        {
          this.DataContext = this;
        };
    }
    public string DisplayText
    {
      get
      {
        return (displayText);
      }
      set
      {
        displayText = value;
        FirePropertyChanged("DisplayText");
      }
    }
    public ICommand Command
    {
      get
      {
        return (command);
      }
      set
      {
        command = value;
        FirePropertyChanged("Command");
      }
    }
    void FirePropertyChanged(string property)
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs(property));
      }
    }  
    ICommand command;
    string displayText;
    public event PropertyChangedEventHandler PropertyChanged;
  }
}

and I also made use of my own AlwaysExecuteDelegateCommand which is just an ICommand implementation that executes the command by calling a supplied delegate ( and always returns True for CanExecute );

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace SilverlightApplication19
{
  public class AlwaysExecuteDelegateCommand : ICommand
  {
    public event EventHandler CanExecuteChanged;

    public AlwaysExecuteDelegateCommand(Action<object> func)
    {
      this.func = func;
    }
    public bool CanExecute(object parameter)
    {
      return (true);
    }
    public void Execute(object parameter)
    {      
      func(parameter);
    }
    Action<object> func;
  }
}

So, hopefully, you can see that what’s going on here is that every 10 seconds we create a notification. In that notification we display a piece of text and a HyperlinkButton. The HyperlinkButton has its Command bound to a piece of code which ultimately asks the main navigation frame to navigate to a particular /Pages/ItemPage.xaml?pageNo={value} URI and by doing that we get the NotificationWindow to cause the navigation in the main UI to happen.

Note  – I didn’t start via this route. I started by simply asking the HyperlinkButton in the NotificationWindow to navigate to /Pages/ItemPage.xaml?pageNo={value} for me but I found that caused a new browser window to open rather than causing my main UI to navigate. At the time of writing I suspect there’s a better way of doing what I want ( or there’s a bug ) so I went down this route.

For completeness, I also have a couple more pages – BlankPage.xaml and ItemPage.xaml both contained in a Pages sub-folder.

Here is BlankPage.xaml;

<navigation:Page x:Class="SilverlightApplication19.Pages.BlankPage"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                 xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                 mc:Ignorable="d"
                 xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
                 d:DesignWidth="640"
                 d:DesignHeight="480"
                 Title="MainPage Page">
  <Grid x:Name="LayoutRoot">
        <Viewbox>
            <TextBlock
                Text="Home Page" />
        </Viewbox>
    </Grid>
</navigation:Page>

and here is ItemPage.xaml;

<navigation:Page
    x:Class="SilverlightApplication19.Pages.ItemPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
    d:DesignWidth="640"
    d:DesignHeight="480"
    Title="ItemPage Page">
    <Grid
        x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition
                Height="*" />
            <RowDefinition
                Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="48"
            Text="{Binding PageNo}" />
        <Button
            Grid.Row="1"
            Content="Return"
            Margin="5"
            Click="OnReturnToMainPage" />
    </Grid>
</navigation:Page>

and the code behind which simply sets up a PageNo property for the UI to bind to by extracting the pageNo parameter from the query string and also provides a Return button by using the NavigationService to navigate back to the /Pages/BlankPage.xaml page;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Navigation;
using System.ComponentModel;

namespace SilverlightApplication19.Pages
{
  public partial class ItemPage : Page, INotifyPropertyChanged
  {
    public ItemPage()
    {
      InitializeComponent();

      this.Loaded += (s, e) =>
        {
          this.DataContext = this;
        };
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
      // no error handling...
      PageNo = int.Parse(NavigationContext.QueryString["pageNo"]);
    }

    public int PageNo
    {
      get
      {
        return (pageNo);
      }
      set
      {
        pageNo = value;
        FirePropertyChanged("PageNo");
      }
    }

    void FirePropertyChanged(string property)
    {
      if (PropertyChanged != null)
      {
        PropertyChanged(this, new PropertyChangedEventArgs(property));
      }
    }   
    void OnReturnToMainPage(object sender, RoutedEventArgs e)
    {
      NavigationService.Navigate(new Uri("/Pages/BlankPage.xaml", UriKind.Relative));
    }
    int pageNo;
    public event PropertyChangedEventHandler PropertyChanged;
  }
}

The only other thing that occurs to me is that you might sometimes want to bring the main window of the application to the foreground when a notification is actioned by a user. I could have made this happen by making sure that I called;

Application.Current.MainWindow.Activate()

when the user clicked on the notification window ( i.e. changing my TimerTick function which causes the NotificationWindow to be displayed and provides a ICommand for the contained HyperlinkButton ) to be;

    void OnTimerTick(object parameter)
    {
      Dispatcher.BeginInvoke(() =>
        {
          ++itemNumber;

          NotificationWindow notify = new NotificationWindow();

          // Note - I don't seem to be able to get the child Content to stretch unless
          // I explicitly set Width and Height on it right now.
          notify.Content = new NotifyContent()
          {
            DisplayText = string.Format("Jump to item {0}", itemNumber),

            Command = new AlwaysExecuteDelegateCommand((o) =>
              {
                navFrame.Navigate(new Uri(string.Format("/Pages/ItemPage.xaml?pageNo={0}",
                  itemNumber), UriKind.Relative));

                Application.Current.MainWindow.Activate();

                notify.Close();
              }),

            Width = 400,                  // Both default and max value
            Height = 100                  // Both default and max value
          };
          notify.Show(2000);
        });
    }
 

Great stuff – I think this is another one of those “line of business” scenarios that brings Silverlight 4 applications closer to being able to meet the requirements of a whole class of applications that Silverlight 3 can’t quite get to.