NuGet with MEF
Now that we have the basics of installing and uninstalling NuGet packages, the next step is integrating the contents of those packages into our application.
Comicster has two kinds of extensions: Skins and Plug-Ins. A skin is just a loose XAML file and any associated content, while a plug-in is one or more assemblies containing classes that implement one or more exported interfaces from Comicster.Core.DLL.
The Plug-Ins Class
To keep all the extensions in one place, Comicster defines a PlugIns
class that it only tracks a single instance of. The public face of the class looks like this:
public class PlugIns
{
public PlugIns(IPackageManager packageManager)
{
...
}
[ImportMany(AllowRecomposition = true)]
public ObservableCollection<ICollectionReader> Readers { get; private set; }
[ImportMany(AllowRecomposition = true)]
public ObservableCollection<ICollectionReaderWriter> Writers { get; private set; }
[ImportMany(AllowRecomposition = true)]
public ObservableCollection<ITool> Tools { get; private set; }
[ImportMany(AllowRecomposition = true)]
public ObservableCollection<ICatalog> Catalogs { get; private set; }
[ImportMany(AllowRecomposition = true)]
public ObservableCollection<ISearch> Searches { get; private set; }
public ObservableCollection<Skin> Skins { get; private set; }
public event EventHandler Composed;
}
... and we register it with Autofac like this:
builder.RegisterType<PlugIns>().SingleInstance();
So we're making use of MEF's ImportMany
attribute here, and we're giving it the ability to "recompose" each collection at runtime. Essentially that means that any changes to the MEF CompositionContainer
will automatically add or remove components from each of those collections.
NuGet Package Events
You'll notice that the PlugIns
class takes a dependency on IPackageManager
. When we resolve an instance of PlugIns
, Autofac passes an implementation of IPackageManager
, also registered as single instance. That means that inside our constructor we can subscribe to some key events to do with packages:
_packageManager.PackageInstalled += _packageManager_PackageInstalled;
_packageManager.PackageUninstalled += _packageManager_PackageUninstalled;
Now we're getting somewhere! Whenever a package is installed or uninstalled, the PlugIns
class will be notified!
Package Installation
Let's have a look at what happens at install time:
void _packageManager_PackageInstalled(object sender, PackageOperationEventArgs e)
{
AddDirectoryCatalog(e.Package);
Skins.AddRange(_packageManager.PathResolver.GetSkinsFor(e.Package));
OnComposed(EventArgs.Empty);
}
Ok, so we're calling a helper method, registering any skins that might be in the package, and then triggering the Composed
event, which gets handled in other parts of the app. The first method takes care of registering any new assemblies with MEF:
void AddDirectoryCatalog(IPackage package)
{
var libDir = Path.Combine(_packageManager.PathResolver.GetInstallPath(package), "lib");
if (Directory.Exists(libDir)) _catalog.Catalogs.Add(new DirectoryCatalog(libDir));
}
If you're familiar with NuGet you'll know that assemblies live in a "lib" folder inside the package. Here we're simply adding a new DirectoryCatalog
pointing to that folder. MEF will scan the folder for assemblies that export types we need (in this case, plug-in types) and add them to our collections.
Now let's look at the other thing that might happen as a package is installed. We're using an extension method called GetSkinsFor(package)
which looks like this:
public static IEnumerable<Skin> GetSkinsFor(this IPackagePathResolver resolver, IPackage package)
{
return from path in package.GetContentFiles().Select(f => Path.Combine(resolver.GetInstallPath(package), f.Path))
where path.EndsWith("skin.xaml", StringComparison.OrdinalIgnoreCase)
select new Skin(Path.GetFileName(Path.GetDirectoryName(path)), new Uri(path, UriKind.Absolute));
}
That's a LINQ query which looks for "skin.xaml" file in folders under the package's "Content" folder, and constructs an instance of our internal Skin
type for each one.
Package Removal
With me so far? If so, the stuff that happens when a package is removed shouldn't come as any great surprise:
void _packageManager_PackageUninstalled(object sender, PackageOperationEventArgs e)
{
var libDir = Path.Combine(e.InstallPath, "lib");
foreach (var catalog in _catalog.Catalogs.OfType<DirectoryCatalog>().Where(c => e.FileSystem.PathsMatch(c.FullPath, libDir)))
{
_catalog.Catalogs.Remove(catalog);
}
foreach (var skin in _packageManager.PathResolver.GetSkinsFor(e.Package))
{
Skins.RemoveAll(s => s.Uri == skin.Uri);
}
OnComposed(EventArgs.Empty);
}
So we're looking for any DirectoryCatalog
instances in our MEF catalogs that match the path of the package that just got uninstalled, and removing them so that their plug-in classes are "unregistered". Then we're doing the same thing for the skins.
Caveats
There are a few problems with this approach, and the #1 problem is that Comicster still has the assemblies loaded when the packages are uninstalled, so NuGet can't delete the folders from disk. There's a way around that using a custom AppDomain with shadow copying enabled, but it's a bit of a learning curve so I haven't tried it yet.
Likewise when you update a package, the old version will still be loaded. I've gotten around that for now by notifying the user that they should restart Comicster when any package is updated.
What do you think? MEF-heads might be able to suggest a cleaner approach, but this is working quite well for me. If I get to the AppDomain stuff I will post that too!
Trackbacks
- Comment utiliser NuGet pour gérer des extensions dans ses propres applications | Around computing | http://muibiencarlota.wordpress.com/2012/02/24/comment-utiliser-nuget-pour-gerer-des-extensions-dans-ses-propres-applications/
- Dew Drop – June 19, 2011 | Alvin Ashcraft's Morning Dew | http://www.alvinashcraft.com/2011/06/19/dew-drop-june-19-2011/
- Extend NuGet Command Line | WP7 Developers Links | http://wpdevelopers.ginktage.com/2011/07/15/extend-nuget-command-line/
- NuGet with MEF « Mas-Tool's Favorites | http://mas-tool.com/?p=2751
- rimonabantexcellence site title | http://www.rimonabantexcellence.com/t.php?ahr0cdovl21hdhroyw1pbhrvbi5uzxqvbnvnzxqtd2l0ac1tzwy=
No new comments are allowed on this post.
Comments
No comments yet. Be the first!