WPF Application with Plugin Architecture
Ben Witt
Posted on October 15, 2024
In modern software architectures, plug-in systems offer a flexible and modular way of extending software without changing the core of the application. A specific WPF project will be used to show how a plug-in architecture can be implemented to create loosely coupled and extensible components. The focus is on the interaction between the main application, plug-ins and the use of a central event aggregator for communication between the components.
Overview over the structure
The plug-in interface
Central to any plug-in system is the definition of a clear interface that all plug-ins must implement. This creates consistency and ensures that the main application and the plug-ins can interact seamlessly with each other. In our example, this is achieved through the IPlugin interface:
namespace MyPluginInterface
{
public interface IPlugin
{
string Name { get; }
IEnumerable<Type> ViewTypes { get; }
IEnumerable<Type> ViewModelTypes { get; }
}
}
This interface specifies that each plug-in provides a name, a list of views (ViewTypes) and the associated ViewModels (ViewModelTypes). This ensures that all plug-ins can be uniformly integrated into the application.
Implementation of a plug-in
A typical plug-in in this system implements the IPlugin interface. In the following example, the plug-in provides two views and the corresponding ViewModels:
using MyFirstPlugin.ViewModels;
using MyFirstPlugin.Views;
using MyPluginInterface;
namespace MyFirstPlugin
{
public class MyFirstPlugin : IPlugin
{
public string Name => "My First Plugin with Multiple Views";
public IEnumerable<Type> ViewTypes => new List<Type>
{
typeof(MyPluginView),
typeof(MyPlugin1View)
};
public IEnumerable<Type> ViewModelTypes => new List<Type>
{
typeof(MyPluginViewModel),
typeof(MyPlugin1ViewModel)
};
}
}
This plug-in defines two views (MyPluginView, MyPlugin1View) and their associated ViewModels (MyPluginViewModel, MyPlugin1ViewModel). This makes it possible to define various UI components within the plug-in that can be integrated into the main application.
Dynamic loading and registration of plug-ins
One of the central functions of a plug-in system is the dynamic loading of plug-ins at runtime. This is done by searching for and loading assemblies that implement the plug-in interface. The following code in the main application shows how this is achieved:
private void LoadPlugins(ContainerBuilder builder)
{
var pluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
var pluginFiles = Directory.GetFiles(pluginPath, "*.dll");
foreach (var pluginFile in pluginFiles)
{
var assembly = Assembly.LoadFrom(pluginFile);
var pluginTypes = assembly.GetTypes().Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
foreach (var pluginType in pluginTypes)
{
var plugin = (IPlugin)Activator.CreateInstance(pluginType);
foreach (var viewType in plugin.ViewTypes)
{
builder.RegisterType(viewType).AsSelf().InstancePerDependency();
}
foreach (var viewModelType in plugin.ViewModelTypes)
{
builder.RegisterType(viewModelType).AsSelf().InstancePerDependency();
}
builder.RegisterInstance(plugin).As<IPlugin>();
// Registers the views and ViewModels
builder.RegisterViewsAndViewModels(assembly);
}
}
}
Here, a directory is searched for plug-ins that are stored as DLLs. Each plug-in is loaded dynamically and the views and ViewModels defined in it are registered with Autofac in the dependency injection container.
Automatic registration of views and ViewModels with Autofac
We use an extended function of Autofac for efficient registration of views and ViewModels:
public static class AutofacExtensions
{
public static void RegisterViewsAndViewModels(this ContainerBuilder builder, Assembly assembly)
{
builder.RegisterAssemblyTypes(assembly)
.Where(t => t.Name.EndsWith("View") && typeof(FrameworkElement).IsAssignableFrom(t))
.OnActivated(args =>
{
var viewType = args.Instance.GetType();
var viewModelTypeName = viewType.Name.Replace("View", "ViewModel");
var viewModelType = viewType.Assembly.GetType(viewType.Namespace + "." + viewModelTypeName);
if (viewModelType == null)
{
viewModelType = assembly.GetTypes().FirstOrDefault(t => t.Name == viewModelTypeName);
}
if (viewModelType != null)
{
var viewModel = args.Context.Resolve(viewModelType);
((FrameworkElement)args.Instance).DataContext = viewModel;
}
})
.AsSelf()
.InstancePerDependency();
builder.RegisterAssemblyTypes(assembly)
.Where(t => t.Name.EndsWith("ViewModel"))
.AsSelf()
.InstancePerDependency();
}
}
This method registers all Views and ViewModels of a plug-in based on naming conventions. Views that end with 'View' are registered and the associated ViewModels are also identified and loaded into the DI container.
Communication between plug-ins via the EventAggregator
In complex applications, the plug-ins must communicate with each other without creating direct dependencies in order to ensure modularity. In our project, this is made possible by the EventAggregator. Here is a concrete example from the MyFirstPlugin:
Plugin 1: Send message:
public class MyPlugin1ViewModel
{
private readonly IEventAggregator _eventAggregator;
public ICommand MyCommand { get; }
public MyPlugin1ViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
MyCommand = new RelayCommand(_ => OnButtonClick());
}
private void OnButtonClick()
{
var msg = new SpecialEvent("Hello from the other view internal my first plugin");
_eventAggregator.Publish(msg);
}
}
In this example, the MyPlugin1ViewModel sends a special event, SpecialEvent, with a message when the user presses a button.
Plugin 2: Receive message
The second plug-in can subscribe to this event and process the message:
public class MySecondPluginViewModel : IHandle<SpecialEvent>
{
private readonly IEventAggregator _eventAggregator;
public MySecondPluginViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
_eventAggregator.Subscribe(this);
}
public void Handle(SpecialEvent message)
{
// Process message
Console.WriteLine(message.Content);
}
}
In this example, MySecondPluginViewModel receives the SpecialEvent and processes its content in the handle method. This ensures that the two plug-ins can communicate via the EventAggregator without being directly dependent on each other.
Conclusion
A plug-in architecture offers a flexible and scalable solution for extending an application. In our example, this architecture is realised through the combination of Autofac for dependency injection and an event aggregator for communication between the components. Such systems are ideal for scenarios in which an application needs to be dynamically extended with new functionalities without changing the core of the application.
Posted on October 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.