Search code examples
wpfmvvmprismmef

MEF can't find module's views when dll in subdirectory


I try to make a little application with Prism and MEF in order to learn how it works. I'm stuck on a fairly frustrating problem. I would like to have a "Modules" subdirectory in my base app directory where I copy all the module's dll as a post build event. These modules are MVVM app with View and ViewModel.

My problem is : When I copy my module's dll in the main app directory, the views are displayed in the shell, but when my modules are in the subdirectory, nothing is displayer. My modules and their parts are found but according to fuslogvw the views can't be found :

* Assembly Binder Log Entry (27/11/2015 @ 16:45:28) *

The operation failed.

Bind result: hr = 0x80070002. The system cannot find the file specified.

Assembly manager loaded from: C:\Windows\Microsoft.NET\Framework\v4.0.30319\clr.dll

Running under executable C:\Users\mouarf\Downloads\Prism-Samples-Wpf-master\Prism-Samples-Wpf-master\HelloWorld\HelloWorld\bin\Debug\HelloWorld.vshost.exe

--- A detailed error log follows.

=== Pre-bind state information ===

LOG: DisplayName = ModuleB.resources, Version=1.0.0.0, Culture=en-US, PublicKeyToken=null

(Fully-specified)

LOG: Appbase = file:///C:/Users/mouarf/Prism/HelloWorld/bin/Debug/

LOG: Initial PrivatePath = NULL

LOG: Dynamic Base = NULL

LOG: Cache Base = NULL

LOG: AppName = HelloWorld.vshost.exe

Calling assembly : ModuleB, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.

===

LOG: This bind starts in LoadFrom load context.

WRN: Native image will not be probed in LoadFrom context. Native image will only be probed in default load context, like with Assembly.Load().

LOG: Using application configuration file: C:\Users\mouarf\Downloads\Prism-Samples-Wpf-master\Prism-Samples-Wpf-master\HelloWorld\HelloWorld\bin\Debug\HelloWorld.vshost.exe.Config

LOG: Using host configuration file:

LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework\v4.0.30319\config\machine.config.

LOG: Policy not being applied to reference at this time (private, custom, partial, or location-based assembly bind).

LOG: Attempting download of new URL file:///C:/Users/mouarf/Prism/HelloWorld/bin/Debug/en-US/ModuleB.resources.DLL.

LOG: Attempting download of new URL file:///C:/Users/mouarf/Prism/HelloWorld/bin/Debug/en-US/ModuleB.resources/ModuleB.resources.DLL.

LOG: Attempting download of new URL file:///C:/Users/mouarf/Prism/HelloWorld/bin/Debug/en-US/ModuleB.resources.EXE.

LOG: Attempting download of new URL file:///C:/Users/mouarf/Prism/HelloWorld/bin/Debug/en-US/ModuleB.resources/ModuleB.resources.EXE.

LOG: Attempting download of new URL file:///C:/USERS/Mouarf/PRISM/HELLOWORLD/BIN/DEBUG/MODULES/en-US/ModuleB.resources.DLL.

LOG: Attempting download of new URL file:///C:/USERS/Mouarf/PRISM/HELLOWORLD/BIN/DEBUG/MODULES/en-US/ModuleB.resources/ModuleB.resources.DLL.

LOG: Attempting download of new URL file:///C:/USERS/Mouarf/PRISM/HELLOWORLD/BIN/DEBUG/MODULES/en-US/ModuleB.resources.EXE.

LOG: Attempting download of new URL file:///C:/USERS/Mouarf/PRISM/HELLOWORLD/BIN/DEBUG/MODULES/en-US/ModuleB.resources/ModuleB.resources.EXE.

LOG: All probing URLs attempted and failed.

I don't know why MEF look in "modules\en-US\", I think it's probably why it doesn't find any views, but I couldn't find how to specify otherwise.

My bootstrapper :

 public class Bootstrapper : MefBootstrapper
{
    protected override void ConfigureAggregateCatalog()
    {
        base.ConfigureAggregateCatalog();

        this.AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(Bootstrapper).Assembly));
        string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Modules");
        DirectoryCatalog catalog = new DirectoryCatalog(path, "*.dll");
        this.AggregateCatalog.Catalogs.Add(catalog);
    }

    protected override DependencyObject CreateShell()
    {
        return this.Container.GetExportedValue<MainWindow>();
    }

    protected override void InitializeShell()
    {
        base.InitializeShell();

        Application.Current.MainWindow = (MainWindow)this.Shell;
        Application.Current.MainWindow.Show();
    }

    protected override void ConfigureContainer()
    {
        base.ConfigureContainer();
    }

    protected override IModuleCatalog CreateModuleCatalog()
    {
        return new ConfigurationModuleCatalog();
    }
}

My modules :

[ModuleExport(typeof(ModuleAModule))]
public class ModuleAModule : IModule
{
    IRegionManager _regionManager;

    [ImportingConstructor]
    public ModuleAModule(IRegionManager regionManager)
    {
        _regionManager = regionManager;
    }

