Apr 4, 2012

MEF 2 Preview - Making tabs or windows share parts and scope

I love MEF, but it isn't without its shortcomings. In previous versions, a big shortcoming was it's lack of anything other than shared or non-shared parts. Either it returned a new instance each time a given type was needed, or everybody got the same one. There was no [simple] way to define scope.

What do I mean by that? Let's take a classic example: a tabbed MDI interface like Visual Studio. You can imagine that each "tab", usually a code file or something, is represented by, say, a DocumentViewModel object. Underneath him you might have many different service or sub-viewmodel dependencies. With MEF as it is, you'd probably set things up like so:

  • DocumentViewModel [Non-Shared]
    • ContentViewModel [Non-Shared]
      • CodeFileViewModel [Non-Shared]
        • IntelliSenseService [Shared]
        • FileService [Non-Shared]

And so on. Each tab created would generate a new DocumentViewModel instance, which would also in turn generate a new ContentViewModel and CodeFileViewModel (if it were a code file you were opening, say) and other things. The IntelliSenseService would likely be shared, since it "learns" from your typing anywhere and it also maintains all the type information gotten from the background compilation service. The FileService, though, would be specific to the actual code file, and would be responsible for the lower-level file I/O calls to the file system.

But say you want to spawn a modal dialog due to user interaction - say it is a Find and Replace window. Pretty standard. This dialog needs a way to make text changes. Whether that's a text service or whatever, it doesn't matter, it will have some kind of dependency. MEF, however, is hamstrung. Either it gives your FindAndReplaceViewModel a new instance of the service or it gives it a shared global one. A global won't work really because it needs to be specific to the document and, also, it makes more sense as far as atomic actions and not stepping on the toes of other open documents. What to do?

Ideally, you'd want MEF to know that some types share scope, such that types marked as shared are only shared within that scope. Shared types that don't define a scope would, implicitly, be globally shared just like before. Suddenly with scope, we can define all of the various viewmodels and services under DocumentViewModel as shared within a "Document" scope.

You can sort of do this with nested containers, but it is not trivial to set up and it wasn't exactly meant for that so there are a lot of potential trip-ups along the way. Thankfully, the MEF folks have noticed this need and it'll be in the next version of MEF set to ship with .NET 4.5!

You can now define a scope graph via nested CompositionScopeDefinition objects and their catalogs. In this way, we can get the desired behavior of sharing types within the scope of something like a document model. Lots of information on this plus an example can be found on the BCL team's blog. Their example is really better than mine, admittedly, but it is a bit simple in that it uses TypeCatalogs. Typically, in your standard WPF app, you'll probably be using AssemblyCatalogs. But wait, how do I split up my types within my assemblies?

Well, you could always use TypeCatalogs, too. This isn't ideal, because every time a type is added or removed, you'll need to update your bootstrapper. The whole point of the AssemblyCatalog was to provide a way to suck up all the exports at once, dynamically searching the assembly. At worst you'd need to define extra AssemblyCatalogs if you added a new assembly reference to your project (and that's where the DirectoryCatalog might come in handy).

Amongst the many new features, including the above scoping feature, you can now also perform filters on your catalogs (returning a new FilteredCatalog object). Using simple ExportMetadata, you can fairly simply wire-up some filters to split up your catalogs into appropriate "levels" for defining scope.

On all of the types beneath and including DocumentViewModel, add an [ExportMetadata("Scope", "Document")] attribute. Then, in your bootstrapping code (where you create your catalogs and such), add all of your various AssemblyCatalogs and whatnot to your AggregateCatalog as normal. Then:

var appLevelCatalog = masterCatalog.Filter(cpd => !cpd.ExportDefinitions.Any(ed => ed.Metadata.ContainsKey("Scope")));
var docLevelCatalog = masterCatalog.Filter(cpd => cpd.ExportDefinitions.Any(ed => ed.Metadata.ContainsKey("Scope") && ed.Metadata["Scope"].ToString() == "Document"));

After that, define your scope tree just like in the BCL Team's example linked above.

var scopeDefinition = new CompositionScopeDefinition(
    new[] { new CompositionScopeDefinition(docLevelCatalog, null) });

var container = new CompositionContainer(scopeDefinition);

I'm sure you can find other ways, but this works well enough and it fits in nicely with the existing MEF attribute decoration style of doing things.

No comments:

Post a Comment