Archive

Posts Tagged ‘Caliburn.Micro’

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.

Using ICustomTypeProvider in Silverlight 5 beta with JSON and ASP.NET MVC 3

June 3, 2011 6 comments

One of the really powerful new features in Silverlight 5 is the new interface ICustomTypeProvider. This allow us to have a dynamic way to bind to objects that we don’t know the shape of until at runtime. There are three blog posts that you need to read before continuing forward. Please read this post by Alexandra Rusina. In it she provides a nice implementation for ICustomTypeProvider. The other post is by Jeremy Likness. In his post he expounds a little more on what Alexandra provided and gives us the ability to parse JSON. The last post, basically gives us an easy way to format our JSON so that we can see it in a human readable format instead of what it looks like once we pull it from the stream.

This post basically will takes us from the point where Jeremy and Alexandra left us and use ASP.NET MVC 3 to act as our service host for getting data across the wire.

Ok, let’s get started. First of all, I created a blank solution called “CustomTypeProvider”.

Next, I added a new Silverlight 5 project. When asked, I changed the host to be a ASP.NET MVC project. I gave the name of the Silverlight project, “CustomerTypeProvider.Silverlight” and the web host, “CustomTypeProvider.Web”.

I like to use Caliburn.Micro whenever I can and I opted to use the latest version of Caliburn.Micro in my Silverlight project. I used NuGet to pull down the package. It is as easy as right-clicking your References and selecting, “Add Library Package Reference”. Once the dialog appeared, I changed it to online and entered, “Caliburn.Micro” in the search and installed it once it appeared.

I basically followed the steps provided in the web page that comes up in Visual Studio once Caliburn.Micro’s package is installed. I also added the following assembly references:

  • System.Json
  • System.ComponentModel.DataAnnotations
  • System.Windows.Controls.Data

Let’s look at the code necessary for the web first:

namespace CustomerTypeProvider.Web.Controllers
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using System.Collections;
    using System.Data.SqlClient;

    [HandleError]
    public class HomeController : Controller
    {
        public JsonResult GetCustomers()
        {
            var data = LoadCustomers();
            return Json(data, JsonRequestBehavior.AllowGet);
        }
        public JsonResult GetOrders()
        {
            var data = LoadOrders();
            return Json(data, JsonRequestBehavior.AllowGet);
        }
        public JsonResult GetQuery(string query)
        {
            var data = LoadQuery(query);
            return Json(data, JsonRequestBehavior.AllowGet);
        }
        private List<Customer> LoadCustomers()
        {
            List<Customer> result = new List<Customer>();
            result.Add(new Customer() { FirstName = "Matt", LastName = "Duffield" });
            result.Add(new Customer() { FirstName = "Dean", LastName = "Duffield" });
            return result;
        }
        private List<Order> LoadOrders()
        {
            List<Order> result = new List<Order>();
            result.Add(new Order() { ContactName= "Matt Duffield",
              OrderDate= DateTime.Now, OrderNumber = 100 });
            result.Add(new Order() { ContactName = "Dean Duffield",
              OrderDate = DateTime.Now, OrderNumber = 101 });
            return result;
        }
        private ArrayList LoadQuery(string query)
        {
            ArrayList objs = new ArrayList();

            //SqlConnection conn = new SqlConnection("Data Source=(local);Initial Catalog=Northwind;User Id=myUsername;Password=myPassword;");
            SqlConnection conn = new SqlConnection("Data Source=(local);Initial Catalog=Northwind;Integrated Security = SSPI;");
            //SqlCommand cmd = new SqlCommand("SELECT TOP 10 [CustomerID],[CompanyName],[Address],[City],[Region],[PostalCode],[Country]  FROM [Customers]", conn);
            SqlCommand cmd = new SqlCommand(query, conn);
            conn.Open();
            SqlDataReader r = cmd.ExecuteReader();
            while (r.Read())
            {
                objs.Add(new
                {
                    CustomerID = (r["CustomerID"] is DBNull ? "" : r["CustomerID"]),
                    CompanyName = (r["CompanyName"] is DBNull ? "" : r["CompanyName"]),
                    Address = (r["Address"] is DBNull ? "" : r["Address"]),
                    City = (r["City"] is DBNull ? "" : r["City"]),
                    Region = (r["Region"] is DBNull ? "" : r["Region"]),
                    PostalCode = (r["PostalCode"] is DBNull ? "" : r["PostalCode"]),
                    Country = (r["Country"] is DBNull ? "" : r["Country"])
                });
            }
            r.Close();
            conn.Close();

            return objs;
        }

        public class Customer
        {
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
        public class Order
        {
            public int OrderNumber { get; set; }
            public DateTime OrderDate { get; set; }
            public string ContactName { get; set; }
        };
    };
}

