Issues with Import

Nov 26, 2009 at 6:55 AM
Edited Nov 27, 2009 at 9:27 PM

Hi all,
I've been struggling with a problem with imports a couple of days now. My scenario is like the following:

I'm building an application based on Weifen Luo's DockPanel Suite and MEF (of course). The application consists of a "MainShell" and loadable modules. In Program.cs I load all modules and such. Pretty much as all the examples do. In MainShell I've put a StatusToolStripLabel (referred to as StatusLabel) which gets exported so that any module that want to set the text of the StatusLabel can import it and just set the text. This works fine. Perhaps not the way one should do it (since I get an intricate problem with it!), but still. The modules loads into a List of DockContent in a Lazy way and this list of views is kept in memory for the whole lifetime of the application (in MainShell form). A user logs in and can choose a view to view from the file menu. The selected view shows up in a DockPanel that is placed on MainShell form (I think you get the picture). StatusLabel is declared like this:

[Export("StatusTextLabel")]
public ToolStripStatusLabel StatusTextLabel
{
    get { return this.StatusToolStripLabel; }
}

First time the application starts it works like a charm. Every module that has imported StatusLabel can set the text property. Then the user logs out and all DockContent views are closed using a iterator in MainShell that closes all open dockcontent views. Observe, the application is not closed only the opened dockcontent views (they get disposed I noticed which seem to have an impact on my problem). Now, a user logs in again and the menuitems are made available again. The views are loaded when the user chooses one in the file menu. The view (DockContent) is shown properly, however, the import StatusLabel is no longer available. It is null?! The import is done like this in a module:

[Import("StatusTextLabel")]
public ToolStripStatusLabel StatusTextLabel { get; set; } // <= the StatusTextLabel is null second time, i.e. after DockContent is disposed?!

The DockContent views gets loaded like this:

foreach (var view in _views)
{
    string menuItemText = view.Metadata.MenuItemText;
    if (!String.IsNullOrEmpty(menuItemText))
    {
        if (menuItemText == thisItemText)
        {
            DockContent dc = view.Value as DockContent;
            if (dc == null)
                return;

            // If the DockContent form is disposed, as it will be when a user logs out,
            // we need to recreate it.
            if (dc.IsDisposed)
            {
                Type type = dc.GetType();
                
                IEnumerable<Lazy<DockContent>> exports = AppContainer.GetExports<DockContent>("HealthMonitor.ViewContract");

                foreach (Lazy<DockContent> export in exports)
                {                                
                    if (export.Value.Name.Equals(view.Metadata.Name))
                    {
                        dc = export.Value;
                        dc = (DockContent)Activator.CreateInstance(type);
                        dc.Show(mainDockPanel);
                        return;
                    }
                }
            }
            else
            {
                dc.Show(mainDockPanel);
                return;
            }
        }
    }
}

I've tried to load the views from the GetExports method with no luck. Is it possible to re-export/import the properties. Do I need to make the CompositionContainer public and let the modules find the export themselves or is this just the wrong way of doing things? Does it have anything to do with how I export my modules (parts)? I've tried Shared, NonShared... etc, etc... Does Weifen Luo's DockPanel suite have anything to do with it? Everything else seem to work like a charm. As long as I don't logout and login again! I have a feeling that disposing (closing) of the DockContent views plays a role in this drama. Not sure though.

One solution would be to just make use of HideOnClose, but I don't fancy that solution and would really like to know WHY my code fails to import the StatusTextLabel (the second time login) and HOW to correct it. I've turned my head inside out on this one! :-)

Any ideas?

Kind Regards
Julius

Nov 29, 2009 at 8:37 PM

Don't mind. I'll move to another framework.

Julius

Nov 29, 2009 at 10:44 PM

HI Julius

No mal intent, we've been on vacation since Wed so probably not spending much time on Codeplex :-).

I've looked at the code above, and have a few questions. Before digging in, can you describe exactly what you are trying to do in this code? I understand your high level goal of using the DockPanel suite.

Thanks

Glenn

 

 

 

Nov 30, 2009 at 6:29 AM

Hi Glenn!

I see! It will explain the silence! :-) Vacation is GOOD!

Well, what I'm trying to achieve is the following. In principal I just want my "modules" to be able to set the StatusTextLabel, i.e. a ToolStripStatusLabel, residing in the MainForm. It works fine the first time a user logs in. However, when I log out I close all open modules so that next user could login, the problem arises. The import StatusTextLabel is null when logging in as either the same user or a different one. Perhaps this is not the way one should do it, but I still think it is weird that I can't set the import again. I have scanned the whole forum for similar issues with no luck. I also had a look at the sample program of DynamicInstantiation and tried to implement that. I also tried GetExportedValue(...). I think I've tried pretty much all ways that doesn't work! :-) I learned a lot of ways to instantiate my DockContents and I really like MEF as a framework.

So, the problem is that when the DockPanel closes a DockContent, it disposes it and all components in it will be destroyed. I tried using HideOnClose, but then new problems arise instead. In my code I have (as most examples have) an ImportMany statement that holds all my loaded modules (DockContent). This list of views (modules) resides in MainForm, so I am able to recreate my modules. However, not the import/export part. Am I making sense?