    public void Initialize()
    {
        _regionManager.RegisterViewWithRegion(RegionNames.RightRegion, typeof(ViewA));
    }
}

My views :

/// <summary>
/// Interaction logic for ViewA.xaml
/// </summary>
[Export]
public partial class ViewA : UserControl
{
    public ViewA()
    {
        InitializeComponent();
    }
}

My viewmodels :

[Export]
public class ViewAViewModel : BindableBase
{
    private string _title = "Module A";
    public string Title
    {
        get { return _title; }
        set { SetProperty(ref _title, value); }
    } 
}

Anyone ?

Edit :

Here's the solution for who whould like to take a look : HelloWorldPrismMef

Edit 2 :

The investigation still goes on, I discovered the really handy mefx ! So my problem seems to be :

[Part] ModuleA.ModuleAModule from: DirectoryCatalog (Path="Modules")

[Primary Rejection]

[Export] ModuleA.ModuleAModule (ContractName="Prism.Modularity.IModule")

[Import] ModuleA.ModuleAModule..ctor (Parameter="regionManager", ContractName="Prism.Regions.IRegionManager")

[Exception] System.ComponentModel.Composition.ImportCardinalityMismatchException: No exports were found that match the constraint contract name

ContractName Prism.Regions.IRegionManager RequiredTypeIdentity Prism.Regions.IRegionManager n'a été trouvée.

at System.ComponentModel.Composition.Hosting.ExportProvider.GetExports(ImportDefinition definition, AtomicComposition atomicComposition)

at Microsoft.ComponentModel.Composition.Diagnostics.CompositionInfo.AnalyzeImportDefinition(ExportProvider host, IEnumerable`1 availableParts, ImportDefinition id)

Does that mean that I need to Export a IRegionManager class ?


Solution

  • The log you have posted is for an attempt to load a resource .dll, something that MEF will never load (resource .dlls are used to store application resource information, like strings for internationalization). You should look for errors that do not mention resource .dlls.

    Also, it seems to me you are attempting to edit the Prism Library HelloWorld example from GitHub. This particular example has tight coupling with ModuleA (by that I mean that ModuleA is used as a project dependency in HelloWorld) and to my knowledge you can not simply move the ModuleA.dll from the main folder to a modules folder and expect it to work.

    My suggestion would be to add a new project, set that to output to a modules folder and see if that loads (leaving the ModuleA project alone). Or you could remove the reference from the HelloWorld project and use the post build event.

    Now regarding the loading of modules from a directory, in my humble opinion, you are over complicating it. All you need is

    AgregateCatalog.Catalogs.Add(new DirectoryCatalog(@".\Modules"));
    

    Or presuming you have a convention that specifies a pattern for module file names that resembles AppName.Modules.[ModuleNameHere].dll (eg: AppName.Modules.LoginModule.dll you could use something like this to load the modules

    AgregateCatalog.Catalogs.Add(new DirectoryCatalog(@".\Modules", "AppName.Modules.*Module.dll"));
    

    Although this does not seem the case here, if you ever try to load modules from a zone deemed as untrustworthy, the default security policy is to ignore the module. This would happen if you attempt to run the application over a network connection like Windows Share. For this scenario you need to add these instructions to App.config

    <runtime>
      <loadFromRemoteSources enabled="true" />
    </runtime>
    

    These should be added after <startup /> section.

    Hope this helps you.

    Edit:

    Does that mean that I need to Export a IRegionManager class ?

    No, that's just complaining because mefx has not loaded the assembly that exports it (Prism.Wpf.dll I think it is called).

    Personally I found mefx.exe to be cumbersome; I prefer the GUI version

    Now regarding your code, I took a look at the GitHub repository and made some changes but not that many (had some issues with references with ModuleC so I had to remove and add again Prism.Mef & company):

    1. Removed the PostBuildEvent from Infrastructure project
    2. Changed the PostBuildEvent from the module projects. This needs some explaining:

      • all macros come appended with the directory delimiter "\" so you do not need to add it (I am reffering to $(OutDir)\Modules => $(OutDir)Modules).
      • COPY/XCOPY require the destination path to end with a delimiter or the destination path will be intepreted as a destinaiton directory ( $(OutDir)Modules => *$(OutDir)Modules* ).
      • Destination directory needs to exist (so first command should be MKDIR)

      I also commented (lines that start with REM are comments) out the command that copies the .pdb because I do not think it is needed and added the /z flag to XCOPY.

    3. Added ILoggerFacade as a dependency to demonstrate that the modules actually load. If you run the application from the Visual Studio Debugger, you will see some messages in the debug window.

    4. Added <loadFromRemoteSources enabled="true" /> in App.config => <configuration /> => <runtime /> so I can run the app over a mounted partition where the project is stored.

    All of this is in the PR.

    Now regarding why it will not auto-display the views in the regions, I can not say yet. I will keep investigating during my free time, but you might have better luck asking Brian Lagunas as he is one of the developers of Prism.

    Edit 2:

    As I was looking at Brian Lagunas's profile I saw he answered this post that luckily solves the issue.

    Will also add a PR to GitHub.