插件化架构(MEF 加载)
大约 8 分钟约 2351 字
插件化架构(MEF 加载)
简介
MEF(Managed Extensibility Framework)是 .NET 内置的插件化框架,支持运行时发现和加载程序集。通过 Import/Export 标注,可以在不修改主程序的情况下扩展功能。WPF 应用结合 MEF 可以实现模块化加载、插件热插拔和功能扩展。
特点
基础插件系统
接口定义与导出
/// <summary>
/// 插件接口定义
/// </summary>
// 插件接口
public interface IPlugin
{
string Name { get; }
string Description { get; }
string Version { get; }
void Initialize();
UserControl? CreateView();
}
// 插件元数据
public interface IPluginMetadata
{
string Name { get; }
int Order { get; }
}
// 插件导出示例
[Export(typeof(IPlugin))]
[ExportMetadata("Name", "数据分析")]
[ExportMetadata("Order", 1)]
public class DataAnalysisPlugin : IPlugin
{
public string Name => "数据分析";
public string Description => "提供数据可视化和分析功能";
public string Version => "1.0.0";
public void Initialize()
{
Console.WriteLine($"[{Name}] 插件初始化");
}
public UserControl? CreateView()
{
return new DataAnalysisView();
}
}
// 另一个插件
[Export(typeof(IPlugin))]
[ExportMetadata("Name", "报表生成")]
[ExportMetadata("Order", 2)]
public class ReportPlugin : IPlugin
{
public string Name => "报表生成";
public string Description => "生成和导出各种报表";
public string Version => "1.0.0";
public void Initialize()
{
Console.WriteLine($"[{Name}] 插件初始化");
}
public UserControl? CreateView()
{
return new ReportView();
}
}插件管理器
MEF 容器配置
/// <summary>
/// 插件管理器
/// </summary>
public class PluginManager
{
private CompositionContainer? _container;
private readonly string _pluginDirectory;
public IReadOnlyList<Lazy<IPlugin, IPluginMetadata>> Plugins { get; private set; }
= Array.Empty<Lazy<IPlugin, IPluginMetadata>>();
public PluginManager(string pluginDirectory)
{
_pluginDirectory = pluginDirectory;
}
// 初始化 — 扫描并加载插件
public void Initialize()
{
// 确保插件目录存在
Directory.CreateDirectory(_pluginDirectory);
// 扫描插件目录中的 DLL
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(_pluginDirectory, "*.dll"));
catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
_container = new CompositionContainer(catalog);
try
{
_container.ComposeParts(this);
Plugins = _container.GetExports<IPlugin, IPluginMetadata>()
.OrderBy(p => p.Metadata.Order)
.ToList();
foreach (var plugin in Plugins)
{
plugin.Value.Initialize();
}
}
catch (CompositionException ex)
{
Console.WriteLine($"MEF 组合错误:{ex.Message}");
}
}
// 获取插件
public IPlugin? GetPlugin(string name)
{
return Plugins.FirstOrDefault(p => p.Metadata.Name == name)?.Value;
}
// 重新加载插件
public void Reload()
{
_container?.Dispose();
Initialize();
}
public void Dispose()
{
_container?.Dispose();
}
}插件集成
主程序集成
/// <summary>
/// 主窗口集成插件
/// </summary>
public partial class MainWindow : Window
{
private readonly PluginManager _pluginManager;
public MainWindow()
{
InitializeComponent();
var pluginPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Plugins");
_pluginManager = new PluginManager(pluginPath);
_pluginManager.Initialize();
LoadPluginMenu();
LoadPluginTabs();
}
// 动态加载菜单
private void LoadPluginMenu()
{
foreach (var plugin in _pluginManager.Plugins)
{
var menuItem = new MenuItem
{
Header = plugin.Metadata.Name,
Tag = plugin
};
menuItem.Click += PluginMenu_Click;
PluginMenu.Items.Add(menuItem);
}
}
private void PluginMenu_Click(object sender, RoutedEventArgs e)
{
var menuItem = (MenuItem)sender;
var plugin = (Lazy<IPlugin, IPluginMetadata>)menuItem.Tag;
var view = plugin.Value.CreateView();
if (view != null)
{
ContentArea.Content = view;
}
}
// 动态加载 Tab
private void LoadPluginTabs()
{
foreach (var plugin in _pluginManager.Plugins)
{
var tab = new TabItem
{
Header = plugin.Metadata.Name
};
var view = plugin.Value.CreateView();
if (view != null)
{
tab.Content = view;
MainTabControl.Items.Add(tab);
}
}
}
}插件通信机制
/// <summary>
/// 插件间通信 — 事件总线
/// </summary>
public class PluginEventBus
{
private readonly Dictionary<string, List<Delegate>> _handlers = new();
// 插件发布事件
public void Publish<TEvent>(string eventName, TEvent eventData) where TEvent : class
{
if (!_handlers.TryGetValue(eventName, out var handlers)) return;
foreach (var handler in handlers.ToList())
{
try
{
if (handler is Action<TEvent> typedHandler)
typedHandler(eventData);
}
catch (Exception ex)
{
Console.WriteLine($"插件事件处理异常:{eventName} - {ex.Message}");
}
}
}
// 插件订阅事件
public void Subscribe<TEvent>(string eventName, Action<TEvent> handler) where TEvent : class
{
if (!_handlers.ContainsKey(eventName))
_handlers[eventName] = new List<Delegate>();
_handlers[eventName].Add(handler);
}
// 取消订阅
public void Unsubscribe<TEvent>(string eventName, Action<TEvent> handler) where TEvent : class
{
if (_handlers.TryGetValue(eventName, out var handlers))
handlers.Remove(handler);
}
}
// 使用示例:插件 A 发布数据,插件 B 接收
public class DataAnalysisPlugin : IPlugin
{
private readonly PluginEventBus _eventBus;
public DataAnalysisPlugin(PluginEventBus eventBus)
{
_eventBus = eventBus;
}
public void Analyze()
{
var result = new AnalysisResult { Score = 95.5 };
_eventBus.Publish("AnalysisCompleted", result);
}
}
public class ReportPlugin : IPlugin
{
public void Initialize(PluginEventBus eventBus)
{
eventBus.Subscribe<AnalysisResult>("AnalysisCompleted", OnAnalysisCompleted);
}
private void OnAnalysisCompleted(AnalysisResult result)
{
// 使用分析结果生成报表
}
}插件沙箱与隔离
/// <summary>
/// 插件安全加载器 — 校验 DLL 签名
/// </summary>
public class PluginSecurityValidator
{
private readonly HashSet<string> _trustedPublishers = new(StringComparer.OrdinalIgnoreCase)
{
"MyCompany",
"TrustedPartner"
};
private readonly string _allowedPluginDirectory;
public PluginSecurityValidator(string allowedPluginDirectory)
{
_allowedPluginDirectory = Path.GetFullPath(allowedPluginDirectory);
}
// 校验插件 DLL 是否可信
public bool ValidatePlugin(string dllPath)
{
// 检查文件是否在允许的目录内
var fullPath = Path.GetFullPath(dllPath);
if (!fullPath.StartsWith(_allowedPluginDirectory))
{
Console.WriteLine($"插件 {dllPath} 不在允许的目录中");
return false;
}
try
{
// 检查 DLL 签名
var assemblyName = AssemblyName.GetAssemblyName(dllPath);
var publicKey = assemblyName.GetPublicKey();
if (publicKey == null || publicKey.Length == 0)
{
Console.WriteLine($"插件 {Path.GetFileName(dllPath)} 未签名,拒绝加载");
return false;
}
return true;
}
catch (Exception ex)
{
Console.WriteLine($"插件校验失败:{ex.Message}");
return false;
}
}
// 校验插件不包含危险类型
public bool ValidatePluginTypes(Assembly assembly)
{
// 禁止加载包含以下命名空间的插件
var forbiddenNamespaces = new[]
{
"System.IO.File",
"System.Diagnostics.Process",
"System.Net.Sockets"
};
var types = assembly.GetTypes();
foreach (var type in types)
{
if (type.GetInterfaces().Contains(typeof(IPlugin)))
{
var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
foreach (var method in methods)
{
// 检查方法体是否包含危险调用(简化版)
// 生产环境应使用 Mono.Cecil 等库进行 IL 级别检查
}
}
}
return true;
}
}插件版本兼容
/// <summary>
/// 插件版本管理
/// </summary>
public class PluginVersionManager
{
private readonly Dictionary<string, PluginVersionInfo> _loadedVersions = new();
private readonly Version _hostApiVersion = new Version(2, 0, 0);
// 检查插件版本兼容性
public bool IsCompatible(PluginVersionInfo pluginVersion)
{
var pluginMinHost = pluginVersion.MinimumHostVersion;
var pluginMaxHost = pluginVersion.MaximumHostVersion;
if (pluginMinHost != null && _hostApiVersion < pluginMinHost)
{
Console.WriteLine($"插件 {pluginVersion.Name} 要求宿主版本 >= {pluginMinHost},当前 {_hostApiVersion}");
return false;
}
if (pluginMaxHost != null && _hostApiVersion > pluginMaxHost)
{
Console.WriteLine($"插件 {pluginVersion.Name} 要求宿主版本 <= {pluginMaxHost},当前 {_hostApiVersion}");
return false;
}
return true;
}
// 处理同名插件版本冲突
public PluginVersionInfo? ResolveConflict(string pluginName, PluginVersionInfo newVersion)
{
if (_loadedVersions.TryGetValue(pluginName, out var existing))
{
if (newVersion.Version > existing.Version)
return newVersion; // 新版本优先
return existing; // 保留旧版本
}
return newVersion;
}
}
public class PluginVersionInfo
{
public string Name { get; set; } = "";
public Version Version { get; set; } = new Version(1, 0, 0);
public Version? MinimumHostVersion { get; set; }
public Version? MaximumHostVersion { get; set; }
public string Description { get; set; } = "";
}插件配置
/// <summary>
/// 插件配置与隔离
/// </summary>
public class PluginConfig
{
public string Name { get; set; } = "";
public bool Enabled { get; set; } = true;
public Dictionary<string, string> Settings { get; set; } = new();
}
public class PluginConfigService
{
private readonly string _configPath;
private Dictionary<string, PluginConfig> _configs = new();
public PluginConfigService(string configPath)
{
_configPath = configPath;
Load();
}
public PluginConfig GetConfig(string pluginName)
{
return _configs.GetValueOrDefault(pluginName) ?? new PluginConfig { Name = pluginName };
}
public void SaveConfig(PluginConfig config)
{
_configs[config.Name] = config;
Save();
}
private void Load()
{
if (File.Exists(_configPath))
{
var json = File.ReadAllText(_configPath);
_configs = JsonSerializer.Deserialize<Dictionary<string, PluginConfig>>(json)
?? new Dictionary<string, PluginConfig>();
}
}
private void Save()
{
var json = JsonSerializer.Serialize(_configs, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_configPath, json);
}
}优点
缺点
总结
MEF 插件化架构适合需要功能扩展的 WPF 应用。核心流程:定义 IPlugin 接口 → 插件 DLL 用 [Export] 导出 → 主程序用 DirectoryCatalog 扫描 → CompositionContainer 加载。插件放在独立目录(Plugins/),主程序启动时自动发现。注意插件接口版本兼容和安全校验。
关键知识点
- 先分清主题属于界面层、ViewModel 层、线程模型还是设备接入层。
- WPF 文章真正的价值在于把 UI、数据、命令、线程和资源关系讲清楚。
- 上位机场景里,稳定性和异常恢复常常比界面花哨更重要。
- WPF 主题往往要同时理解依赖属性、绑定、可视树和命令系统。
项目落地视角
- 优先明确 DataContext、绑定路径、命令源和线程切换位置。
- 涉及设备时,补齐超时、重连、日志、告警和资源释放策略。
- 复杂控件最好提供最小可运行页面,便于后续复用和排障。
- 优先确认 DataContext、绑定路径、命令触发点和资源引用来源。
常见误区
- 把大量逻辑堆进 code-behind,导致页面难测难维护。
- 在后台线程直接操作 UI,或忽略 Dispatcher 切换。
- 只验证正常路径,不验证设备掉线、权限缺失和资源泄漏。
- 在 code-behind 塞太多状态与业务逻辑。
进阶路线
- 继续向 MVVM 工具链、控件可复用性、性能优化和视觉树诊断深入。
- 把主题放回真实设备流程,思考启动、连接、采集、显示、告警和恢复。
- 沉淀成控件库、通信层和诊断工具,提高整套客户端的复用度。
- 继续补齐控件库、主题系统、诊断工具和启动性能优化。
适用场景
- 当你准备把《插件化架构(MEF 加载)》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合桌面业务系统、监控看板、工控上位机和设备交互界面。
- 当系统需要同时处理复杂 UI、后台任务和硬件通信时,这类主题尤为关键。
落地建议
- 优先明确 View、ViewModel、服务层和设备层边界,避免代码隐藏过重。
- 涉及实时刷新时,提前设计 UI 线程切换、节流和资源释放。
- 对硬件通信、日志、告警和异常恢复建立标准流程。
排错清单
- 先检查 DataContext、绑定路径、INotifyPropertyChanged 和命令状态是否正常。
- 排查 Dispatcher 调用、死锁、后台线程直接操作 UI 的问题。
- 出现内存增长时,优先检查事件订阅、图像资源和窗口生命周期。
复盘问题
- 如果把《插件化架构(MEF 加载)》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《插件化架构(MEF 加载)》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《插件化架构(MEF 加载)》最大的收益和代价分别是什么?
