WPF和DLL扩展加载
目录
1. DLL基础概念
1.1 什么是DLL
- 动态链接库的定义
- DLL与静态库的区别
- .NET程序集的概念
1.2 DLL的类型
- 托管DLL(.NET程序集)
- 非托管DLL(Win32)
- 混合模式DLL
1.3 DLL的优势
- 代码复用
- 模块化开发
- 动态更新
- 内存优化
2. WPF中的DLL加载机制
2.1 程序集加载方式
// 静态加载
Assembly assembly = Assembly.LoadFrom("path/to/dll");
// 动态加载
Assembly assembly = Assembly.LoadFile("path/to/dll");
// 反射加载
Type type = assembly.GetType("Namespace.ClassName");
2.2 AppDomain与程序集隔离
- AppDomain的概念
- 程序集隔离机制
- 跨域调用
在 .NET 框架中,AppDomain (应用程序域) 是一个重要的概念,它提供了一种在单个进程内隔离应用程序或组件的方式。你可以把它想象成一个轻量级的“沙盒”。
程序集隔离指的是将不同的程序集(.dll 或 .exe 文件)加载到相互独立的 AppDomain 中,从而实现它们之间的隔离。这种隔离带来的主要好处有:
- 错误隔离: 一个 AppDomain 中的代码如果抛出未处理的异常,它只会影响到该 AppDomain,而不会导致整个进程崩溃。这使得你可以更健壮地运行第三方代码或插件。
- 卸载隔离: 程序集一旦被加载到 AppDomain 中,就无法轻易地从内存中卸载。但通过卸载整个 AppDomain,可以有效地释放其加载的所有程序集和资源。这对于需要动态加载和卸载插件或组件的应用程序非常有用。
- 配置隔离: 每个 AppDomain 都有自己的配置,例如应用程序设置、权限集等。这使得在同一个进程中运行不同配置的应用程序成为可能。
- 资源隔离: 不同的 AppDomain 拥有各自的静态变量和资源,避免了相互干扰。
2.3 程序集解析事件
AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
3. 程序集反射加载
3.1 反射基础
- Type类的使用
- 获取类型信息
- 创建实例
3.2 动态类型发现
// 扫描程序集中的所有类型
var types = assembly.GetTypes()
.Where(t => t.GetInterfaces().Contains(typeof(IPlugin)))
.ToList();
3.3 接口驱动的扩展
- 定义扩展接口
- 实现扩展发现
- 动态实例化
3.4 实际应用示例
public class PluginManager
{
public List<IPlugin> LoadPlugins(string directory)
{
var plugins = new List<IPlugin>();
var dllFiles = Directory.GetFiles(directory, "*.dll");
foreach (var dll in dllFiles)
{
var assembly = Assembly.LoadFrom(dll);
var pluginTypes = assembly.GetTypes()
.Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface);
foreach (var type in pluginTypes)
{
var plugin = (IPlugin)Activator.CreateInstance(type);
plugins.Add(plugin);
}
}
return plugins;
}
}
4. 插件化架构设计
4.1 插件接口设计
public interface IPlugin
{
string Name { get; }
string Version { get; }
void Initialize();
void Execute();
void Dispose();
}
4.2 插件生命周期管理
- 插件加载
- 插件初始化
- 插件执行
- 插件卸载
4.3 插件通信机制
- 事件总线
- 消息传递
- 共享数据
4.4 插件配置管理
public class PluginConfiguration
{
public string Name { get; set; }
public string AssemblyPath { get; set; }
public bool Enabled { get; set; }
public Dictionary<string, object> Settings { get; set; }
}
5. 动态加载与卸载
5.1 Assembly.LoadFrom vs Assembly.LoadFile
- 区别与使用场景
- 性能考虑
- 依赖解析
5.2 程序集卸载
// 创建新的AppDomain
AppDomain pluginDomain = AppDomain.CreateDomain("PluginDomain");
// 在插件域中加载程序集
pluginDomain.Load(assemblyName);
// 卸载插件域
AppDomain.Unload(pluginDomain);
5.3 内存管理
- 程序集缓存
- 垃圾回收
- 内存泄漏预防
6. 依赖注入与扩展
6.1 服务容器集成
// 使用Microsoft.Extensions.DependencyInjection
public void ConfigureServices(IServiceCollection services)
{
// 扫描程序集注册服务
var assemblies = Directory.GetFiles(pluginDirectory, "*.dll")
.Select(Assembly.LoadFrom);
foreach (var assembly in assemblies)
{
services.AddServicesFromAssembly(assembly);
}
}
6.2 扩展点设计
- 扩展接口定义
- 扩展发现机制
- 扩展注册
6.3 热插拔支持
- 文件监控
- 动态重载
- 状态管理
7. 实际应用案例
7.1 主题系统
public interface ITheme
{
string Name { get; }
ResourceDictionary GetResources();
}
// 动态加载主题
public void LoadTheme(string themePath)
{
var assembly = Assembly.LoadFrom(themePath);
var themeType = assembly.GetTypes()
.FirstOrDefault(t => typeof(ITheme).IsAssignableFrom(t));
if (themeType != null)
{
var theme = (ITheme)Activator.CreateInstance(themeType);
Application.Current.Resources.MergedDictionaries.Add(theme.GetResources());
}
}
7.2 控件扩展
- 自定义控件库
- 动态控件加载
- 样式扩展
7.3 业务模块化
- 功能模块分离
- 按需加载
- 模块间通信
8. 最佳实践与注意事项
8.1 安全性考虑
- 程序集签名验证
- 权限控制
- 沙箱隔离
8.2 性能优化
- 延迟加载
- 程序集缓存
- 资源管理
8.3 错误处理
try
{
var assembly = Assembly.LoadFrom(dllPath);
// 处理加载成功
}
catch (BadImageFormatException)
{
// 处理无效程序集
}
catch (FileLoadException)
{
// 处理加载失败
}
catch (Exception ex)
{
// 处理其他异常
}
8.4 调试与诊断
- 程序集加载日志
- 性能监控
- 内存分析
8.5 部署策略
- 程序集版本管理
- 依赖关系处理
- 更新机制
总结
WPF中的DLL扩展加载为应用程序提供了强大的模块化和可扩展性能力。通过合理的设计和实现,可以构建出灵活、可维护的应用程序架构。关键是要平衡灵活性、性能和安全性,选择合适的加载策略和架构模式。
实操
我们在wpf中需要建立一个wpf类库,然后在这个基础上引用原来的库,最后将其编译为dll就行,记得使用项目引用,可以防止再新建一个公用的类库。
首先我们需要一个加载dll的方法实现。
private void LoadExtentions()
{
try
{
string pluginPath = Path.Combine(AppContext.BaseDirectory, "Extensions");
if (!Directory.Exists(pluginPath))
{
Console.WriteLine("Extensions folder not found.");
return;
}
var dllFiles = Directory.GetFiles(pluginPath, "*.dll");
if(!dllFiles.Any())
{
Console.WriteLine("No plugins found.");
return;
}
var extensionPages = new List<(Type PageType, Type ViewModelType, string Title)>();
foreach (var dllFile in dllFiles) {
var assembly = Assembly.LoadFrom(dllFile);
Console.WriteLine($"Loaded plugin: {assembly.GetName().Name}");
var viewModelTypes = assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(ExtentionViewModel))).ToList();
var pageTypes = assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(Page))).ToList();
foreach (var pageType in pageTypes)
{
var viewModelType = viewModelTypes.FirstOrDefault(vm =>
vm.Name == pageType.Name + "ViewModel");
string title = GetPageTitle(viewModelType, pageType);
extensionPages.Add((pageType, viewModelType, title));
Console.WriteLine($"Found extension: {title} - {pageType.Name}");
}
}
ExtensionPages = extensionPages;
Console.WriteLine($"Total extensions loaded: {ExtensionPages.Count}");
}
catch (Exception ex) {
Console.WriteLine($"Failed to load plugin: {ex.Message}");
}
}
然后我们使用静态属性类去存储扩展页面的信息:
public static List<(Type PageType, Type ViewModelType, string Title)> ExtensionPages { get; private set; } = new();
这个方法是加载继承了ExtensionViewModel 和 Page 类 的类方法,我们需要从获得的viewmodel类中得到我们想要的Title Icon等信息。
private string GetPageTitle(Type viewModelType, Type pageType)
{
if (viewModelType != null)
{
try
{
var instance = Activator.CreateInstance(viewModelType);
// 尝试 Title 属性
var titleProperty = viewModelType.GetProperty("Title");
if (titleProperty != null)
{
var title = titleProperty.GetValue(instance) as string;
Console.WriteLine($"Title property value: '{title}'");
if (!string.IsNullOrEmpty(title)) return title;
}
else
{
Console.WriteLine("Title property not found");
}
// 尝试 DisplayName 属性
var displayNameProperty = viewModelType.GetProperty("DisplayName");
if (displayNameProperty != null)
{
var displayName = displayNameProperty.GetValue(instance) as string;
Console.WriteLine($"DisplayName property value: '{displayName}'");
if (!string.IsNullOrEmpty(displayName)) return displayName;
}
}
catch (Exception ex)
{
Console.WriteLine($"Error creating ViewModel instance: {ex.Message}");
}
}
// 默认使用类名(去掉Page后缀)
string className = pageType.Name;
if (className.EndsWith("Page"))
{
className = className.Substring(0, className.Length - 4);
}
Console.WriteLine($"Using default class name: '{className}'");
return className;
}
完成之后我们就可以去写依赖注入的服务注册扩展实现的方法
private void RegisterExtensionPages(IServiceCollection services)
{
foreach (var (pageType, viewModelType, title) in ExtensionPages)
{
services.AddSingleton(pageType);
if (viewModelType != null)
{
services.AddSingleton(viewModelType);
}
Console.WriteLine($"Registered extension: {title}");
}
}
最后在导航栏一侧去注册就ok了,当然也可以通过反射程序集的方式加载导航栏,看自己的项目需求,下面是获取图标和添加导航的方法实现。
private void InitializeMenuItems()
{
// 添加默认菜单项
_menuItems.Add(new NavigationViewItem("Home", SymbolRegular.Home24, typeof(DashboardPage)));
_menuItems.Add(new NavigationViewItem("Color采集", SymbolRegular.Color24, typeof(ColorCollecterPage)));
// 添加扩展页面的菜单项
foreach (var (pageType, viewModelType, title) in App.ExtensionPages)
{
// 尝试从ViewModel获取图标
var icon = GetIconFromViewModel(viewModelType);
var menuItem = new NavigationViewItem(title, icon, pageType)
{
ToolTip = $"打开{title}扩展"
};
_menuItems.Add(menuItem);
}
}
// 获取图标
private SymbolRegular GetIconFromViewModel(Type viewModelType)
{
if (viewModelType != null)
{
try
{
// 尝试从 Icon 属性获取图标
var iconProperty = viewModelType.GetProperty("Icon");
if (iconProperty != null)
{
var instance = Activator.CreateInstance(viewModelType);
var icon = iconProperty.GetValue(instance);
if (icon is SymbolRegular symbolIcon)
{
return symbolIcon;
}
}
}
catch
{
// 如果无法获取图标,使用默认图标
}
}
// 默认图标
return SymbolRegular.Home24;
}
但是这里要注意一件事情,那就是这种加载方式下,xaml的加载不同于我们之前使用的 在 this.DataContext = this;方法 ,可以在xaml上用ViewModel.进行绑定,默认情况下,如果在插件实现的库中这样写有很大概率是找不到你的viewModel的,会出现绑定失败的,因此一般情况下,推荐直接使用ViewModel进行 业务逻辑和数据的绑定。