Comment Feed for Channel 9 - Creating An Application With Full Plug-in Supporthttp://video.ch9.ms/ecn/c4f/images/9941747_100.jpgChannel 9 - Creating An Application With Full Plug-in Support
In this article, you'll learn how to create an application with full plug-in support.
Introduction
It's been a number of months since I released the
first version of my Utility Runner application. Utility Runner makes it possible to run system utilities with as little overhead as possible: Instead of lots of tray icons, numerous EXE's, and the associated memory and startup time overhead, this application
manages multiple utilities from one place.
To open this solution, you'll need Visual Studio 2008 Express Edition (Visual C# or Visual Basic), at least. If you don't have it yet, you should get it!
Working with MEF
Little has changed functionally from the last version of the application, beyond some refactoring that's taken place since the new version of MEF was released. For one thing, the attribute for marking an export is no longer sealed, so you can now create
a subclassed attribute that encompasses the export name and any other metadata included with it. Consumers can simply use your new custom attribute for a stronger-typed experience. In my case, I created a WpfServiceMetadata class. Read on for more information.
Managing Addins
My big challenge was figuring out how to implement an addin manager to support multiple utility addins, like in Firefox. MEF lets you easily mark what classes are addins and where they should fit into your overall code, but I wanted to take it further.
I decided that my goals were:
The ability to load an addin through the UI The ability to disable/enable addins The ability to remove an addin
There are a number of ways to achieve these goals. Mine isn't the ideal way, but it works pretty well nonetheless.
Loading Addins
Loading addins is mostly handled by the MEF Framework itself, but there is some manual work involved as well. I decided that instead of scanning a folder for DLL's like in the first version, I'd take things a step further: I came up with a simple packaging
format to contain the addin and its associated files.
The packaging format is simply a Zip file renamed with the .util extension. The file contains at least the DLL of the addin itself, but might also include associated libraries, images, sounds, or other resources. I toyed with creating a manifest file to
help supply addin information that didn't require class loading, but I kept it simple for now.
Before loading DLL's with addins, the AddinManager class will scan the addins folder for .util files and unzip them to same-named folders. Each of these folders gets added to the list of folders in which MEF searches. The original file then gets
deleted.
Adding an addin
An application that uses addins needs a way for users to add new ones. The easiest method would be to have the user just drag new .util files into the Addins folder, but this isn't very user-friendly. I wanted to make it easy for a user to click something
in the application to install an addin.
If the user clicks the Add New Addin From File button, they use a file browse button to locate the file. The application then copies that file to the Addins folder and notifies the user that it will be loaded on next startup. It would be great to
load the addin immediately, and MEF supports that, but I decided to skip dynamic addin control for simplicity.
Instead of going through the dialog when loading an addin, it would be nice to support double-clicking. To do this, an association needs to be made from the .util extension to the executable. If you launch the executable with a file argument and it's the
right format, it will automatically copy it to the Addins folder. If an instance is already running, the new instance will just exit. Similarly, if you launch the executable when another instance is running, the existing instance will show itself and the
new instance will exit.
It's important to note that standard users don't have write access to the Program Files
folder. For this reason, addins need to be stored in the user's local profile folder. System-level addins can be stored in the
ProgramData folder. Addins could be in the user's roaming profile folder, but I haven't thought through all of that yet. (For example, if a user logs into different machines, an addin might not run on each system due to different hardware configurations.)
Disabling an addin
Disabling an addin should be a simple matter of renaming the addin folder. You can't do this at runtime though, since the DLL's are locked and loaded. As an alternative, I create a file (zero-length) with the same name and add the .disable extension to
it. I check for this upon startup and before loading, and can handle it properly.
Enabling and removing addins are handled similarly, using .enable and .delete extensions on the zero-length files.
Creating Addins
To create an addin, you need to 1) implement the IWpfService interface, and 2) add the
WpfServiceMetadata attribute with a name for your addin. From there, be sure to implement all the methods in the interface. The
Start method is called at initialization (you shouldn't take much time in the constructor), and
Stop is called at the end, for cleanup. Any time you want to update the
Status, be sure to raise the StatusChanged event.
[WpfServiceMetadata(&quot;SampleAddin&quot;)]
public class SampleAddinImpl : IWpfService
{
}
The final thing you'll need to do is copy your addin DLL to a folder with your addin name, plus the .util extension (Yes, an extension on the folder name. I do this in Visual Studio with a post-build event. If you debug the application, it looks for an
Addins folder under the current directory. When started normally (such as when installed) it uses the local user profile.
When you're ready to distribute the addin, compress the DLL and any supporting files into the top-level of a ZIP file, then rename the .zip extension to .util. You can load this from the Addins page of the app settings, or manually move it to the
MefUtilRuner\Addins folder in your local profile folder (c:\users\{USERNAME}\AppData):
TimedQueue
Message boxes, balloon help, status bars—these are all great ways to display a message to the user, but they all only show one string at a time. If you call it again before the reader has the chance to see it, it's just gone. There are some nice message
managers that make it easy to manage stacking alerts, but I decided to stick with the built-in balloon help provider, and just manage how often I send changes. It's basically a buffered balloon provider that will only show messages every x number of seconds,
regardless of how many attempts the application makes. To use it, simply add items to the collection. An event is raised whenever an item is available. If an item has exceeded the maximum time-to-live threshold, the users of the collection never see it.
// Repeat while there are items (one pulse may occur with several items ready)
while (item != null)
{
// If this is an old item, no event is raised. This could happen if
// a process goes into a loop and dumps a large number in a very short
// amount of time.
if (DateTime.Now.Subtract(item.TimeStamp).TotalMilliseconds &lt; _maxTTL)
{
RaiseEvent(item.Item);
Thread.Sleep(_interval);
}
item = default(ItemWrapper&lt;T&gt;);
lock (_lock)
{
if( _items.Count &gt; 0 )
item = _items.Dequeue();
}
}
The main window creates an instance of the TimedQueue class. Every time an addin wants to display a message, it's added to the collection. When
ItemAvailableEvent fires, it's dispatched to the UI thread to be displayed. Dispatching prevents cross-threading issues between the background
TimedQueue thread and the UI thread.
void statuses_ItemAvailableEvent(object sender,
ItemAvailableEventArgs&lt;StatusMessage&gt; e)
{
Dispatcher.BeginInvoke((ThreadStart)delegate() {
StatusUpdatedHandler(e.Item); }
, DispatcherPriority.Background);
}
Though this instance is being used for status messages, you can also use TimedQueue
for other purposes, when you are willing to lose old messages. One example would be to throttle user input, such as a game that doesn't want constant firing or jumping as fast as the user clicks a button.
Next Steps
There are a number of things that would be nice to have in this application. It would be good to allow utilities to extend the context menu to enable or disable something, or at least to jump straight to their configuration page.
Using ClickOnce to deploy utilities would also be nice since it's so clean for the user, but there is a cost: ClickOnce is very strict about how applications can interact with the system. They are always per-user, and they live in “secret” folders. Developers
can't read or write to the hard drive except to protected storage (similar to the iPhone I suppose). I'm not sure if they are restricted in other ways, but it could cause a hardship.
It would also be nice if addins could be added at runtime. This is a fairly simple case, but I didn't get to it yet. Furthermore, if you add, you might expect to enable/disable/remove at runtime too. Unfortunately, disabling and removing aren't really
possible. Sure, I could call Stop on a utility and remove it from the UI (and not call
Start on it next time), but that's not really disabled since it could still be running until the next restart. I couldn't force it to die unless I used separate application domains, which I'm loath to do.
Finally, I'd really like to get an “app store” type of repository going. I imagine being able to publish utilities like Sidebar Gadgets or Windows Live Writer Plugins so users can browse, read information, and click-and-install. That could be a really
great way to make the idea take off.
Conclusion
There are definitely some rough edges at this point, but it's getting there. The source code is fully available and I'd be willing to give commit access to anyone interested in moving things forward. Just drop me a line!
About Arian
Arian Kulp is a software developer living in Western Oregon. He creates samples, screencasts, demos, labs, and articles; speaks at programming events; and enjoys spending time with his family.
enThu, 22 Feb 2018 05:22:01 GMTThu, 22 Feb 2018 05:22:01 GMTRev9