WPF MEF插件中DialogBehavior加载对话框资源失败的解决方案咨询
我之前在做WPF+MEF插件化项目时,也碰到过类似的对话框资源查找坑,完全懂你不想破坏松耦合的顾虑!给你几个实际可行的方向,都是不用把插件资源硬塞到主应用里的:
1. 给ViewModel扩展资源查找逻辑,优先查自身程序集
原来的Application.Current.TryFindResource只会扫主应用的资源字典,那我们可以给插件的ViewModel(或者封装一个插件专用的DialogViewModel基类)加个自定义资源查找方法——先查主应用全局资源,找不到的话就去当前ViewModel所在的插件程序集里找对应的View资源。
比如写个静态辅助方法:
public static object GetViewForViewModel(Type viewModelType) { // 先尝试主应用全局资源 var appResource = Application.Current.TryFindResource(viewModelType); if (appResource != null) return appResource; // 再从ViewModel所在程序集加载资源字典 var assembly = viewModelType.Assembly; // 这里约定插件的资源字典放在Resources/PluginViews.xaml,你可以根据自己的结构调整 var resourceUri = new Uri($"{assembly.GetName().Name};component/Resources/PluginViews.xaml", UriKind.Relative); try { var pluginResources = new ResourceDictionary { Source = resourceUri }; return pluginResources.TryFindResource(viewModelType); } catch (IOException) { // 处理资源字典不存在的情况 return null; } }
然后把DialogBehavior里原来的Application.Current.TryFindResource(viewModel.GetType())替换成这个方法就行。如果想更灵活,还可以给IDialogViewModel加个FindViewResource接口方法,主应用的VM用默认逻辑,插件的VM实现这个方法来查自己的资源。
2. 扩展MVVMLight的ViewLocator,用命名约定匹配VM和View
MVVMLight自带的ViewLocator本来就是用来做VM和View映射的,默认是按命名规则(比如UserControlViewModel对应UserControlView)查找。我们可以扩展它的查找逻辑,让它不仅扫主应用程序集,也去ViewModel所在的插件程序集里找对应的View,这样连资源字典都不用配置了,完全靠约定松耦合。
比如在主应用启动时加这段代码:
// 覆盖默认的View查找逻辑 ViewLocator.GetViewType = viewModelType => { // 先按MVVMLight默认规则找主应用里的View var defaultViewType = ViewLocator.DefaultGetViewType(viewModelType); if (defaultViewType != null) return defaultViewType; // 去ViewModel所在的插件程序集找同名的View(去掉ViewModel后缀) var viewTypeName = viewModelType.FullName.Replace("ViewModel", "View"); return viewModelType.Assembly.GetType(viewTypeName); };
之后DialogBehavior里就可以直接用ViewLocator获取View类型,实例化后绑定VM就行,不用再依赖TryFindResource了。
3. 插件加载时注册资源到独立的全局字典
可以在主应用里建一个静态的PluginResourceManager类,专门用来管理所有插件的资源字典。插件在MEF加载完成后(比如在插件的导出类构造函数里),把自己的资源字典加载并注册到这个管理器里。然后DialogBehavior查找资源时,先查这个管理器里的所有插件字典,再查主应用的全局资源。
示例代码大概是这样:
// 主应用里的资源管理器 public static class PluginResourceManager { private static readonly List<ResourceDictionary> _pluginResources = new List<ResourceDictionary>(); public static void AddPluginResources(ResourceDictionary resources) { if (!_pluginResources.Contains(resources)) { _pluginResources.Add(resources); } } public static object FindResource(object key) { // 先查插件资源 foreach (var dict in _pluginResources) { if (dict.TryFindResource(key) is object resource) { return resource; } } // 再查主应用资源 return Application.Current.TryFindResource(key); } } // 插件里的导出类 [Export(typeof(IPlugin))] public class VCNEditorPlugin : IPlugin { public VCNEditorPlugin() { // 加载插件自己的资源字典并注册 var resourceUri = new Uri("VCNEditorPlugin;component/Resources/PluginDialogs.xaml", UriKind.Relative); var resourceDict = new ResourceDictionary { Source = resourceUri }; PluginResourceManager.AddPluginResources(resourceDict); } }
然后把DialogBehavior里的资源查找换成PluginResourceManager.FindResource(viewModel.GetType())就行,这样既不会污染主应用的资源字典,又能统一查找所有插件的资源。
这几个方案我都用过,个人比较推荐第二种用ViewLocator的方式,最符合MVVM的约定优先原则,也最省心。如果你的插件已经有资源字典映射了,第一种或第三种也可以快速适配。
内容的提问来源于stack exchange,提问作者c3rebro

