在用WPF MVVMLight做畢設的過程中,偶然看到關於MEF插件式開發的技術文章,就想試試看能不能把每個模塊做成插件。
我原先想實現的功能就是一個模塊就是一個插件,所有插件加載到主界面的TreeView導航中,點擊對應的項,顯示對應的頁面,而TreeView的集合並非我手動一個一個new TreeViewItem 是根據每個頁面的元數據來生成
后來我也確實部分實現了這個小需求,但是不能做成無限極樹,而且在用CustomExportMetadata設置元數據時也非常不合理,只能刪掉然后一個一個new TreeViewItem了 (╯‵□′)╯︵┻━┻(菜雞的無能狂怒)
先來看看簡陋的效果圖(啥也看不出來)
然后看下項目結構
- Core 核心接口庫,想要讓我加載你的插件,你就必須遵守我的規則
- Plugins 用戶控件庫,插件
- MvvmLight.MEF 主程序加載插件
MEF需要用到的dll
- System.ComponentModel.Composition
- System.ComponentModel.Composition.Hosting
注意
插件沒加載進去先看看查找插件dll的路徑對不對
-
在Core類庫中定義一個視圖IView接口
public interface IView { //約束插件類型 //我並不關心你的class叫什么名字,我只需要知道誰繼承了這個接口,然后我就可以轉換成IView類型的。 //這難道就是傳說中的面向接口編程😂 }
-
定義一個元數據IMetaData接口 (根據你的需要定義元數據)
/// <summary> /// 優先級 /// </summary> [DefaultValue(0)] int Priority { get; } /// <summary> /// 名稱(不能重復) /// </summary> string Name { get; } /// <summary> /// 描述 /// </summary> string Description { get; } /// <summary> /// 作者 /// </summary> string Author { get; } /// <summary> /// 版本 /// </summary> string Version { get; }
-
實現元數據接口的特性
[MetadataAttribute] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public class CustomExportMetadata : ExportAttribute, IMetaData { /// <summary> /// 優先級 /// </summary> public int Priority { get; private set; } /// <summary> /// 名稱 /// </summary> public string Name { get; private set; } /// <summary> /// 描述 /// </summary> public string Description { get; private set; } /// <summary> /// 作者 /// </summary> public string Author { get; private set; } /// <summary> /// 版本 /// </summary> public string Version { get; private set; } /// <summary> /// 構造函數 /// </summary> public CustomExportMetadata() : base(typeof(IMetaData)) { this.UId = Guid.NewGuid().ToString(); } /// <summary> /// 構造函數 /// </summary> public CustomExportMetadata() : base(typeof(IMetaData)) { } /// <summary> /// 構造重載 /// </summary> /// <param name="priority">優先級</param> public CustomExportMetadata(int priority) : this() { this.Priority = priority; } // <summary> /// 構造重載 /// </summary> /// <param name="priority">優先級</param> /// <param name="name">名稱</param> public CustomExportMetadata(int priority, string name) : this(priority) { this.Name = name; } /// <summary> /// 構造重載 /// </summary> /// <param name="priority">優先級</param> /// <param name="name">名稱</param> /// <param name="description">描述</param> public CustomExportMetadata(int priority, string name,string description) : this(priority, name) { this.Description = description; } /// <summary> /// 構造重載 /// </summary> /// <param name="priority">優先級</param> /// <param name="name">名稱</param> /// <param name="description">描述</param> /// <param name="author">作者</param> public CustomExportMetadata(int priority, string name,string description,string author) : this(priority, name, description) { this.Author = author; } /// <summary> /// 構造重載 /// </summary> /// <param name="priority">優先級</param> /// <param name="name">名稱</param> /// <param name="description">描述</param> /// <param name="author">作者</param> /// <param name="version">版本</param> public CustomExportMetadata(int priority, string name,string description,string author,string version) : this(priority, name, description, author) { this.Version = version; }
-
在你想被加載的UserControl.xaml.cs里繼承IView並添加Export和CustomExportMetadata特性
Export的參數是聲明你要把這個類導出為什么類型
CustomExportMetadata的參數對照上面的重載我們知道MEF作為IOC的方式之一,它的主要作用是解耦,MEF加上面向接口編程,可以使得你的設計更加靈活。我們知道類的構造函數是可以重載的,我們通過構造函數可以向對象傳遞參數。那么如果我們的MEF也需要通過構造函數傳參怎么辦呢?別擔心,有我們神奇的ImportingConstructor為您解決.
[https://www.cnblogs.com/landeanfen/p/4848885.html](https://www.cnblogs.com/landeanfen/p/4848885.html "C#進階系列——MEF實現設計上的“松耦合”(四):構造函數注入")/// <summary> /// TestPluginsUserControl.xaml 的交互邏輯 /// </summary> [Export(typeof(IView))] [CustomExportMetadata(1, "測試模塊1下的第一個頁面", "測試模塊1下的第一個頁面", "HFMS","0.0.1")] public partial class TestPluginsUserControl : UserControl, IView { [ImportingConstructor] public TestPluginsUserControl() { InitializeComponent(); } }
-
定義TreeItem類
public class TreeItem { public int Id { get; set; } public int ParentId { get; set; } public string Name { get; set; } public List<TreeItem> Nodes { get; set; } }
-
在主窗體的ViewModel里加載插件(延遲加載和普通加載)這里用的是懶加載(延遲加載) 普通加載請點擊最下方鏈接
private List<TreeItem> _tvModel; /// <summary> /// TreeView的ItemSource /// </summary> public List<TreeItem> TVModel { get { return _tvModel; } set { Set(ref _tvModel, value); } } private ObservableCollection<TabItem> _tabItemTests = new ObservableCollection<TabItem>(); /// <summary> /// TabControl的ItemSource /// </summary> public ObservableCollection<TabItem> TabItemTests { get { return _tabItemTests; } set { Set(ref _tabItemTests, value); } } //你要導入的類型和元數據數組,所有繼承IView接口並導出的頁面都在這個數組里 //后面參數也不知道有啥用,還在學習中 [ImportMany(typeof(IView),AllowRecomposition = true)] private Lazy<IView, IMetaData>[] Plugins { get; set; } private CompositionContainer container = null; //窗體加載時調用的方法 private void ExcuteLoadWindow() { //獲取工作目錄 var dir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); if (dir.Exists) { //就是這里,讀取所有符合條件的dll var catalog = new DirectoryCatalog(dir.FullName, "MEF.*.dll"); container = new CompositionContainer(catalog); try { //從特性化對象的數組創建可組合部件,並在指定的組合容器中組合這些部件。 container.ComposeParts(this); } catch (CompositionException ce) { Console.Write(ce.Message); } //排個序,我尋思着是不是有的插件還依賴着別的插件,所以需要排序 Plugins.OrderBy(p => p.Metadata.Priority); //傳個最頂級的節點id和創建好的節點集合 TVModel = GetTrees(0,SetTrees()); } } /// <summary> /// 遞歸生成樹結構 /// </summary> /// <param name="parentid">父節點Id</param> /// <param name="nodes">節點集合</param> /// <returns></returns> public List<TreeItem> GetTrees(int parentid, List<TreeItem> nodes) { List<TreeItem> mainNodes = nodes.Where(x => x.ParentId == parentid).ToList<TreeItem>(); List<TreeItem> otherNodes = nodes.Where(x => x.ParentId != parentid).ToList<TreeItem>(); foreach (TreeItem dpt in mainNodes) { dpt.Nodes = GetTrees(dpt.Id, otherNodes); } return mainNodes; } public List<TreeItem> SetTrees() { //這里創建節點集合 List<TreeItem> treeItems = new List<TreeItem> { //添加模塊1 new TreeItem() { Id = 1, ParentId = 0, Name = "測試模塊1", Nodes = new List<TreeItem>() }, new TreeItem() { Id = 2, ParentId = 1, Name = "測試模塊1下的第一個頁面", Nodes = new List<TreeItem>() }, new TreeItem() { Id = 3, ParentId = 1, Name = "測試模塊1下的第二個頁面", Nodes = new List<TreeItem>() }, //添加模塊1下更多級節點 new TreeItem() { Id = 4, ParentId = 1, Name = "有子節點的子節點", Nodes = new List<TreeItem>() }, new TreeItem() { Id = 5, ParentId = 4, Name = "測試模塊1下的有子節點的子節點第一個頁面", Nodes = new List<TreeItem>() }, //添加模塊2 new TreeItem() { Id = 6, ParentId = 0, Name = "測試模塊2", Nodes = new List<TreeItem>() }, new TreeItem() { Id = 7, ParentId = 6, Name = "測試模塊2下的第一個頁面", Nodes = new List<TreeItem>() }, new TreeItem() { Id = 8, ParentId = 6, Name = "測試模塊2下的第二個頁面", Nodes = new List<TreeItem>() } }; return treeItems; } private void ExcuteLoadUserControl(object obj) { if (obj is TreeView treeview) { foreach (var item in Plugins) { if (item.Metadata.Name == (treeview.SelectedItem as TreeItem).Name) { if (TabItemTests.Where(x => x.Header.ToString() == item.Metadata.Name).Count() < 1) { TabItemTests.Add(new TabItem() { Header = item.Metadata.Name, Content = item.Value, }); } return; } } } }
最后有個關於MVVMLight的問題沒解決
UserControl的ViewModel我也是統一用ViewModelLocator來管理,
但是在xaml里 ViewModel不能綁定到UserControl的DataContext,只能綁給次一級元素的DataContext
<UserControl.Resources>
<ResourceDictionary>
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
</ResourceDictionary>
</UserControl.Resources>
<Grid DataContext="{Binding Main,Source={StaticResource Locator}}">
<TextBlock Text="{Binding TestText}"/>
</Grid>
參考鏈接:
https://www.cnblogs.com/hippieZhou/p/9404043.html#4183303
感謝@hippieZhou園友的指導。
第一次寫隨筆,只是根據這個鏈接照打了一遍。:)