Requirements
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.
Windows Communication Foundation
WCF is used for Intra-Process Communication (IPC), introduced in .NET 3.0.
Unlike other IPC methods such as DynamicDataExchange, 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. 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.
Discussion
Okay, this article is massive, so I'm expecting a grammatical and spelling
nightmare, so feel free to pick me up on that. WCF is also very new to me (this
was my project idea for learning how to use it), so it there is any
coding/technical errors, I'd really appreciate if somebody could point them
out.
Is this sort of post too 'epic'? Should I have split it into multiple
parts?
Hope somebody got some benefit/enjoyment out of it other than myself
