Archive

Archive for July, 2011

What’s new in Silverlight 5 update….

I had a great time speaking with the WNC Developers Guild in Asheville. Here are the slides and a link to the code from my presentation.

Categories: English Tags: ,

What’s new in Silverlight 5

July 18, 2011 1 comment

I will be doing a presentation on Thursday, July 21 at the WNC .NET Developers Guild. Here is a summary of the presentation:

Silverlight 5 has quite a lot of new powerful features. We will take a look at what is currently available in the Silverlight 5 Beta and also review and discuss what will be available when Silverlight 5 is released later this year. Some of the new features hopefully will be rolled back in to the v.Next version of .NET so that we can pick them up in WPF as well.

Looking forward to it!

Categories: English Tags: , ,

Building a better mouse trap…Notification Windows

July 1, 2011 2 comments

If you have played with Silverlight 4, you probably have come across the NotificationWindow. I think it is a great idea. Unfortunately, I think it is too limited for real world scenarios. The only blog post that really tries to make it something that could possibly be useful in a Silverlight asynchronous world is this one by Tim Heuer. If you are interested go ahead and read what he proposes. Back already? I think that takes us a little closer but what I am really looking for is a notification system that allows me to have multiple notifications at a time like that of Outlook or a chat program.

Here is what I am looking for, what I really want is a system like Growl or any of its kind. I want it to be able to run from either in the browser or out of th browser for Silverlight applications. Let’s fast forward and take a look at what I am proposing and the solution that I have come up with. Let me introduce you to the Notification Manager (really great on names right?):

Of course the sample application is only to demonstrate the how the notification system works in Silverlight. Let’s look at the folder structure that makes up this solution:

Don’t worry we will walk through everything that is here and get a good understanding as to what is going on before we finish. For the purpose of this blog post, I have put everything under a single project but normally I would separate this out into its own project, e.g. Core. That is why I have a folder with the name Core and all of the pertinent objects fall under it.

First let’s discuss some of the requirement I had in mind and some of the decisions I made to make this happen:

  • I wanted to have multiple notification windows visible at any time.
  • I wanted to be able to control whether the notifications would auto-hide or not.
  • I wanted to style my notification system like that of the Notification Window.
  • I wanted the notifications to come up in my applications in the bottom right side of the application.
  • I wanted the notifications to stack up from the bottom to the top of the application.
  • If the notifications went past the top of the applications, I wanted to be able to scroll through them.
  • I wanted to be able to close any notification on demand.
  • I wanted to use an API that could potentially be used in a single line of code.

This example uses Caliburn.Micro for event aggregation and MEF for composition. Feel free to use any framework or tools that you are comfortable with to modify this. I wanted to show you a functional example using Caliburn.Micro so that you could just use it as is and port it into your own projects if you wanted to.

Now that you know a little of the requirments that I had, let’s take a look and see what I ended up doing to satisfy those requirements. First off, I created two controls: NotificationControl and NotificationManager.

Let’s look at the NotificationManager first:

namespace Core.Controls
{
    using System;
    using System.Collections;
    using System.Collections.Specialized;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Controls.Primitives;
    using System.Windows.Data;
    using System.Windows.Media.Imaging;
    using Core.Models;

    public class NotificationManager : DockPanel
    {
        #region Dependency Properties

        #region Interval

        public static readonly DependencyProperty IntervalProperty = DependencyProperty.Register(
            "Interval",
            typeof(int),
            typeof(NotificationManager),
            new PropertyMetadata(0));

        ///
<summary> /// Gets or sets a value that indicates whether
 /// the field is required.
 /// </summary>
        public int Interval
        {
            get { return (int)GetValue(NotificationManager.IntervalProperty); }
            set { SetValue(NotificationManager.IntervalProperty, value); }
        }

        #endregion

        #region ItemsSource

