I’m a big believer in re-use. Code I don’t aihave to write is code I don’t have to
debug or maintain. When I discovered that I’d really like some notification
toast in my Windows Forms enterprise application, I immediately started looking
for libraries.
The definitive toast solution for Mac OS is Growl.
Growl is a separate application/control panel that runs on your system and manages
notifications from all subscribing services on your system. No more overlapping toast.
It also provides a lot of filtering, control, and plugin-based extensibility. Did
you want your ‘new mail’ notifications to show up on your cell phone as SMS rather
than on your desktop? Just do it. Want the toilet to flush when the build fails? Get
an Arduino and script it up!
Needless to say I was really chuffed to find the Growl
for Windows project. Their application is still in beta, but the developers are
very responsive, and they are checking code fixes in within hours of getting bug reports
when they can. GfW can provide you notifications for your GMail, Outlook, Visual Studio
Builds, and the current song playing in Pandora.
Installing Growl With My Application
My application installs with NSIS. Adding
automatic installation of the Growl client to my installer was very quick. I grabbed
the .msi file from the Growl download, and put it within my build tree. Then I added
the following snippet to my NSIS script:
Section "Growl
Install" SEC01
File "Growl\Windows Deployment.msi"
ExecWait 'MSIEXEC.EXE /I "$INSTDIR\Windows Deployment.msi"
/QB- ALLUSERS=1'
SectionEnd
That’s it for the install. However, I wanted it to uninstall cleanly as well, so I
added the following line to the uninstall section:
ExecWait 'MSIEXEC.EXE
/x "$INSTDIR\Windows Deployment.msi" /qn'
Two caveats:
-
You may not want to uninstall Growl automatically for the user, in case they had installed
it on their own and want to uninstall your app, but keep Growl. This isn’t an issue
in 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 think
I can just run my NSIS exe ‘As Administrator’ and have it work, but there may be issues
with msiexec and UAC.
Code to Send Notifications
First things first, lets establish some interfaces to program to, that way we can
swap 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 Message
type can be instantiated of a specific type so that it’s very easy to add new types
as 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 = "Application
Error";
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 Growl
process 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("Growl
not 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(INotificationMessage
message)
{
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 depending
on the user’s response. If you don’t want to use one of the callbacks, just leave
it 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("You
clicked"),
DeclineMessageCallback = () => MessageBox.Show("Pay
Attention!")
};
sender.SendMessage(message);
It’s worth noting that the first registration of your app may take a moment for Growl
to process, and that’s why I have the Sleep() in there. If your app doesn’t intend
to immediately send a message after it’s first registration, that’s unnecessary.
Resources