As recommended by johannesnestler, I like to use
MEF[
^] for apps like these.
This is going to be a long answer but hang in there, it will be worth it...
I've made a couple of examples for you so that you have an idea of how you could do it using MEF:
1. Console Solution - This demonstrates how MEF can work;
2. WPF app with modules - This expands on the Console example with autoloading modules
Firstly, here is a simple console app example that shows how MEF works:
internal static class Program
{
private static void Main()
{
Mef.Initialize();
var process = new Process();
process.Service?.Print();
Console.ReadKey();
}
}
class Process
{
public Process()
{
Mef.ComposeParts(this);
}
[Import]
public DummyService Service { get; set; }
}
[Export]
class DummyService
{
public void Print()
{
Console.WriteLine("Running Service...");
}
}
MEF does all the heavy lifting for you.
[Export]
has a matching
Import
and does it's magic when you ask it to:
Mef.ComposeParts(this);
. Here is the (shorterned) class that I use to wrap MEF:
public static class Mef
{
private static object lockObj = new object();
private static CompositionContainer container;
private static AggregateCatalog catalog;
public static void Initialize()
{
catalog = new AggregateCatalog();
container = new CompositionContainer(catalog);
}
public static void ComposeParts(params object[] attributedParts)
{
lock (lockObj)
container.ComposeParts(attributedParts);
}
}
The
Initialize()
method stitches all the Imports & Exports together.
Now, for the WPF plugins and loading/displaying UserControls, a little bit more work is required. The solution is made up of 3 parts:
1. Common plugin contact Dll
2. Main WPF app
3. The modules containing the UserControls
The Common plugin is made up of:
1. The
MEF
Helper Class
2. A
UIProvider
- metadata that describes the plugin used by the main app
3. An
IView
interface - used to bind the UserControl to the UIProvider
4. An
ExportView
attribute - For naming unique IViews that are exported
The revised
MEF
class that finds plugin modules and loads them in realtime:
public static class Mef
{
private static object lockObj = new object();
private static CompositionContainer container;
private static AggregateCatalog catalog;
private static void Initialize()
{
catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(path: ".", searchPattern: "*.exe"));
catalog.Catalogs.Add(new DirectoryCatalog(path: ".", searchPattern: "*.dll"));
container = new CompositionContainer(catalog);
}
public static void Initialize(bool? isRecomposable = true)
{
Initialize();
if (isRecomposable == true)
StartWatch();
}
public static void Initialize<T>(T attributedPart, bool isRecomposable)
{
Initialize(isRecomposable);
if (isRecomposable)
ComposeParts(attributedPart);
else
lock (lockObj)
container.SatisfyImportsOnce(attributedPart);
}
public static void ComposeParts(params object[] attributedParts)
{
lock (lockObj)
container.ComposeParts(attributedParts);
}
private static void StartWatch()
{
var watcher = new FileSystemWatcher() { Path = ".", NotifyFilter = NotifyFilters.LastWrite };
watcher.Changed += (s, e) =>
{
string lName = e.Name.ToLower();
if (lName.EndsWith(".dll") || lName.EndsWith(".exe"))
Refresh();
};
watcher.EnableRaisingEvents = true;
}
public static void Refresh()
{
DispatcherHelper.CheckBeginInvokeOnUI(() =>
{
foreach (DirectoryCatalog dCatalog in catalog.Catalogs)
dCatalog.Refresh();
});
}
}
public static class DispatcherHelper
{
public static Dispatcher UIDispatcher
{
get;
private set;
}
public static void CheckBeginInvokeOnUI(Action action)
{
if (action == null)
return;
CheckDispatcher();
if (UIDispatcher.CheckAccess())
action();
else
UIDispatcher.BeginInvoke(action);
}
private static void CheckDispatcher()
{
if (UIDispatcher == null)
{
var error = new StringBuilder("The DispatcherHelper is not initialized.");
error.AppendLine();
error.Append("Call DispatcherHelper.Initialize() in the static App constructor.");
throw new InvalidOperationException(error.ToString());
}
}
public static void Initialize()
{
if (UIDispatcher != null
&& UIDispatcher.Thread.IsAlive)
return;
UIDispatcher = Dispatcher.CurrentDispatcher;
}
}
The
DispatcherHelper
is used to avoid any cross thredding issues.
Now the base UIProvider. This allows for multiple types like
UserControl
and
Page
types and describes each plugin to the hosting app:
public interface IUIProviderBase
{
string Key { get; }
string Title { get; }
}
public abstract class UIProviderBase : IUIProviderBase
{
public string Key { get; set; }
public string Title { get; set; }
}
And base
UIViewProviderBase
implementation that has a method for providing the UserControl, the marker interface
IView
, and a custom
Export
attribute for the
UserControl
:
public interface IUIViewProviderBase : IUIProviderBase
{
ExportFactory<IView> Entry { get; set; }
}
public abstract class UIViewProviderBase : UIProviderBase, IUIViewProviderBase
{
public abstract ExportFactory<IView> Entry { get; set; }
}
public interface IView
{
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class ExportViewAttribute : ExportAttribute
{
public ExportViewAttribute(string contractName)
: base(contractName, typeof(IView))
{
}
}
Now we can create standalone DLL/EXE plugin modules. The plugin module needs to reference the
Common plugin contact Dll above. The DLL should either be a Wpf UserControl or Wpf Custom Control project type.
Here is one Plugin example. Make as many as you want to test with...
First the plugin UIProvider to describe the plugin with the factory method for the view (UserControl):
[Export(typeof(IUIViewProviderBase))]
public class UIProvider : UIViewProviderBase
{
public UIProvider()
{
Key = "PluginA";
Title = "My Plugin A";
}
[Import("MyViewA")]
public override ExportFactory<IView> Entry { get; set; }
}
Now the plugin View (UserControl) itself:
[ExportView("MyViewA")]
public partial class View : UserControl, IView
{
public View()
{
InitializeComponent();
}
}
And here is a mock XAML for testing purposes:
<UserControl x:Class="PluginA.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:PluginA"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Viewbox>
<TextBlock Text="ViewA"/>
</Viewbox>
</UserControl>
Lastly, the WPF Host app. I like to use the MVVM design pattern, but code behind can work just as well. Make sure that you also add a reference to the
Common plugin contact Dll above.
First the ViewModel to host the plugins for the main view/window:
public class MainViewModel : IPartImportsSatisfiedNotification
{
public MainViewModel()
{
Mef.Initialize(this, isRecomposable: true);
}
[ImportMany(typeof(IUIViewProviderBase), AllowRecomposition = true)]
public ObservableCollection<IUIViewProviderBase> Plugins { get; set; }
public void OnImportsSatisfied()
{
}
}
We need a
UIExportViewConverter
class to load the plugin View (UserControl)
class UIExportViewConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((ExportFactory<IView>) value).CreateExport().Value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Now we can bind the
MainViewModel
to the
MainWindow
and display our plugins:
<Window
x:Class="PluginHost.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:PluginHost"
xmlns:converter="clr-namespace:PluginHost.Converters"
Title="MEF PLUGIN HOST EXAMPLE" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<converter:UIExportViewConverter x:Key="UIExportViewConverter"/>
<DataTemplate x:Key="ItemTemplate">
<ContentControl Height="100"
Content="{Binding Entry,
Converter={StaticResource UIExportViewConverter}}"/>
</DataTemplate>
<DataTemplate x:Key="DetailsTemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="TITLE: " FontWeight="Bold"
Margin="0 0 10 0"/>
<TextBlock Text="{Binding Title}"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding Plugins}"
ItemTemplate="{StaticResource ItemTemplate}"/>
<ListBox Grid.Row="1"
Margin="10"
Height="100"
ItemsSource="{Binding Plugins}"
ItemTemplate="{StaticResource DetailsTemplate}"/>
<TextBlock Grid.Row="2"
Margin="10"
Text="{Binding Plugins.Count, FallbackValue=Total: 0, StringFormat=Total: {0}}"/>
</Grid>
</Window>
The plugin host app will show the loaded plugins, the plugin metadata from the
UIProvider
and a total count of plugins loaded.
When you run the app, the plugins are not loaded. Copy PluginA.DLL into the app folder and the plugin will automatically load and display. The same thing will happen for every other plugin that you drop into the app folder.
Hope this helps... Enjoy! :)
*UPDATE:* This may also be of interest to you:
MEF and AppDomain - Remove Assemblies On The Fly[<a href="https://www.codeproject.com/Articles/633140/MEF-and-AppDomain-Remove-Assemblies-On-The-Fly" target=_blank" title="New Window">^]