        public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
            "ItemsSource", typeof(IEnumerable), typeof(NotificationManager), new PropertyMetadata(OnItemsSourceChanged));
        public IEnumerable ItemsSource
        {
            get { return (IEnumerable)GetValue(ItemsSourceProperty); }
            set { SetValue(ItemsSourceProperty, value); }
        }
        private static NotificationManager _notificationManager;
        private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            _notificationManager = d as NotificationManager;
            var old = e.OldValue as INotifyCollectionChanged;
            if (old != null)
            {
                old.CollectionChanged -= items_CollectionChanged;
            }
            var items = e.NewValue as INotifyCollectionChanged;
            if (items != null)
            {
                items.CollectionChanged += new NotifyCollectionChangedEventHandler(Items_CollectionChanged);
            }
        }
        static void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.Action == NotifyCollectionChangedAction.Add)
            {
                if (e.NewItems != null)
                {
                    foreach (Core.Models.NotificationModel newItem in e.NewItems)
                    {
                        var item = newItem;
                        if (item != null)
                        {
                            _notificationManager.AddNotification(item);
                        }
                    }
                }
            }
            else if (e.Action == NotifyCollectionChangedAction.Remove)
            {
                if (e.OldItems != null)
                {
                    foreach (Core.Models.NotificationModel oldItem in e.OldItems)
                    {
                        var item = oldItem;
                        if (item != null)
                        {
                            _notificationManager.RemoveNotification(item);
                        }
                    }
                }
            }
        }

        #endregion

        #endregion

        #region ctor

        public NotificationManager()
        {
            this.LastChildFill = false;
        }

        #endregion

        #region Helper Methods

        public void AddNotification(NotificationModel item)
        {
            NotificationControl nc = new NotificationControl(item, this.Interval);
            this.Children.Add(nc);
            // Wire up the closed event.
            nc.Closed += (s, earg) =>
            {
                var n = s as NotificationControl;
                if (n != null)
                {
                    var child = (from Core.Models.NotificationModel c in this.ItemsSource where c.Id == n.Id select c).FirstOrDefault();
                    if (child != null)
                    {
                        var coll = this.ItemsSource as Caliburn.Micro.BindableCollection;
                        if (coll != null)
                        {
                            // Remove from the data collection.
                            coll.Remove(child);
                        }
                    }
                }
            };
        }
        public void RemoveNotification(Core.Models.NotificationModel item)
        {
            var child = (from NotificationControl c in this.Children where c.Id == item.Id select c).FirstOrDefault();
            if (child != null)
            {
                this.Children.Remove(child);
            }
        }

        #endregion

    };
}

As you can see, there is not a lot of logic to this control. This control derives from a DockPanel. I need the DockPanel so that I don’t have to spend a lot of time computing the layout of the NotificationControl objects. I can simply use the DockPanel and set the Dock property on each object to “Bottom” and it is ready to perform bottom up style stacking.

Next, you will see that I have two dependency properties. I have an integer Interval property that lets me determine how long the NotificationControl is visible. The default value of this property is zero (0). I use this default value as a kill switch and never hide the controls.

I also have an ItemsSource property. I wanted to have this since I wanted to be able to have the notifications added to the manager like that of a ListBox or ItemsControl. I assign a callback so that whenever the ItemsSource property is changed it get fired. I test to see if the new or old ItemsSource values are of type INotifyCollectionChanged. If so, I wire up or down the CollectionChanged event. In the CollectionChanged event handler, I examine the Action property off of the NotifyCollectionChangedEventArgs passed into the handler. I then determine if the operation is an Add or Remove. In each condition I am filtering the items in the collection to the type of a NotificationModel. This model holds all the data that will later be bound for the visual part of the NotificationControl.

We are now at the helper methods and I have one for each operation. The AddNotification method takes in a NotificationModel object as a parameter. It creates a NotificationControl object passing into the constructor the NotificationModel object and the Interval property value. Next it adds the NotificationControl object to the Children property and thus onto the DockPanel. We also wire up the Closed event on the NotificationControl and remove it from the ItemsSource collection.

In the RemoveNotification method, we also take in a NotificationModel object as a parameter. Every NotificationControl has a Guid Id property exposed and we use this Id value to remove it from the Children visual collection.

Okay, we have a good understanding of the manager, let’s take a look at the NotificationControl:

namespace Core.Controls
{
    using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Threading;
    using Core.Models;
    using System.Windows.Shapes;
    using Core.Utilities;