Okay, so there is a little going on but we will start from the top. We basically have three public methods at the top:

  • GetCustomers()
  • GetOrders()
  • GetQuery(string query)

The first two call helper methods that basically create a generic List<> with some sample data and pass it back to the calling method.  The third is a little more dynamic in that it uses ADO.NET to create a SqlConnection, SqlCommand, and finally a SqlDataReader.  It executes the query passed in and then loops through the data reader to shapes a new object that gets added to an ArrayList.  The ArrayList then gets passed back to the calling method. One thing to note about the data reader loop is that we are checking for DBNull and giving it a default value. This is necessary on the client side so that we can infer the type information.

In each of the corresponding methods above, a new Json object is created passing in the data that was returned from the helper methods as well as a second parameter is set to allow the Get verb.

Now let’s move on to the client side.  In the client side, I have to folders that are of interest:

  • Formatting
  • Framework

In the Formatting folder is the logic necessary to format the JSON stream that comes from the web make it human readable.

In the Framework folder is the logic that I took from Alexandra Rusina’s and from Jeremy Likness’ blog posts.  I won’t go into detail in these classes since there are separate posts by each of them dedicated to the classes.  What I will show you is the ShellView and ShellViewModel to see how we get this data and render on screen.

Let’s take a look at the ShellView:

<UserControl x:Class="CustomerTypeProvider.Silverlight.ShellView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk" 
    >
    <Grid Background="White" Margin="5">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <sdk:DataGrid AutoGenerateColumns="True"
            ItemsSource="{Binding Path=ResultsPaneItems}"
            IsReadOnly="True"
            />
        <TextBox x:Name="JsonResult" AcceptsReturn="True"
                 Grid.Column="1" Grid.Row="0" 
                 Width="400" VerticalScrollBarVisibility="Auto" 
                 HorizontalScrollBarVisibility="Auto"/>
        <StackPanel Grid.Column="0" Grid.Row="1" Orientation="Horizontal">
            <Button x:Name="LoadCustomers" Content="Load Customer" Margin="5" />
            <Button x:Name="LoadOrders" Content="Load Orders" Margin="5" />
            <Button x:Name="LoadQuery" Content="Load Query" Margin="5" />
        </StackPanel>
    </Grid>
</UserControl>

Because I am using Caliburn.Micro, my XAML is very clean.  We see a DataGrid that has it’s ItemsSource property bound to collection property called, “ResultsPaneItems” object.  We also see a TextBox that is bound to a property called, “JsonResult” as well.  Finally, we see three buttons that call the three corresponding web methods.
Looking at the ShellViewModel we see the following:

namespace CustomerTypeProvider.Silverlight 
{
    using System.ComponentModel.Composition;
    using System.Collections.ObjectModel;
    using Core.Framework;
    using Core.Framework.Formatting;
    using Caliburn.Micro;
    using System;
    using System.Net;
    using System.Json;
    using System.IO;
    using System.Diagnostics;

    [Export(typeof(IShell))]
    public class ShellViewModel : Screen, IShell 
    {
        public ObservableCollection<CustomType> ResultsPaneItems { get; private set; }
        private string _jsonResult;
        public string JsonResult
        {
            get { return _jsonResult; }
            set
            {
                _jsonResult = value;
                NotifyOfPropertyChange(() => JsonResult);
            }
        }

