How do I handle parts that are both shared and non-shared?

Mar 2, 2009 at 5:29 PM

I am struggling with a composition problem that I can’t seem to see my way clear of. The gist of the problem is that I have a set of objects (call them “Documents”), each of which can have zero or more instances, and each of which aggregates other objects (call them “DocumentParts”). Each DocumentPart requires knowledge of the Document that contains it. From the point of view of the DocumentPart, the Document is shared (i.e. all DocumentParts contained by a given Document all refer back to that particular Document instance). However, from the point of view of the rest of the application, the Document is non-shared (i.e. there can be multiple instances of a given type of document).

Documents are created at runtime based on a request from the end user. How can I structure MEF to cleanly handle the composition for me? Essentially, each Document needs to import each of the DocumentParts it needs – that’s easy. However, each DocumentPart needs to import the specicfic Document that caused its creation. That’s the bit I can’t figure out how to do.

I am using the latest MEF drop in case that makes a difference to the answer. I have been reading all about container hierarchies and filtered containers because that’s where I suspect the answer lies, but I can’t quite see the solution.

In pseudo-code, it looks something like this:

public class Application

{

    // A list of all instantiated documents.

    private List<IDocument> _documents = new List<IDocument>();

   

    // An imported list of all the available document types the user can create. The application presents

    // these choices to the user via a menu or other mechanism.

    [Import]

    private ExportCollection<IDocument> _availableDocumentTypes = null;

 

    // The following is called at runtime based on a user request to create a new instance of a given type of document.

    public IDocument CreateDocument(string requestedType)

    {

        // The following line is just pseudo-code representing the creation of a new instance of the requested document type.

        // I use MEF to perform the creation so the implementation details of the document are hidden from this class. When

        // the imports of this new instance are satisfied as part of the creation process, it causes the creation of the

        // aggregated DocumentParts.

        IDocument doc = _availableTypes[requestedType].GetExportedObject<IDocument>();

 

        // Add the new instance to our list of existing instances and return the new instance to the caller.

        _documents.Add(doc);

        return doc;

    }

}

 

public class DocumentA : IDocument

{

    // This import triggers the creation of a new DocumentPart1 instance.

    [Import(RequiredCreationPolicy = CreationPolicy.NonShared)]

    private IDocumentPart1 _docPart1;

 

    // This import triggers the creation of a new DocumentPart2 instance.

    [Import(RequiredCreationPolicy = CreationPolicy.NonShared)]

    private IDocumentPart2 _docPart2;

}

 

[Export(typeof(IDocumentPart1))]

[CompositionOptions(CreationPolicy = CreationPolicy.NonShared)]

public class DocumentPart1 : IDocumentPart1

{

    // How do I get this import to give me the correct IDocument instance??

    [Import]

    private IDocument _containingDoc = null;

}

 

[Export(typeof(IDocumentPart2))]

[CompositionOptions(CreationPolicy = CreationPolicy.NonShared)]

public class DocumentPart2 : IDocumentPart2

{

    // How do I get this import to give me the correct IDocument instance??

    [Import]

    private IDocument _containingDoc = null;

}

Developer
Mar 6, 2009 at 5:18 AM
First question I would have is how exactly is your CreateDocument function constructing documents? First off the GetExportedObject function on an Export object doesn't act as a factory method and construct you a new instance every time you call it, even if the Document type is NonShared. So you need to figure out a way to construct your documents. One possible way would be to create a DocumentFactory contract and import a set of those that will you can use to construct unique document instances. Another way might be to pull on the Container (or ExportProvider) directly because each of the calls to exportProvider.GetExportedObject will construct a new instance for you if the type is marked as NonShared.

More directly to your question, the issue your running into is that you want the DocumentPart's to import a specific instance of a document. That's generally referred to as instanced based wire-up and currently MEF doesn't have a way to support that automatically. Two approaches come to mind on how to approach this:
1) Add a SetDocument(IDocument) method to your base IDocumentPart interface and manually set the document during the ImportedCompleted callback on your Document.
2) Create a CompositionContainer which contains a catalog for just the IDocumentParts for each Document you construct and manually add the single IDocument instance to that Container and compose it. With this approach you are sort of scoping the container in such a way that it has information to construct all the IDocumentParts but it only contains a single IDocument instance so when those IDocumentParts import an IDocument they will all get the same single instance.

Hope that helps,
Wes
Mar 6, 2009 at 4:01 PM
Edited Mar 6, 2009 at 4:01 PM
Wes,

Thanks for your reply. I am constructing the documents using the container, so I am in fact getting a new Document instance each time. When I wrote my original posting, I did not realize that an Export object caches the object it creates and therefore does not act as a factory. That became evident soon afterward :-(

I have been resisting implementing your first suggestion simply because it seemed like it was short-circuiting the composition process - each document needs to have intimate knowledge of the aggregated parts and has to remember to provide the part with a reference to itself - until it does that the aggregated part is not valid. Maybe that's OK, but I was looking for something more like your 2nd suggestion and that is where I hit the roadblock.

Part of the problem is that aside from the Document instances, the composition container also provides a whole bunch of other parts for the application (i.e. logging service, license service, the application object itself, etc., most of which are singletons). Each DocumentPart needs access to all these infrastructure parts as well as to just its own Document instance. If I create a second container for the Document and seed it with the Document instance, how do the DocumentParts get access to all the infrastructure? Seeding the new container with all the infrastructure instances is not practical, and if I make the documnent container a child of the main container, I back to square 1 - the Document will again be NonShared won't it?

It almost seems like what I need to do is this (and please correct me if I am out to lunch or there is a better way):
1) Create my main composition container with catalogs for everything except "Document".
2) Make "Document" a Shared part
3) When it is time to create a new Document, create a new composition container with a catalog containing just Document and make this new container a child of the main composition container .
4) Pull the new Document instance from the Document container.

If I do this, the Document and all its aggregated parts are created from the Document container. They have access to all the shared infrastructure parts (because the Document container is a child of the main container), yet Document is a singleton.

If this sounds correct, I have one other question. Is there a way to remove a part from a catalog? My "Document" implementation is currently part of an assembly that also includes many of the other infrastructure parts. It would be painful to have to strip it out into a separate assembly. I'd really like to just push the whole assembly into a catalog and then remove the one part from consideration.

One last point on the topic of instance-based wire-up. It would be very helpful if this could be accomplished by simply providing a constructor that has a single parameter of the appropriate type and marking that constructor as the ImportingConstructor. The framework would activate the instance using this constructor and provide a handle to the object that triggered its creation. This would handle the case where activation was triggered by a field or property import. You could go a step further when the activation is triggered by "GetExportedObject()" by providing an overload that accepts additional parameters to be passed to the constructor. Based on the number and type of these parameters, MEF could look for a constructor that matches.

Thanks,
Ron