    public class NotificationControl : ContentControl
    {
        DispatcherTimer _timer = new DispatcherTimer();

        #region ctor

        public NotificationControl(NotificationModel item, int interval)
        {
            this.DefaultStyleKey = typeof(NotificationControl);
            this._id = item.Id;
            this.Interval = interval;

            if (Interval > 0)
            {
                _timer.Interval = new TimeSpan(0, 0, 0, 0, Interval); // 5 Seconds
                _timer.Tick += new EventHandler(Each_Tick);
                _timer.Start();
            }

            // Set the Header binding.
            Binding binding = new Binding();
            binding.Source = item;
            binding.Path = new PropertyPath("Header");
            this.SetBinding(NotificationControl.HeaderProperty, binding);
            // Set the Text binding.
            binding = new Binding();
            binding.Source = item;
            binding.Path = new PropertyPath("Text");
            this.SetBinding(NotificationControl.TextProperty, binding);
            // Set the Dock property.
            DockPanel.SetDock(this, Dock.Bottom);
            // Add a little margin to the bottom and top.
            this.Margin = new Thickness(0, 2, 0, 2);
            // Swap out the icon based on the NotificationType.
            switch (item.NotificationType)
            {
                case NotificaitonType.Info:
                    this.FillBrush = new SolidColorBrush(ColorHelper.ToColor("#FF98CFE8"));
                    this.Icon = new BitmapImage(new Uri("/SilverlightApplication2;component/Assets/Images/48x48/about.png", UriKind.Relative));
                    break;
                case NotificaitonType.Question:
                    this.FillBrush = new SolidColorBrush(ColorHelper.ToColor("#FF2C3CF5"));
                    this.Icon = new BitmapImage(new Uri("/SilverlightApplication2;component/Assets/Images/48x48/help2.png", UriKind.Relative));
                    break;
                case NotificaitonType.Warning:
                    this.FillBrush = new SolidColorBrush(ColorHelper.ToColor("Yellow"));
                    this.Icon = new BitmapImage(new Uri("/SilverlightApplication2;component/Assets/Images/48x48/warning.png", UriKind.Relative));
                    break;
                case NotificaitonType.Error:
                    this.FillBrush = new SolidColorBrush(ColorHelper.ToColor("Red"));
                    this.Icon = new BitmapImage(new Uri("/SilverlightApplication2;component/Assets/Images/48x48/error.png", UriKind.Relative));
                    break;
                case NotificaitonType.Custom:
                    this.FillBrush = new SolidColorBrush(ColorHelper.ToColor("White"));
                    this.Icon = new BitmapImage(new Uri("/SilverlightApplication2;component/Assets/Images/48x48/user1.png", UriKind.Relative));
                    break;
            }
        }

        #endregion

        private Guid _id = Guid.NewGuid();
        public Guid Id
        {
            get { return _id; }
        }

        #region Dependency Properties

        #region FillBrush

        public static readonly DependencyProperty FillBrushProperty = DependencyProperty.Register(
            "FillBrush",
            typeof(Brush),
            typeof(NotificationControl),
            null);

        ///
<summary> /// Gets or sets a value that indicates whether
 /// the field is required.
 /// </summary>
        public Brush FillBrush
        {
            get { return (Brush)GetValue(NotificationControl.FillBrushProperty); }
            set { SetValue(NotificationControl.FillBrushProperty, value); }
        }

        #endregion

        #region Interval

        public static readonly DependencyProperty IntervalProperty = DependencyProperty.Register(
            "Interval",
            typeof(int),
            typeof(NotificationControl),
            new PropertyMetadata(0));

        ///
<summary> /// Gets or sets a value that indicates whether
 /// the field is required.
 /// </summary>
        public int Interval
        {
            get { return (int)GetValue(NotificationControl.IntervalProperty); }
            set { SetValue(NotificationControl.IntervalProperty, value); }
        }

        #endregion

        #region Icon
        public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
            "Icon",
            typeof(ImageSource),
            typeof(NotificationControl),
            new PropertyMetadata(new BitmapImage(new Uri("/Core;component/Assets/Images/48x48/about.png",UriKind.Relative))));

