Plugins in WPF MvvM with MEF 2

1k views Asked by At

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

1

There are 1 answers

11
BionicCode On BEST ANSWER

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();
  }
}