WPF Application with Plugin Architecture

Ben Witt - Jul 30 - - Dev Community

Creating a modern WPF application involves a blend of robust architectural patterns and frameworks to ensure maintainability, scalability, and flexibility. This article discusses the development of a WPF application using the MVVM (Model-View-ViewModel) pattern, EF Core for database integration, a plugin architecture, and Autofac for dependency injection. A key focus will be on implementing a robust user management system that includes authentication, authorization, and role management. Additionally, we will explore communication between plugins using an event aggregator and a shared service provider, and demonstrate plugin versioning and loading in the main window.

Architectural Overview

The main components of our application include:

  1. MVVM Pattern: Separation of concerns between UI and business logic.
  2. EF Core: Database access and management.
  3. Plugin Architecture: Extensibility through dynamically loaded plugins.
  4. Autofac: Dependency injection management.
  5. Event Aggregator and Shared Service Provider: Facilitating communication between plugins.

Implementing the Main Application

Configuring Dependency Injection with Autofac

Autofac allows us to manage dependencies in a flexible and extendable way. Below is an example configuration for the DI container:

public class Bootstrapper
{
    public IContainer Bootstrap()
    {
        var builder = new ContainerBuilder();

        // Register MVVM components
        builder.RegisterType<MainViewModel>().AsSelf();
        builder.RegisterType<MainWindow>().AsSelf();

        // Register EF Core DbContext
        builder.RegisterType<AppDbContext>().AsSelf().InstancePerLifetimeScope();

        // Register Plugin Loader
        builder.RegisterType<PluginLoader>().As<IPluginLoader>();

        // Register Event Aggregator
        builder.RegisterType<EventAggregator>().As<IEventAggregator>().SingleInstance();

        // Register AuthService
        builder.RegisterType<AuthService>().As<IAuthService>().InstancePerLifetimeScope();

        return builder.Build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Loading Plugins and Displaying the Landing Page

The application loads plugins during startup and displays a simple landing page. Here’s how plugins are loaded:

public interface IPlugin
{
    void Initialize(IContainer container);
}

public class PluginLoader : IPluginLoader
{
    private readonly IContainer _container;

    public PluginLoader(IContainer container)
    {
        _container = container;
    }

    public void LoadPlugins(string path)
    {
        var pluginAssemblies = Directory.GetFiles(path, "*.dll")
                                         .Select(Assembly.LoadFrom);
        foreach (var assembly in pluginAssemblies)
        {
            var pluginTypes = assembly.GetTypes().Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract);
            foreach (var pluginType in pluginTypes)
            {
                var plugin = (IPlugin)Activator.CreateInstance(pluginType);
                plugin.Initialize(_container);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

To display the landing page, we ensure that the MainWindow is correctly set up:

public partial class MainWindow : Window
{
    private readonly IPluginLoader _pluginLoader;

    public MainWindow(IPluginLoader pluginLoader)
    {
        InitializeComponent();
        _pluginLoader = pluginLoader;
        Loaded += OnLoaded;
    }

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        _pluginLoader.LoadPlugins("Plugins");
        // Display landing page content
        DataContext = new MainViewModel();
    }
}
Enter fullscreen mode Exit fullscreen mode

Implementing User Management

Data Models for User Management

We start by defining the data models for users, roles, and their relationships:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public string PasswordHash { get; set; }
    public ICollection<UserRole> UserRoles { get; set; }
}

public class Role
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ICollection<UserRole> UserRoles { get; set; }
}

public class UserRole
{
    public int UserId { get; set; }
    public User User { get; set; }
    public int RoleId { get; set; }
    public Role Role { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Authentication and Authorization Service

The AuthService handles authentication and authorization:

public interface IAuthService
{
    User Authenticate(string username, string password);
    bool Authorize(User user, string role);
}

public class AuthService : IAuthService
{
    private readonly AppDbContext _context;

    public AuthService(AppDbContext context)
    {
        _context = context;
    }

    public User Authenticate(string username, string password)
    {
        var user = _context.Users.Include(u => u.UserRoles).ThenInclude(ur => ur.Role)
                                 .FirstOrDefault(u => u.Username == username);
        if (user == null || !VerifyPassword(user.PasswordHash, password))
        {
            return null;
        }
        return user;
    }

    public bool Authorize(User user, string role)
    {
        return user.UserRoles.Any(ur => ur.Role.Name == role);
    }

    private bool VerifyPassword(string hashedPassword, string password)
    {
        // Implement password verification logic here
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Plugin Communication with Event Aggregator

Event Aggregator

An event aggregator facilitates decoupled communication between plugins:

public interface IEventAggregator
{
    void Publish<TEvent>(TEvent eventToPublish);
    void Subscribe<TEvent>(Action<TEvent> eventHandler);
}

public class EventAggregator : IEventAggregator
{
    private readonly ConcurrentDictionary<Type, List<object>> _subscribers = new();

    public void Publish<TEvent>(TEvent eventToPublish)
    {
        if (_subscribers.TryGetValue(typeof(TEvent), out var handlers))
        {
            foreach (var handler in handlers.OfType<Action<TEvent>>())
            {
                handler(eventToPublish);
            }
        }
    }

    public void Subscribe<TEvent>(Action<TEvent> eventHandler)
    {
        _subscribers.AddOrUpdate(
            typeof(TEvent),
            _ => new List<object> { eventHandler },
            (_, handlers) => { handlers.Add(eventHandler); return handlers; }
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Plugin Versioning and Loading

Plugin Metadata and Version Checking

To ensure plugins are compatible, we implement version checking:

public interface IPluginMetadata
{
    string Name { get; }
    Version Version { get; }
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class PluginAttribute : Attribute, IPluginMetadata
{
    public string Name { get; }
    public Version Version { get; }

    public PluginAttribute(string name, string version)
    {
        Name = name;
        Version = Version.Parse(version);
    }
}

public class PluginLoader : IPluginLoader
{
    private readonly IContainer _container;

    public PluginLoader(IContainer container)
    {
        _container = container;
    }

    public void LoadPlugins(string path)
    {
        var pluginAssemblies = Directory.GetFiles(path, "*.dll")
                                         .Select(Assembly.LoadFrom);
        foreach (var assembly in pluginAssemblies)
        {
            var pluginTypes = assembly.GetTypes()
                                      .Where(t => typeof(IPlugin).IsAssignableFrom(t) && 
                                                  t.GetCustomAttribute<PluginAttribute>() != null);
            foreach (var pluginType in pluginTypes)
            {
                var metadata = pluginType.GetCustomAttribute<PluginAttribute>();
                if (IsCompatible(metadata))
                {
                    var plugin = (IPlugin)Activator.CreateInstance(pluginType);
                    plugin.Initialize(_container);
                }
            }
        }
    }

    private bool IsCompatible(IPluginMetadata metadata)
    {
        // Implement compatibility check logic here
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

The architecture outlined above provides a robust foundation for developing a modular and extensible WPF application. By integrating the MVVM pattern, EF Core, a plugin architecture, and Autofac, the application becomes highly maintainable and future-proof. Implementing a comprehensive user management system, along with thread-safe plugin communication, ensures usability and reliability. A well-planned plugin versioning strategy keeps the application compatible and extensible.

. . . . . . . . . . . .