        ///
<summary> /// Gets or sets a value that indicates whether
 /// the field is required.
 /// </summary>
        public ImageSource Icon
        {
            get { return (ImageSource)GetValue(NotificationControl.IconProperty); }
            set { SetValue(NotificationControl.IconProperty, value); }
        }
        #endregion

        #region Header
        public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
            "Header",
            typeof(string),
            typeof(NotificationControl),
            new PropertyMetadata(OnHeaderPropertyChanged));

        ///
<summary> /// Gets or sets a value that indicates whether
 /// the field is required.
 /// </summary>
        public string Header
        {
            get { return (string)GetValue(NotificationControl.HeaderProperty); }
            set { SetValue(NotificationControl.HeaderProperty, value); }
        }

        private static void OnHeaderPropertyChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
        }
        #endregion

        #region Text
        public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
            "Text",
            typeof(string),
            typeof(NotificationControl),
            new PropertyMetadata(OnTextPropertyChanged));

        ///
<summary> /// Gets or sets a value that indicates whether
 /// the field is required.
 /// </summary>
        public string Text
        {
            get { return (string)GetValue(NotificationControl.TextProperty); }
            set { SetValue(NotificationControl.TextProperty, value); }
        }

        private static void OnTextPropertyChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
        {
        }
        #endregion

        #endregion

        #region Overrides

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            Button closeButton = GetTemplateChild("closeButton") as Button;
            if (closeButton != null)
            {
                closeButton.Click += new RoutedEventHandler(closeButton_Click);
            }
        }

        #endregion

        #region Events

        public event EventHandler Closed;

        void closeButton_Click(object sender, RoutedEventArgs e)
        {
            EventHandler handler = this.Closed;
            if (handler != null)
            {
                _timer.Stop();
                handler(this, EventArgs.Empty);
            }
        }

        public void Each_Tick(object o, EventArgs sender)
        {
            closeButton_Click(this, new RoutedEventArgs());
        }

        #endregion

    };
}

This is longer but really doesn’t do anything really exciting. If we look we see that the bulk of the work is done in the constructor. We also have a DispatcherTimer object and several dependency properties that represent the User Interface of the control. This control derives from the ContentControl.

In the constructor, we set the DispatcherTimer only if the Interval value passed is is greater than zero (0). Next we set the binding for Header and Text. We set the Dock property to “Bottom”. We give ourselves a slight margin on the Top and Bottom of the control so that the are not stacked on top of eachother. Finally we switch on the NotificationType and set the corresponding FillBrush and Icon properties. The FillBrush uses a ColorHelper class to makes things easier in Silverlight. In the OnApplyTemplate we find the closeButton and wire up the Click event. We also have a Closed event that gets fired whenever the button is clicked or the DispatcherTimer fires.

As you have already seen, we have an enum NotificationType:

namespace Core
{
    public enum NotificaitonType
    {
        Info,
        Question,
        Warning,
        Error,
        Custom
    };
}

We are going to skip over the Events folder and look at our Interfaces:

namespace Core
{
    using System;

    public interface INotification
    {
        string Header { get; set; }
        string Text { get; set; }
        NotificaitonType NotificationType { get; set; }
    };

    public interface INotificationService
    {
        void ProcessInfo(string header, string text);
        void ProcessQuestion(string header, string text);
        void ProcessWarning(string header, string text);
        void ProcessError(string header, Exception ex);
        void ProcessCustom(string header, string text);
    };
}

There are two interfaces: INotification and INotificationService. I use INotification for my data objects. I use INotificationService for my external API in using this solution.

Let’s look at the NotificationModel class:

namespace Core.Models
{
    using Caliburn.Micro;
    using System;

    public class NotificationModel : PropertyChangedBase, INotification
    {
        #region Properties

        private Guid _id = Guid.NewGuid();
        public Guid Id
        {
            get { return _id; }
        }

        private string _header = "";
        public string Header
        {
            get { return _header; }
            set
            {
                _header = value;
                NotifyOfPropertyChange(() => Header);
            }
        }

        private string _text = "";
        public string Text
        {
            get { return _text; }
            set
            {
                _text = value;
                NotifyOfPropertyChange(() => Text);
            }
        }

