2

I have tried the following very good tutorial https://www.eidias.com/blog/2013/7/26/plugins-in-wpf-mvvm-with-mef#cm-249 to migrate to MEF2 but for some reason the assemblies are not shown in the catalog. From MEF2 I wanted to use the API Configuration (RegistrationBuilder class) (here an example: https://stefanhenneken.wordpress.com/2013/01/21/mef-teil-11-neuerungen-unter-net-4-5/ ), maybe somebody has an idea how to apply MEF2 correctly to the tutorial. Thank you very much.

here the overview of the solution: solution overview

In the MainViewModel.cs I don't know yet how to integrate the imports into the RegistrationBuilder.Can you check the rest of the code? Thanks.

namespace WPF_MEF_App
{
    public class MainWindowModel : NotifyModelBase
    {
        public ICommand ImportPluginCommand { get; protected set; }
        private IView PluginViewVar;

       [Import(typeof(IView), AllowRecomposition = true, AllowDefault = true)]
        public IView PluginView
        {
            get { return PluginViewVar; }
            set{ PluginViewVar = value; NotifyChangedThis();}
        }

        [ImportMany(typeof(IView), AllowRecomposition = true)]
        public IEnumerable<Lazy<IView>> Plugins;

        private AggregateCatalog catalog;
        private CompositionContainer container;

        public MainWindowModel()
        {
            ImportPluginCommand = new DelegateCommand(ImportPluginExecute);
            RegistrationBuilder builder = new RegistrationBuilder();
            builder.ForType<PluginSecondScreen>()
                .Export<IView>(eb =>
                {
                    eb.AddMetadata("Name", "PluginSecond");
                })
                .SetCreationPolicy(CreationPolicy.Any);
            //.ImportProperties(pi => pi.Name == "IView",
            //        (pi, ib) => ib.AllowRecomposition());

            builder.ForType<CalculatorScreen>()
                .Export<IView>(eb =>
                {
                    eb.AddMetadata("Name", "CalculatorScreen");
                })
                .SetCreationPolicy(CreationPolicy.Any);
                //.ImportProperties(pi => pi.Name == "IView",
                //        (pi, ib) => ib.AllowRecomposition());

            catalog = new AggregateCatalog();

            string pluginsPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
            catalog.Catalogs.Add(new DirectoryCatalog(pluginsPath, "Plugin*.dll"));
            catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly(), builder));

            //also we add to a search path a subdirectory plugins
            pluginsPath = Path.Combine(pluginsPath, "plugins");
            if (!Directory.Exists(pluginsPath))
                Directory.CreateDirectory(pluginsPath);
            catalog.Catalogs.Add(new DirectoryCatalog(pluginsPath, "Plugin*.dll"));            

            //Create the CompositionContainer with the parts in the catalog.
            container = new CompositionContainer(catalog);
        }

        private void ImportPluginExecute()
        {
            //refresh catalog for any changes in plugins
            //catalog.Refresh();

            //Fill the imports of this object
            //finds imports and fills in all preperties decorated
            //with Import attribute in this instance
            container.ComposeParts(this);
            //another option
            //container.SatisfyImportsOnce(this);
        }
    }
}

Here are the two plugins: I have already commented the exports here, because they are no longer needed for RegistrationBuilder. enter image description here enter image description here

BionicCode
  • 1
  • 4
  • 28
  • 44
  • Maybe you should show how you setup the container and registered the dependencies. This way one can tell what you did wrong. – BionicCode Mar 28 '20 at 02:05
  • I'll set the code in a minute. ;-) – FriesenPole Mar 28 '20 at 07:47
  • I merged your posts into your question. *Please delete your other posts.* This are not answers, but part of your question. If you want to add information to your question or change e.g. the formatting, please edit the original question using the _edit_ button. – BionicCode Mar 28 '20 at 11:49
  • Thank you. I will now take a look at your code. – BionicCode Mar 28 '20 at 16:26
  • Sorry I'm new here. I delete the answer ;-) Thanks – FriesenPole Mar 28 '20 at 18:31
  • i saw that when merging the two posts the first image (solution overview) was not correct. i uploaded it again. – FriesenPole Mar 28 '20 at 18:43
  • Alright. Also it's better to post code instead of a screenshot. If somebody wants to test your code, he could just copy it. Apparently, copying text is not possible with images. – BionicCode Mar 28 '20 at 18:47

