CodeSnippet: Snarl.NET using WCF (Part I)
Requirements
- .NET 3.0 or greater
Introduction
Growl is an application for Apple's OS X which provides a standard "toasts" interface. Toasts are popups which fade after a short amount of time, and host a small amount of data (usually text, but images, sounds, etc aren't unheard of).
Snarl is an application for Windows which is inspired by Growl which offers a similar implementation, currently coded in Visual Basic 6.
This article will implement several features of Snarl using Windows Communication Foundation (WCF) and Windows Presentation Foundation (WPF). Before we get started on the actual code, I'll explain briefly what WCF is.
Windows Communication Foundation
As I mentioned in a previous article, Dynamic Data Exchange (DDE) isn't the only (or best) Inter-Process Communication (IPC) technique, but it was the only method available given the target of Windows Live Messenger.
WCF is used for IPC, introduced in .NET 3.0. Unlike DDE however, WCF isn't just for IPC on one machine - it can be distributed across networks/the Internet.
One very neat thing about WCF is allowing us to 'encapsulate' the data/methods we want to make available/require, independent of the way it will be transported. The encapsulated methods form the contract, while the transport method is the binding. The different bindings determine how it is transported and accessed. BasicHttpBinding, for example, can be accessed intra-process, on a LAN, or on a WAN, whereas netNamedPipesBinding is only available for IPC.
For more reading material on WCF, it doesn't hurt to start at Wikipedia.
Snarl.NET
WCF bindings can be defined in XML (similar to ASP.NET's web.config) or programmatically. As such, I'll provide the code for both.
Since Snarl.NET is designed to an intra-process application, we'll run with netNamedPipes.
WCF bindings can be defined in XML (similar to ASP.NET's web.config) or programmatically. This project contains examples of both.
To start off, I'll define a few 'properties' which make up this project.
- Since Snarl.NET is designed to an intra-process application, we'll run with netNamedPipes.
- We'll use WPF for the presentation of popups, because its easier and prettier to achieve these effects
- We need a 'master' class/object to maintain the popups, otherwise each one would overwrite the other.
- This isn't the best designed, but this is just a demo of WCF/WPF, not good design.
- For WCF, we need a reference to System.ServiceModel
Create a new WPF Application Project, and add two new blank Class (StaticWindow.cs and SnarlService.cs) and a new XAML Window (Popup.Xaml) to the project.
Popup.Xaml is what will be presented when there are new notifications. I've modelled it roughly on Snarl/Growls default interface.
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="SnarlWPF.Popup" Title="Popup" Height="120" Width="383" AllowsTransparency="True" WindowStyle="None" ShowInTaskbar="False" Background="{x:Null}" WindowStartupLocation="Manual"> <Window.Resources> <Storyboard x:Key="sbFadeOut"> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="{x:Null}" Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="00:00:00" Value="1"/> <SplineDoubleKeyFrame KeyTime="00:00:05" Value="0"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </Window.Resources> <Grid MouseEnter="Window_MouseMove" MouseLeave="Grid_MouseLeave"> <Grid Margin="20,20,0,0"> <Rectangle Margin="10,10,0,0" Stroke="{x:Null}" RadiusX="20" RadiusY="20" HorizontalAlignment="Left" Width="365" Height="102" VerticalAlignment="Top" Fill="#7F000000"/> <TextBlock x:Name="tbInfo" TextWrapping="Wrap" Text="this is some sample text" Margin="105.098,54,8,8" Foreground="#FFFFFFFF" IsHyphenationEnabled="False" OverridesDefaultStyle="False" > <TextBlock.BitmapEffect> <DropShadowBitmapEffect Direction="297" ShadowDepth="1" Softness="0.165" Opacity="1"/> </TextBlock.BitmapEffect> </TextBlock> <TextBlock TextWrapping="Wrap" Foreground="#FFFFFFFF" x:Name="tbHeading" Margin="105.098,18.023,8,0" Height="21.977" VerticalAlignment="Top"><TextBlock.BitmapEffect> <DropShadowBitmapEffect Direction="297" ShadowDepth="1" Softness="0.165" Opacity="1"/> </TextBlock.BitmapEffect><Run FontSize="14" FontWeight="Bold" Text="This is a heading"/></TextBlock> </Grid> <Image HorizontalAlignment="Left" Margin="0,0,0,20" Width="101" Source="Chat.png" Stretch="Fill" RenderTransformOrigin="0.5,0.5" Height="101"> <Image.RenderTransform> <TransformGroup> <ScaleTransform ScaleX="-1" ScaleY="1"/> <SkewTransform AngleX="0" AngleY="0"/> <RotateTransform Angle="0"/> <TranslateTransform X="0" Y="0"/> </TransformGroup> </Image.RenderTransform> </Image> </Grid> </Window>
- sbFadeOut is the fade out animation. It lasts for five seconds, and goes from 100% to 0% Opacity.
- The image used is Xi4Dox by Panoramix
- It won't show in the TaskBar
- There are eventhandlers on the Grid.
The code behind Popup.Xaml (.Xaml.cs) deals with the animation, placement, and obviously the setting of the text for the popup. There is currently no way to change the image, but it really isn't that hard to implement.
public partial class Popup : Window { private delegate void SetNotificationDelegate(String heading, String body); private Window1 parent; private Storyboard sbFadeOut; private TimeSpan ts = new TimeSpan(); public Popup(Window1 parent, Int32 numPopups, String heading, String body) { InitializeComponent(); this.Left = SystemParameters.VirtualScreenWidth - this.Width; this.Top = SystemParameters.WorkArea.Height - (this.Height * (numPopups + 1)); this.parent = parent; tbInfo.Text = body; tbHeading.Text = heading; sbFadeOut = (Storyboard)FindResource("sbFadeOut"); sbFadeOut.Completed += new EventHandler(sbFadeOut_Completed); sbFadeOut.Begin(this,true); } void sbFadeOut_Completed(object sender, EventArgs e) { parent.RemovePopup(this); } private void Window_MouseMove(object sender, MouseEventArgs e) { ts = (TimeSpan)sbFadeOut.GetCurrentTime(this); sbFadeOut.Stop(this); this.Opacity = 1; } private void Grid_MouseLeave(object sender, MouseEventArgs e) { sbFadeOut.Begin(this,true); sbFadeOut.Seek(this, ts, TimeSeekOrigin.BeginTime); } }
One caveat with pausing a storyboard is that you can't modify the opacity (and I assume other things would be unmodifyable) until it is stopped. The above gets around it by recording the storyboards current time, stopping the storyboard, changing the opacity, restarting the animation and seeking to the recorded time.
The StaticWindow class is so we can address the 'master class' - if it was better designed, the static class would be our 'master' class.
static class StaticWindow { public static Window1 window; public static void SetWindow(Window1 w) { window = w; } }
The SnarlService class will form the Service Contract.
using System; using System.Collections.Generic; using System.ServiceModel; using System.Text; namespace SnarlWPF { [ServiceContract] class SnarlService { [OperationContract] void SetNotification(String title, String body) { StaticWindow.window.SetNotification(title, body); } } }
You should see that SnarlService.SetNotification calls 'SetNotification' in our master window. We better define that now. In Window1.Xaml.Cs (which was created when we selected 'WPF Application' as our project type), we'll need to create a Delegate to go with the SetNotification method.
private delegate void SetNotificationDelegate(String heading, String body);
The delegate is needed so that we can use the Dispatcher. The Dispatcher helps us avoid cross threaded UI changes.
public void SetNotification(String heading, String body) { if (!Dispatcher.CheckAccess()) { Dispatcher.Invoke(DispatcherPriority.Normal, new SetNotificationDelegate(SetNotification), heading, body); return; } Popup wPop = new Popup(this,Popups.Count, heading, body); wPop.Show(); Popups.Add(wPop); }
The rest of the class (below) is made up of (mostly) controlling the TrayIcon (there is no WPF equivelant, so you have to us System.Windows.Forms and System.Drawing; I did this as Using WF = System.Windows.Forms), starting the WCF service, and closing the popup.
public partial class Window1 : Window { private delegate void SetNotificationDelegate(String heading, String body); public List<Popup> Popups = new List<Popup>(); private WF.NotifyIcon niTray = new WF.NotifyIcon(); public Window1() { InitializeComponent(); this.Hide(); niTray.Text = "Snarl.NET"; niTray.Icon = System.Drawing.Icon.ExtractAssociatedIcon("Chat.ico"); niTray.Visible = true; WF.ContextMenu cmTray = new WF.ContextMenu(); cmTray.MenuItems.Add("Quit",onClickQuit); niTray.ContextMenu = cmTray; StaticWindow.SetWindow(this); try { StartWCF(); } catch (Exception e) { MessageBox.Show(e.Message); } } private void StartWCF() { Type serviceType = typeof(SnarlService); Uri basePipeAddress = new Uri("net.pipe://localhost/Snarl"); ServiceHost host = new ServiceHost(serviceType, basePipeAddress); ServiceMetadataBehavior behavior = new ServiceMetadataBehavior(); host.Description.Behaviors.Add(behavior); BindingElement bindingElement = new NamedPipeTransportBindingElement(); CustomBinding binding = new CustomBinding(bindingElement); host.AddServiceEndpoint(serviceType, new NetNamedPipeBinding(), basePipeAddress); host.AddServiceEndpoint(typeof(IMetadataExchange), binding, "MEX"); host.Open(); } public void SetNotification(String heading, String body){..} public void RemovePopup(Popup pop) { Popups.Remove(pop); pop.Close(); pop = null; } private void onClickQuit(Object sender, EventArgs e) { niTray.Visible = false; Application.Current.Shutdown(); } }
Clients
Now that Snarl is out of the way, we need to create some clients for it, otherwise we won't be able to see the popups!
CommandLine client
This is a very basic client aimed at just testing that everything is working okay. Create a new Console Application (remember to make it .NET 3 or 3.5, otherwise you can't include WCF). This client will use the somewhat more automatic approach, which relies on the XML app.config file. This isn't appropriate in all cases - particularly when you're generating library files - but when it is appropriate, it can save a lot of time.
The automatic generation of proxy/client requires adding a service reference, which can be done by right clicking on a project in the Solution Explorer, and clicking 'Add Service Reference' (tricky eh?), however this requires the service you want to connect to to be running, so fire up the SnarlWPF service that we wrote before. The address required by the Add Service Reference Dialog is "net.pipe://localhost/Snarl".
It actually looks up "net.pipe://localhost/Snarl/MEX" (Metadata EXchange), which is why we had to add two endpoints in our WCF service. If everything went well, it should popup with the service, as well as the method(s) available in the contract. Give the Service Reference a name, and click okay. You should see an app.config file automatically generated/added to the project.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ServiceModel; namespace SnarlClient { class Program { static void Main(string[] args) { SnarlReference.SnarlServiceClient proxy = new SnarlReference.SnarlServiceClient(); String input=""; while (input != "q") { input = Console.ReadLine(); proxy.SetNotification("",input); } Console.WriteLine("Press any key to terminate."); Console.ReadKey(); } } }
As you can see, this client is very basic. You enter a line of text, and providing the Snarl service is running, a popup will appear.
Windows Live Messenger client
This client is a little trickier, but follows the same basic process - we need to create a proxy/client class, we wait for an event, we fire.
There is a bit of setup you will need to do first if you want to try this. Add the registry key (DWORD) AddInFeatureEnabled, to HKEY_CURRENT_USER\SOFTWARE\Microsoft\MSNMessenger, and set AddInFeatureEnabled to 1.
Create the project (Class Library), go into the project properties, and make sure the Assembly Name matches the Namespace.Classname, ie, SnarlWLM.Addin. While you're in the project properties, sign the project otherwise it cannot be installed into the Global Assembly Cache.
Once you compile your DLL, you'll need to install it into the Global Assembly Cache (GAC), via a commandline utility, gacutil.exe. The syntax is gacutil /i <name of your dll>, ie gacutil /i SnarlWLM.Addin.DLL
Okay, phew, now that's out of the way, we can get to coding it. First, we need the proxy/client class, but as I said, since the end target for the DLL is in the GAC, we can't have a config file. We need to generate the proxy class by using SVCUTIL.exe.
Fire up Snarl, and then in a command prompt enter
svcutil.exe /language:cs /out:generatedProxy.cs net.pipe://localhost/Snarl
Add generatedProxy.cs to your project - this is almost the equivalent of adding the Service Reference, it is just as if we're missing the app.config file, so we need to add those details.
NetNamedPipeBinding binding = new NetNamedPipeBinding(); EndpointAddress ep = new EndpointAddress("net.pipe://localhost/Snarl"); ChannelFactory<SnarlService> SnarlChannelFactory = new ChannelFactory<SnarlService>(binding, ep); SnarlService snarlClient = SnarlChannelFactory.CreateChannel();
We can then use snarlClient the same way as proxy in the first client. The rest of the client essentially deals with the (extremely limited) Windows Live Messenger API, which is another article in itself. I highly recommend Bart De Smet's article, "Your First Windows Live Messenger Add-In"
public class Addin : IMessengerAddIn { private MessengerClient messenger; private SnarlService snarlClient; public void Initialize(MessengerClient messenger) { this.messenger = messenger; messenger.AddInProperties.Creator = "Paul Jenkins"; messenger.AddInProperties.Description = "Snarl.NET WLM Plugin"; messenger.AddInProperties.FriendlyName = "SNWLM"; messenger.Shutdown += new EventHandler(messenger_Shutdown); messenger.StatusChanged += new EventHandler<StatusChangedEventArgs>(messenger_StatusChanged); NetNamedPipeBinding binding = new NetNamedPipeBinding(); EndpointAddress ep = new EndpointAddress("net.pipe://localhost/Snarl"); ChannelFactory<SnarlService> SnarlChannelFactory = new ChannelFactory<SnarlService>(binding, ep); snarlClient = SnarlChannelFactory.CreateChannel(); } void messenger_StatusChanged(object sender, StatusChangedEventArgs e) { try { //Due to a limitation in WLM's API, if a user comes back from idle/away/busy/etc, a popup will appear if (e.User.Status == UserStatus.Online) snarlClient.SetNotification("Live Messenger Contact Online...", e.User.FriendlyName); } catch (Exception ex) { MessageBox.Show(ex.Message); } } void messenger_Shutdown(object sender, EventArgs e) { messenger.Dispose(); } }
Extension
- Create a plugin architecture, so that Snarl.NET can load plugins which will propagate notifications rather than having small applications monitor them, ie Pop3 checker.
- Add support for DDE, or other end points (HTTP/etc)
- Add options on where popups appear on the screen
- Fix up the placement code so that popups won't overlap each other if many appear at once.
- Add skinning/templating of popups.
Download
Source code (134kb)
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 2.5 Australia License.