        private NotificaitonType _notificationType = NotificaitonType.Info;
        public NotificaitonType NotificationType
        {
            get { return _notificationType; }
            set
            {
                _notificationType = value;
                NotifyOfPropertyChange(() => NotificationType);
            }
        }

        #endregion

    };
}

Again, this class implements the INotification interface. It also derives from the PropertyChangedBase from Caliburn.Micro.

The next and more interesting class is the NotificationService class:

namespace Core.Services
{
    using System;
    using System.ComponentModel.Composition;
    using Core.Models;
    using Caliburn.Micro;

    [Export(typeof(INotificationService))]
    public class NotificationService : INotificationService
    {
        #region ctor

        public NotificationService()
        {

        }

        #endregion

        #region MessageBus

        private IEventAggregator _messageBus;
        [Import]
        public IEventAggregator MessageBus
        {
            get { return _messageBus; }
            set
            {
                _messageBus = value;
                if (_messageBus != null)
                {
                    _messageBus.Subscribe(this);
                }
            }
        }

        #endregion

        public void ProcessInfo(string header, string text)
        {
            Process(header, text, null, NotificaitonType.Info);
        }
        public void ProcessQuestion(string header, string text)
        {
            Process(header, text, null, NotificaitonType.Question);
        }
        public void ProcessWarning(string header, string text)
        {
            Process(header, text, null, NotificaitonType.Warning);
        }
        public void ProcessError(string header, Exception ex)
        {
            Process(header, null, ex, NotificaitonType.Error);
        }
        public void ProcessCustom(string header, string text)
        {
            Process(header, text, null, NotificaitonType.Custom);
        }
        private void Process(string header, string text, Exception ex, NotificaitonType notificationType)
        {
            NotificationModel n = new NotificationModel()
            {
                Header = header,
                Text = (notificationType == NotificaitonType.Error) ? ex.Message : text,
                NotificationType = notificationType
            };
            MessageBus.Publish(new NotificationEvent() { Notification = n });
        }
    };
}

As you can see, I am using MEF to export this type. I am also importing an IEventAggregator object that I call MessageBus. The rest is just the implementation of the INotificationService interface. The final method is the one that actually does all the work and it simply creates a new NotificationModel object and then publishes using a NotificationEvent with the Notification property set to the newly created object.

We can now take a look at the Events folder:

namespace Core
{
    using Core.Models;

    public class NotificationEvent { public NotificationModel Notification { get; set; } }
}

If you have done any event aggregation this may seem foreign but we are basically passing stronly typed messages and the definition of our message is the NotificationEvent class. It has only one property and that is of type NotificationModel.

The only other piece that is missing from the standpoint of creating these controls is the generic.xaml file and I will let you take a look at the styling when you download the project.

Let’s switch gears and look at what it takes to use this in an application. I am going not going to go into getting your project setup for Caliburn.Micro as the project site has many examples for you to download and see how all that works. We will focus on the ShellView and ShellViewModel classes.

The ShellView is shown below:

<UserControl x:Class="SilverlightApplication2.ShellView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:l="clr-namespace:Core.Controls"
             >
    <Grid x:Name="LayoutRoot" 
          Background="Black">

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        
        <StackPanel VerticalAlignment="Bottom" Margin="10">
            <Button x:Name="SendInfoMessage"
                    Content="Send Info Message" 
                    Margin="5"
            />
            <Button x:Name="SendQuestionMessage"
                    Content="Send Question Message" 
                    Margin="5"
            />
            <Button x:Name="SendWarningMessage"
                    Content="Send Warning Message" 
                    Margin="5"
            />
            <Button x:Name="SendErrorMessage"
                    Content="Send Error Message" 
                    Margin="5"
            />
            <Button x:Name="SendCustomMessage"
                    Content="Send Custom Message" 
                    Margin="5"
            />

        </StackPanel>


        <ScrollViewer Grid.Column="1" 
                      Width="430" 
                      HorizontalAlignment="Right" 
                      BorderThickness="0"
                      VerticalScrollBarVisibility="Auto">
            <l:NotificationManager x:Name="Notifications" 
                                   Interval="5000"
                                   ItemsSource="{Binding NotificationItems}"/>
        </ScrollViewer>

    </Grid>