Thanks!
Julius

Dec 1, 2009 at 10:19 PM

Ok, so I was beginning to give up hope on solving my problem and move over to another framework. A framework I'm familiar with - Smart Client Software Factory. However, for this application I thought it would be overkill to have such a large framework just to solve that small type of problem. Then, from a clear blue sky there was lightning and there was Glenn! :-)

One idea Glenn and I was talking about got me thinking - reloading and unloading my views. i.e. my DockContents. But, how to do it? Well, tonight I solved it and this is how:

I compose a container in Program.cs like this:

static void Compose()
{
    AssemblyCatalog assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
    AggregateCatalog aggregateCatalog = new AggregateCatalog();
    aggregateCatalog.Catalogs.Add(assemblyCatalog);
    
    CompositionContainer container = new CompositionContainer(aggregateCatalog);

    Application.Run(container.GetExportedValue<MainForm>());
}
I let the Main method call Compose(). I do this because I have a form (DockContent) that I want to load at start up and that is done in 
OnImportsSatisfied(). This only happens once, i.e. when the application starts.
My MainForm constructor "re-composes" the views, and this time it also includes parts in a Modules folder like this:
private void ComposeViews()
{
    DirectoryCatalog directoryCatalog = new DirectoryCatalog(@".\Modules");
    AssemblyCatalog assemblyCatalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
    AggregateCatalog aggregateCatalog = new AggregateCatalog();
    aggregateCatalog.Catalogs.Add(directoryCatalog);
    aggregateCatalog.Catalogs.Add(assemblyCatalog);

    CompositionContainer container = new CompositionContainer(aggregateCatalog);
    container = new CompositionContainer(aggregateCatalog);

    DockableViews dockableViews = container.GetExportedValue<DockableViews>();
    _views = dockableViews.Views; // declared as a private variable as Lazy<DockContent, IHealthMonitorViewMetadata>[] _views; 
}
DockableViews is declared as an Export and is a class that actually imports all my DockContent views. It looks like this:
[Export]
public class DockableViews
{
    [ImportMany("HealthMonitor.ViewContract", AllowRecomposition = true)]
    private Lazy<DockContent, IHealthMonitorViewMetadata>[] _views = null;

    public Lazy<DockContent, IHealthMonitorViewMetadata>[] Views
    {
        get { return this._views; }
    }
}
Now, when the user logs out I just call ComposeViews() again and a new fresh container will be created for me and 
StatusTextLabel will be everything else than NULL! :-)
Mission accomplished! There are probably a dozen other ways to do this in a more best practice way, but for now, I'm just very pleased to 
get it running!
I must say, that it took some serious thinking and brain twisting to get it to work and I'm very grateful for your help and support, Glenn!!
Thanks,
Julius
Dec 2, 2009 at 12:59 AM

Hi Julius

Glad we could help and that you overcame your issues :-)

Few suggestions / comments from looking at the code.

1. Looking at the sample, it doesn't look like you are using recomposition, as you DockableViews are getting destroyed when someone logs out and recreated when they log-in. If that is the case you can remove the AllowRecomposition=true, unless you indeed planning on using recomposition.

2. Instead of creating the catalog over and over at each login, I would save the catalog globally across logins. It will reduce the reflection cost by only scanning the assemblies once. Your compose method should then accept the catalog as a parameter and then create the container using that catalog.

3. I would call dispose on the container when it is released at logout in order to ensure all disposable exports are disposed, otherwise they will never be disposed.

4. Why are you using "HealthMonitor.ViewContract" as the contract? We recommend using types over strings whenever possible. Do you have multiple contracts that use the same type DockContent or is it only used for this? If it's only one, I would suggest using just DockContent for the contract, thus your code would change to:

    [ImportMany]
    private Lazy<DockContent, IHealthMonitorViewMetadata>[] _views = null;

5. Just a small suggestion, instead of creating a private backing field and a public getter, you can actually use an auto property syntax yet still have a private setter.

[ImportMany("HealthMonitor.ViewContract")]
public Lazy<DockContent, IHealthMonitorViewData>[] Views {get; private set;}

Other than, I'd say it looks good.

Regards

Glenn

Dec 2, 2009 at 6:46 AM

Thanks for your comments! I will dig into your suggestions and see if I can make the changes.

Regards,
Julius

Dec 13, 2009 at 10:44 AM

Hi all again, unfortunately I spoke too soon!

Okay, so my StatusTextLabel isn't null, however it isn't the same StatusTextLabel MainForm exports either?! Something is fishy here? No matter how I do it, I can't get the StatusTextLabel to show the values I set it to. I've done the changes Glenn suggested, but they only fixes some cosmetic things in the code.

Any ideas?

Merry X-mas!
Julius

Dec 15, 2009 at 6:24 AM

Hi again,

I may have solved my problem! I'll be back, as a famous movie star once said.

Regards,
Julius