Halfwit Extensibility - Notifications

The original version of Halfwit wasn't out in the wild very long before people started asking for features that I hadn't thought of. The first was minimize-to-tray, and very quickly thereafter was popup notifications ("toasts") for new tweets.

Notifications in Halfwit 1 are a poor-man's toast. When new tweets arrive, a balloon popup appears telling you how many tweets just came in. No more detail than that. Unsurprisingly, people wanted more.

I, however, am not a big fan of toast notifications, so when I started writing Halfwit 2 I had pretty much resigned myself to losing the feature altogether.

Recently a couple of people have independently asked for notifications to be integrated with Growl for Windows, which is a kind of "global notifications system", letting users customize how the popups look etc. I thought long and hard about it, and eventually arrived at one conclusion: If they want it so bad, let 'em write it themselves! :-)

Thus was born Halfwit's extensibility and eventing system.

Inspired by (read: stolen from) Paul Stovell's work on FunnelWeb, Halfwit uses a combination of MEF, Autofac and MVVM Light to achieve its plugin system. Here's a rundown.

Core Classes

The first step in creating a plug-in architecture was to move all the bits that a plug-in author might need into a separate assembly. I called this assembly "Halfwit2.Core" and extracted a bunch of interfaces from existing classes into it.

Here's the interface you need to implement if you are writing a plug-in:

[InheritedExport]
public interface IHalfwitPlugIn
{
    void Initialize(ContainerBuilder builder);
}

Every class that implements that interface is instantiated at runtime and given a chance to "inject" stuff into the application. More on this later.

Next is the interface you implement to define an event handler:

public interface IHalfwitEventHandler<in T> where T : IHalfwitEvent
{
    void Handle(T e);
}

And lastly, here's the only event so far in the whole program:

public class StatusesReceivedEvent : IHalfwitEvent
{
    /* plumbing omitted */

    public IEnumerable<IHalfwitStatus> Items { get; private set;  }
}

Defining your Event Handler

As a plug-in author, you want to define a new way to notify users when tweets arrive in Halfwit. You create yourself a new Class Library project, add a reference to Autofac.dll and Halfwit2.Core.dll, and create an event handler, like this:

public class Handler : IHalfwitEventHandler<StatusesReceivedEvent>
{
    public void Handle(StatusesReceivedEvent e)
    {
        /* tell the user about e.Items */
    }
}

Now you just need some way to tell Halfwit about your new handler. Do that by creating a plug-in class:

public class HandlerPlugIn : IHalfwitPlugIn
{
    public void Initialize(ContainerBuilder builder)
    {
        builder.RegisterType<Handler>().As<IHalfwitEventHandler<StatusesReceivedEvent>>();
    }
}

Note that there's nothing stopping you from implementing both the IHalfwitPlugin and the IHalfwitEventHandler<T> interfaces in the one class!

Lastly, compile your new assembly and drop the DLL into the Halfwit plugins folder. More on that later.

Loading the Plug-Ins

Here's how Halfwit locates and initializes the plug-ins when it starts up.

First, we register a new Module with Autofac called PlugInsModule:

builder.RegisterModule<PlugInsModule>();

PlugInsModule has a property called Plugins which is populated by MEF (you'll see how in a second):

[ImportMany]
public IEnumerable<IHalfwitPlugIn> Plugins { get; set; }

When the PlugInsModule.Load method is called, it first determines whether the predefined plug-ins folder exists. If it doesn't, it creates it and then returns (snice there can't be any plug-ins to load). If the folder does exist, it asks MEF to import any IHalfwitPlugin implementations it can find in that folder and populate our Plugins property:

var container = new CompositionContainer(new DirectoryCatalog(Properties.Settings.Default.PlugInPath));
container.SatisfyImportsOnce(this);

Now that the property has items in it, we can iterate over them and ask each one to initialize itself, injecting whatever it needs into our ContainerBuilder:

foreach (var p in this.Plugins)
{
    p.Initialize(builder);
}

Now all our plug-ins are loaded and initialized, ready to be called by the application.

Raising the StatusesReceivedEvent

Here's where MVVM Light's IMessenger comes into play. In our MainViewModel, which only exists once for the whole application lifetime, we register our interest in StatusesReceivedEvent messages on the message bus, like this:

messenger.Register<StatusesReceivedEvent>(this, NotifyStatusesReceived);

Our NotifyStatusesReceived method is pretty straight forward. Check out that awesome try/catch block with no exception handling! That's how hardcore developers roll! (Note: don't do this.)

void NotifyStatusesReceived(StatusesReceivedEvent e)
{
    if (_statusesReceivedEventHandlers == null) return;

    foreach (var h in _statusesReceivedEventHandlers)
    {
        try
        {
            h.Handle(e);
        }
        catch
        {
        }
    }
}

In our timeline, when new tweets come in, we simply let our application know by publishing a StatusesReceivedEvent on the message bus:

MessengerInstance.Send(new StatusesReceivedEvent(newItems));

Now the MainViewModel will receive that message, and let all our registered event handlers know about it!

The User Experience

Halfwit's UI philosophy is "keep it simple" (also known as "don't make too much work for the mabster"), so the user experience for adding and removing plug-ins is about as simple as it gets. You click on the Tools button and select Options from the dropdown menu, and you see this on the Options page:

Halfwit Plug-In Options

Yes, it's one button. All it does is open the folder that Halfwit's looking in for plug-in files. For most users, that's a magical folder living in the forbidden realm of ClickOnce-deployed applications, so they'd never find it without our help. As you can see, I've also included a "safe mode" option in case I break the plug-in system with an update or a plug-in itself starts causing problems.

Wrapping Up

This architecture not only makes it easy for people to write event handlers for new-tweet notifications, it also makes it easy for me to introduce other "events" into Halfwit for future plug-ins. I'll document them as they get created!

Halfwit 2 is currently in public beta: Download it here, and look for plug-ins here. If you're interested in writing your own, let me know!

halfwit ioc mvvm
Posted by: Matt Hamilton
Last revised: 15 Oct, 2024 09:56 PM History

Comments

22 Mar, 2011 07:22 PM

I would like to first develop a twitter client for my self and then plug-ins can you help me in it.

Regards

No new comments are allowed on this post.