</UserControl>

Because I am using Caliburn.Micro, the buttons automatically wire up the click events. I have a ScrollViewer and then my NotificationManager. You can see that I am setting the Interval for all controls to 5000. I am also binding the ItemsSource to a NotificationItems property.

Let’s see what is happening on the ShellViewModel:

namespace SilverlightApplication2 {
    using System;
    using System.ComponentModel.Composition;
    using Core;
    using Core.Models;
    using Caliburn.Micro;

    [Export(typeof(IShell))]
    public class ShellViewModel : IShell, IHandle
    {

        #region ctor

        public ShellViewModel()
        {
            _notificationItems = new BindableCollection();
        }
        #endregion

        #region Properties

        readonly BindableCollection _notificationItems;
        public BindableCollection NotificationItems
        {
            get { return _notificationItems; }
        }

        #endregion

        #region MessageBus

        private IEventAggregator _messageBus;
        [Import]
        public IEventAggregator MessageBus
        {
            get { return _messageBus; }
            set
            {
                _messageBus = value;
                if (_messageBus != null)
                {
                    _messageBus.Subscribe(this);
                }
            }
        }

        #endregion

        #region NotificationService

        private INotificationService _notificationService;
        [Import]
        public INotificationService NotificationService
        {
            get { return _notificationService; }
            set { _notificationService = value; }
        }

        #endregion

        #region Actions

        public void SendInfoMessage()
        {
            NotificationService.ProcessInfo("Save", "Save successful!");
        }

        public void SendQuestionMessage()
        {
            NotificationService.ProcessQuestion("Confirm Delete?", "Are you sure you want to delete this record?");
        }

        public void SendWarningMessage()
        {
            NotificationService.ProcessWarning("Warning", "Organization name does not allow null.");
        }

        public void SendErrorMessage()
        {
            Exception ex = new Exception("Connection failed to database.  Please contact your system administrator.");
            NotificationService.ProcessError("Error", ex);
        }

        public void SendCustomMessage()
        {
            NotificationService.ProcessCustom("Matt says...", "Hey, what are you doing?");
        }

        #endregion

        #region IHandle

        public void Handle(NotificationEvent message)
        {
            NotificationItems.Add(message.Notification);
        }

        #endregion

    };
}

I am satisfying the requirements for MEF and exporting this class using the IShell marker interface. I am also using Caliburn.Micro’s way of event aggregation with the IHandleinterface. I have a collection of NotificationItems of type NotificationModel and I instantiate them in the constructor. I have a MessageBus property and NotficaitonService property that I bring in via MEF of type IEventAggregator and INotificationService. As you can see, each button click event method has a single line of code except for the one that create an Exception object. If you recall, the NotificationService had a Process method that actually created a NotificationModel object and then used the event aggregation to broadcast the event.

It is interesting to point that I am in essence broadcasting and handling the event in the ShellViewModel. I broadcast it throught my NotificationService object and I handle the event in the Handle method which simple add the NotificationModel object to the collection.

I hope this gives you an idea as to what you can do to provide for a very rich notification service in your applications. This approach would also work in WPF as well.

I have a few things left that I would like to do to make this a first class citizen.

  • I would like to have a simple hyperlink that allow me to copy any notification message to the clipboard.
  • I would also like to extend the Question notification so that it would actually let you do what it needs to do and that is answer yes/no/cancel or ok/cancel, etc.
  • In the Custom notification, I would like to extend it so that you could pass in your own Avatar to better represent a chat application with a hyperlink to respond.
  • Have the ability to only show a NotificationControl as a dialog and not part of the NotificationManager. Meaning that it would show up in the center of the application.

I will try and do an update blog once I have some time to further flesh this out.

I hope this helps you out and look forward to any other paradigms that you have come up with from a standpoint of notifications. I know this can be a pain if you are using WCF RIA Services in Silverlight and you have a centralized dialog that displays messages from the DomainDataSource objects. It will put popup on top of popup using the ChildWindow and that is not the best user experience. That is one of the reasons that I started looking for an alternative solution.

You can download the sample project here.