        #region ctor

        public ShellViewModel()
        {
            ResultsPaneItems = new ObservableCollection<CustomType>();

        }

        #endregion

        public void LoadCustomers()
        {
            LoadResultsPane("/home/GetCustomers");
        }

        public void LoadOrders()
        {
            LoadResultsPane("/home/GetOrders");
        }
        public void LoadQuery()
        {
            string query = "SELECT TOP 10 [CustomerID],[CompanyName],[Address],[City],[Region],[PostalCode],[Country]  FROM [Customers]";
            LoadResultsPane("/home/GetQuery?query=" + query);
        }

        private void LoadResultsPane(string uri)
        {
            ResultsPaneItems = new ObservableCollection<CustomType>();
            NotifyOfPropertyChange("ResultsPaneItems");

            Uri serviceUri = new Uri(uri, UriKind.Relative);
            WebClient downloader = new WebClient();
            downloader.OpenReadCompleted += (s, e) =>
            {
                // Load the JsonArray from the service call.
                StreamReader sr = new StreamReader(e.Result);
                var content = sr.ReadToEnd();
                JsonResult = new JsonFormatter(content).Format();
                var jsonArray = JsonArray.Load(e.Result) as JsonArray;
                Debug.WriteLine(content);
                if (jsonArray == null) return;

                // Pull the first record as a template.
                var template = jsonArray[0] as JsonObject;
                if (template == null) return;

                // Use the template to get type information.
                var jsonHelper = new JsonHelper<CustomType>(template);

                // Iterate over the results and add to the underlying collection.
                foreach (var item in jsonArray)
                {
                    var customType = new CustomType();
                    jsonHelper.MapJsonObject(customType.SetPropertyValue, item);
                    ResultsPaneItems.Add(customType);
                }
            };
            // Retrieve the data.
            downloader.OpenReadAsync(serviceUri);
        }

    };
}

Okay, let’s start from the top and work our way down. First we see that declaration of the ResultsPaneItems collection. Next we see the property for the JsonResult string. In the constructor, we initialize the collection.

Next, we have the three methods that respond to the corresponding buttons. All three of them call a helper method called, “LoadResultsPane(string uri)”. It is this method that does all of the heavy lifting. The only difference in the methods is that the last one also passes in the query as part of the query string to the GetQuery method on the controller.

In the LoadResultsPane(…) method, we are initializing the ResultsPaneItems to a new collection. Note that this collection is of type CustomType. This is important since we want each button click to load a fresh set of data. Next we use the Uri and WebClient objects to perform the OpenReadAsync(…) call. This is an asynchronous call and that is we we have an anonymous delegate in the form of a lambda expression stating what to do when the call comes back. Inside the expression, we create a StreamReader object off of the Result. We read the stream so that we can use our formatter class and display the raw JSON that was just returned to us. Next we create a JsonArrary and load the same Result object trying to cast it to a JsonArrary object. If that is successfuly, then we use the first row as a representation of the data so that we can type it accordingly. If you recall, this is why we had the test for DBNull in the data reader loop on the controller. Finally, we loop over the JsonArrary and create a CustomType object. We use the JsonHelper provided by the previous blog posts and then add it to our collection.

That is all there is to it. You now have a very dynamic model in which you can data bind to any object that is sent from the server. You no longer need to know about it. The server-side code could be further abstracted so that you had a completely generic solution instead of the hard-coding I have shown in the loop of the data reader.

Here is a sample screen shot of the application:

You can download the sample code here. Don’t forget that this solutions works only for Silverlight 5 beta.
Hope you enjoy…

I am speaking at Carolina CodeCamp 2011

I am going to be doing three presentations at the Carolina CodeCamp 2011. The three topics include Silverlight Dashboards, Taking a look at Caliburn.Micro, and Taking a look at Prism 4.

Hope to see you all there.

Follow

Get every new post delivered to your Inbox.

Join 211 other followers