1 Answers1

1

I checked your attempt. A few points that need improvement.

  1. Generally you should configure the container in central location, usually at the application entry point at startup e.g. inside a method of App.xaml.cs. A class never creates its own container to import its dependencies. If this is neccessary consider to import an ExportFactory<TImport> (never pass around the container).
  2. You have to import dependencies either via constructor (recommended) or properties and not fields. Therefore you need to add get and set to the definitions of PluginView and Plugins.
  3. You should use either annotation based dependency resolving or API based. Don't mix it. Therefore you have to remove the Import attribute from all properties in the MainWindowModel.
  4. You cannot have multiple implementations of an interface e.g. IView and a single import (cardinality). You should either import a collection of concrete types, register only a single concrete type or introduce a specialized interface for each concrete type (e.g. PluginSecondScreen and ICalculatorScreen) where each interface inherits the shared interface (e.g. IView).
  5. Don't forget to dispose the CompositionContainer after you are done with initialization.
  6. SetCreationPolicy(CreationPolicy.Any) is redundant as CreationPolicy.Any is the default value which usually defaults to CreationPolicy.Shared.
  7. Try to use interfaces everywhere
  8. Avoid string literals when using class or class member or type names. Use nameof instead:
    ImportProperties(pi => pi.Name == "Plugins")
    should be:
    ImportProperties(pi => pi.Name == nameof(MainWindowModel.Plugins). This makes refactoring a lot easier.

MainWindowModel.cs

class MainWindowModel
{
  // Import a unique matching type or import a collection of all matching types (see below).
  // Alternatively let the property return IView and initialize it form the constructor,
  // by selecting an instance from the `Plugins` property.
  public IPluginSecondScreen PluginView { get; set; }

  // Import many (implicit)
  public IEnumerable<Lazy<IView>> Plugins { get; set; }
}

Interfaces IView and specializations in order to create unique types:

interface IView
{
}

interface IPluginSecondScreen : IView
{
}

interface ICalculatorScreen : IView
{
}

Interface implementations:

class PluginSecondScreen : UserControl, IPluginSecondScreen
{
}

class CalculatorScreen : UserControl, ICalculatorScreen
{
}

Initialize the application from App.xaml.cs using the Application.Startup event handler:

private void Run(object sender, StartupEventArgs e)
{
  RegistrationBuilder builder = new RegistrationBuilder();
  builder.ForTypesDerivedFrom<IView>()
    .ExportInterfaces();

  builder.ForType<MainWindowModel>()
    .Export()
    .ImportProperties(
      propertyInfo => propertyInfo.Name.Equals(nameof(MainWindowModel.Plugins), StringComparison.OrdinalIgnoreCase) 
        || propertyInfo.Name.Equals(nameof(MainWindowModel.PluginView), StringComparison.OrdinalIgnoreCase));

  var catalog = new AggregateCatalog();
  catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly(), builder));
  catalog.Catalogs.Add(new DirectoryCatalog(Environment.CurrentDirectory, "InternalShared.dll", builder));
  catalog.Catalogs.Add(new DirectoryCatalog(Environment.CurrentDirectory, "PluginCalculator.dll", builder));
  catalog.Catalogs.Add(new DirectoryCatalog(Environment.CurrentDirectory, "PluginSecond.dll", builder));

  using (var container = new CompositionContainer(catalog))
  {    
    MainWindowModel mainWindowModel = container.GetExportedValue<MainWindowModel>();

    this.MainWindow = new MainWindow() { DataContext = mainWindowModel };
    this.MainWindow.Show();
  }
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • i will have a look at it tomorrow and let you know if it worked. i must to put the kids to bed now ;-) Thanks again for your effort. – FriesenPole Mar 28 '20 at 18:47
  • i get an execution: Exception thrown: 'System.ComponentModel.Composition.ImportCardinalityMismatchException' in System.ComponentModel.Composition.dll An unhandled exception of type 'System.ComponentModel.Composition.ImportCardinalityMismatchException' occurred in System.ComponentModel.Composition.dll Es wurden keine Exporte gefunden, die der Einschränkung entsprechen: ContractName WPF_MEF_App.MainWindowModel RequiredTypeIdentity WPF_MEF_App.MainWindowModel – FriesenPole Mar 31 '20 at 06:57
  • i think it's because when you add dll's, no conventions are given. right? maybe you would have time to rebuild this example (https://www.eidias.com/blog/2013/7/26/plugins-in-wpf-mvvm-with-mef#cm-249) so that I can better understand how the whole concept works? At the end of the example is the download of the source code. That would be great :-) – FriesenPole Mar 31 '20 at 07:10
  • I've updated my answer. Indeed, the instantiation of the `DirectoryCatalog` was not correct. Check the code again, please. I imported the DLLs according to your solution structure shown in your posted image. Please verify folder paths and file names. You may have to adjust the directory paths. If you stay with `Environment.CurrentDirectory` as lookup path, then make sure all compiled assemblies (DLLs), which you want to import into the container, are copied to the execution folder (e.g. "/WPF_MEF_App/bin/Debug/"). – BionicCode Mar 31 '20 at 07:54
  • thanks so much, it works. have another question for you. how would you load the individual dll's into individual processes? does it make sense to load them into different AppDomains, so that the main application continues running when the dll crashes. here i found an example. https://stefanhenneken.wordpress.com/2012/03/05/mef-teil-10-parts-ber-exportprovider-und-app-config-in-appdomain-laden/ what do you think about this or how do you control it? – FriesenPole Mar 31 '20 at 09:02
  • This is a really complex question.It all depends on the requirements. You could ask a new question about it. – BionicCode Mar 31 '20 at 20:34
  • You have different isolation levels like same AppDomain, dedicated AppDomain and dedicated Process. Communicating across boundaries is not trivial. This involves serialization on AppDomain level or other forms of IPC (Interprocess Communication) like pipes or a simple WCF service on Process level. Processes are the safest regarding crashes/isolation. You can compile the DLL or the plugin as executable e.g., console application and execute it using `Process.Start` or create an executable host assembly at runtime using Refllection.Emit and IL generators. – BionicCode Mar 31 '20 at 20:34
  • Alternatively take a look at the MAF framework ([WPF Add-Ins Overview](https://learn.microsoft.com/en-us/dotnet/framework/wpf/app-development/wpf-add-ins-overview), [Managed AddIn Framework (System.AddIn) with WPF](http://matthidinger.com/archive/2008/10/12/managed-addin-framework-system.addin-with-wpf/)), which offers this isolation levels out of the box. If you use a dedicated AppDomain for specific DLLs, you can easily swap out DLLs, which means you can load/replace different interface implementations at runtime. – BionicCode Mar 31 '20 at 20:34
  • That's why I don't like the blog you've posted. Using a configuration file to configure imports makes MEF obsolete and is a huge step back in time, leaving all the comfort behind. Especially in the context of dedicated AppDomains. MEF allows dynamic DLL loading, so instead of maintaining a configuration file, you can simply overwrite the loaded assembly to provide a new/updated implementation. Classes which are used across AppDomains have to derive from `MarshalByRefObject`. This can be problematic as C# doesn't allow multi-inheritance. – BionicCode Mar 31 '20 at 20:35
  • Alternatively dynamically generate adapter classes, which extend `MarshalByRefObject` instead, to act as a proxy to the actual import. MEF doesn't support isolated domains naturally (opposed to MAF), so you have to implement all the stress recovery yourself e.g. unload/reload crashed assembly. – BionicCode Mar 31 '20 at 20:35
  • If it is worth the effort depends. If the plugins/modules are key or business features of the application, it doesn't matter if the whole application crashes, as a crashing plugin would be not tolerable. If the plugin is an extension, maybe a custom 3rd party module, you don't want it to crash your application. – BionicCode Mar 31 '20 at 20:35