I’m a big believer in re-use. Code I don’t aihave to write is code I don’t have todebug or maintain. When I discovered that I’d really like some notificationtoast in my Windows Forms enterprise application, I immediately started lookingfor libraries.
The definitive toast solution for Mac OS is Growl.Growl is a separate application/control panel that runs on your system and managesnotifications from all subscribing services on your system. No more overlapping toast.It also provides a lot of filtering, control, and plugin-based extensibility. Didyou want your ‘new mail’ notifications to show up on your cell phone as SMS ratherthan on your desktop? Just do it. Want the toilet to flush when the build fails? Getan Arduino and script it up!
Needless to say I was really chuffed to find the Growlfor Windows project. Their application is still in beta, but the developers arevery responsive, and they are checking code fixes in within hours of getting bug reportswhen they can. GfW can provide you notifications for your GMail, Outlook, Visual StudioBuilds, and the current song playing in Pandora.
Installing Growl With My Application
My application installs with NSIS. Addingautomatic installation of the Growl client to my installer was very quick. I grabbedthe .msi file from the Growl download, and put it within my build tree. Then I addedthe following snippet to my NSIS script:
Section "GrowlInstall" SEC01File "GrowlWindows Deployment.msi"ExecWait 'MSIEXEC.EXE /I "$INSTDIRWindows Deployment.msi"/QB- ALLUSERS=1'SectionEnd
That’s it for the install. However, I wanted it to uninstall cleanly as well, so Iadded the following line to the uninstall section:
ExecWait 'MSIEXEC.EXE/x "$INSTDIRWindows Deployment.msi" /qn'
Two caveats:
- You may not want to uninstall Growl automatically for the user, in case they had installedit on their own and want to uninstall your app, but keep Growl. This isn’t an issuein my business deployment, but it may be one for a public application.
- There may be some issues with UAC installs, and I haven’t tested this yet. I thinkI can just run my NSIS exe ‘As Administrator’ and have it work, but there may be issueswith msiexec and UAC.
Code to Send Notifications
First things first, lets establish some interfaces to program to, that way we canswap out implementations for testing and if our needs change.
public interface INotificationMessage{string MessageType { get; }string MessageDescription { get; }string MessageId { get; }string MessageText { get; set; }Image MessageIcon { get; }Action AcceptMessageCallback { get; set; }Action DeclineMessageCallback { get; set; }}public interface INotificationSender{void SendMessage(INotificationMessage message);}
Next, we’ll implement a specific Notification Message type. Each Notification Messagetype can be instantiated of a specific type so that it’s very easy to add new typesas well as very easy to use them. Each type can have an icon of it’s own.
public class ApplicationErrorNotificationMessage: INotificationMessage{public string MessageType{ get; private set; }public string MessageDescription{ get; private set; }public string MessageId{ get; private set; }public string MessageText{ get; set; }public Image MessageIcon { get; private set;}public Action AcceptMessageCallback { get; set;}public Action DeclineMessageCallback { get; set;}public ApplicationErrorNotificationMessage(){this.MessageType = "APPLICATION_ERROR";this.MessageDescription = "ApplicationError";this.MessageId = Guid.NewGuid().ToString();this.MessageIcon = Application.Properties.Resources.AppIcon;}}
It appears that growl caches the message icons, so you might need to restart the Growlprocess if you go changing your Notification icons.
Finally we have our implementation of INotificationSender which registers with Growl,ensures that it is installed, and ensures that it is running.
public class GrowlNotificationSender: INotificationSender{private GrowlConnector connector;private Image applicationIcon;private string applicationName;private INotificationMessage[] messageTypes;private Dictionary<string,INotificationMessage> sentMessages;public GrowlNotificationSender(INotificationMessage[]messageTypes,Image applicationIcon,string applicationName){this.connector = new GrowlConnector();this.messageTypes = messageTypes;this.applicationIcon = applicationIcon;this.applicationName = applicationName;this.sentMessages = new Dictionary<string,INotificationMessage>();EnsureGrowlIsRunning();RegisterWithGrowl();}private void RegisterWithGrowl(){Application thisApplication = new Application(applicationName);thisApplication.Icon = applicationIcon;List<NotificationType> notificationTypes = new List<NotificationType>();foreach (var messageType in messageTypes){NotificationType notificationType =new NotificationType(messageType.MessageType,messageType.MessageDescription);notificationType.Icon = messageType.MessageIcon;notificationTypes.Add(notificationType);}this.connector.Register(thisApplication, notificationTypes.ToArray());this.connector.NotificationCallback += new GrowlConnector.CallbackEventHandler(connector_NotificationCallback);this.connector.EncryptionAlgorithm = Cryptography.SymmetricAlgorithmType.PlainText;}public void EnsureGrowlIsRunning(){Process[] processlist = Process.GetProcesses();foreach (Process theprocess in processlist){if (theprocess.ProcessName.Equals("Growl")){ return; }}Detector detector = new Growl.CoreLibrary.Detector();if (detector.IsAvailable){System.Diagnostics.Process.Start(detector.InstallationFolder + @"Growl.exe");Thread.Sleep(1000);return;}else{throw new FileNotFoundException("Growlnot installed?");}}void connector_NotificationCallback(Response response,CallbackData callbackData){if (sentMessages[callbackData.Data] != null){if (callbackData.Result == CallbackResult.CLICK){if (sentMessages[callbackData.Data].AcceptMessageCallback!= null){sentMessages[callbackData.Data].AcceptMessageCallback.Invoke();}}else{if (sentMessages[callbackData.Data].DeclineMessageCallback!= null){sentMessages[callbackData.Data].DeclineMessageCallback.Invoke();}}sentMessages.Remove(callbackData.Data);}}public void SendMessage(INotificationMessagemessage){this.sentMessages.Add(message.MessageId, message);CallbackContext callbackContext = new CallbackContext();callbackContext.Data = message.MessageId;callbackContext.Type = message.MessageType;Notification notification = new Notification(this.applicationName,message.MessageType,message.MessageId,message.MessageDescription,message.MessageText);EnsureGrowlIsRunning();this.connector.Notify(notification, callbackContext);}}
This implementation keeps track of sent messages, and executes callbacks on them dependingon the user’s response. If you don’t want to use one of the callbacks, just leaveit undefined.
I haven’t found a way to get Growl to send the CLOSE message rather than the TIMEOUT,but that’s ok, since I treat them the same.
Using The Code
You’d probably want to wire this up with your IOC container, but here’s a manual usage.
INotificationSender sender = new GrowlNotificationSender(new[] { new ApplicationErrorNotificationMessage()},Moneta.Properties.Resources.Crystal_128_package_network,"My Application");Thread.Sleep(1000);ApplicationErrorNotificationMessage message = new ApplicationErrorNotificationMessage{MessageText = "Hi There",AcceptMessageCallback = () => MessageBox.Show("Youclicked"),DeclineMessageCallback = () => MessageBox.Show("PayAttention!")};sender.SendMessage(message);
It’s worth noting that the first registration of your app may take a moment for Growlto process, and that’s why I have the Sleep() in there. If your app doesn’t intendto immediately send a message after it’s first registration, that’s unnecessary.