Using NuGet for Application Plug-Ins

Comicster now uses NuGet to manage its plug-ins and skins (which I collectively call "extensions"). Using NuGet in your own application is easy! Let's go through the core parts:

First you'll need a reference to NuGet.Core. You can install this using NuGet itself, but be aware that as of this writing (version 1.4) the assembly is only for the full .NET Framework "extended profile" - Client Profile applications won't build if you use NuGet.Core. I'm currently using a custom build of the assembly until version 1.5 is released.

Once you've referenced NuGet.Core, you'll need a repository from which to install packages. There are a couple of different kinds, but the easiest way to get started is to point to a local folder on your machine. Either way, use the PackageRepositoryFactory to spin one up:

var repository = PackageRepositoryFactory.Default.CreateRepository(@"C:\Packages);

Next you'll need a package manager with which to install, update and remove packages. You'll need to tell it where to get them from (the repository) and where to put them (the destination folder):

var manager = new PackageManager(repository, @"D:\Extensions");

In Comicster, I use "My Documents\Comicster\Extensions" for the destination, calculated like this:

Path.Combine(
    Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Comicster", "Extensions")

Now that you have a package manager, you can fetch a list of packages to show your user. Comicster has an ExtensionsViewModel that does this. Here's its "Refresh" method:

IEnumerable<IActivity> Refresh()
{
    PageMessage = "Retrieving extensions...";

    var fetcher = new PackageFetcher(_packageManager, _pageSize, _offset, SelectedFilter.Key);
    yield return fetcher;

    _packages.Clear();
    _packages.AddRange(fetcher.Packages
        .Select(p => new ExtensionViewModel(this, MessengerInstance, p, _packageManager)));

    // the total count of packages, not the same as _packages.Count!
    _packageCount = fetcher.PackageCount; 
}

A couple of things to look at here.

First, note that my Refresh method is declared to return an IEnumerable<IActivity>. Because the act of fetching packages should be asynchronous (important if you're going off to a web feed), it's using the coroutine support in MadProps.MvvmLight. I've made a PackageFetcher activity to do the heavy lifting. I won't post the entire code here, but the crux of it looks something like this:

_packageManager.SourceRepository.GetPackages()
                                .Where(p => p.Tags.Contains(_filter))
                                .OrderBy(p => p.Id)
                                .Skip(_offset)
                                .Take(_pageSize)
                                .ToList();

Phew! The GetPackages() method returns an IQueryable<IPackage>, which means we can perform LINQ operations on it from the client and only the overall query will be sent to the "server" (in the case of using web feeds). You can see that I'm paging my results here using Skip() and Take().

In Comicster I wrap packages up in an ExtensionViewModel instance which has commands for installing, updating and removing packages. Installing looks like this:

void Install()
{
    try
    {
        _packageManager.InstallPackage(_package, false);
        _host.NotifyPackages();
    }
    catch (Exception ex)
    {
        MessengerInstance.Send(new GalaSoft.MvvmLight.Messaging.DialogMessage(ex.Message, null));
    }
}

So the first thing I do there is attempt to install the package, passing false for the "ignoreDependencies" command because I want them if they're needed.

I then notify the host ViewModel (the ExtensionsViewModel) that a package has been installed, so that it can update the view for all the other extensions, in case a dependency has also been installed.

Lastly, in the event of an exception I simply show a message to the user (using MVVM Light's messaging system).

Updating and removing packages work exactly the same way. IPackageManager gives you UpdatePackage() and UninstallPackage() methods that work predictably.

The last thing to talk about in this post is the package repository we used right at the top. Comicster, of course, doesn't use a local folder for its package source - it uses a web feed. Well, actually it uses both. If you want plug-in developers to be able to test their packages before uploading them, you need to give them an easy way to do that.

To use two different repositories simultaneously you can use the AggregateRepository class. Here's what Comicster does:

var repository = new AggregateRepository(
    new [] {
        PackageRepositoryFactory.Default.CreateRepository("http://extend.comicster.net/nuget"),
        PackageRepositoryFactory.Default.CreateRepository(packagesFolder)
        });

You can see I'm referencing a web feed (at the Comicster Extensions site) as well as my local folder, but I'm simply using the PackageRepositoryFactory to create the repositories. That's pretty easy!

To learn about hosting your own NuGet feed, I recomment a read through of this post from Phil Haack. His NuGet.Server package makes it a snap!

In a future post I'll write about integrating the packages you've downloaded into your app using MEF. Stay tuned!

mvvm nuget
Posted by: Matt Hamilton
Last revised: 25 Oct, 2014 09:25 AM History

Trackbacks