MEF即Managed Extensibility Framework,于.net 4.0引入。MEF通过简单地给代码附加“[Import]”和“[Export]”标记,我们就可以清晰地表明组件之间的“服务消费”与“服务提供”关系,MEF在底层使用反射动态地完成组件识别、装配工作。从而使得开发基于插件架构的应用系统变得简单。
实际上在Codeplex、codeproject上已经有很多类似主题的示例,但因本身在于使用MEF实现一个复杂的系统,初学者只会感觉眼花缭乱[至少当时我在学习MEF官方发布的例子时倍感吃力],特写了一个简单逻辑的例子希望能帮到初学者。
代码整体结构如下:
在上图中,Extension.Core封装了扩展程序接口,只有实现了改接口的模块才能被客户端发现。WPFPlugOne和WPFPlugTwo分别是两个使用MEF实现了插件。
我们先来看看实现插件的核心Extension.Core ,其中最主要的是IExtensionService,只有实现了该接口的插件才能被客户端使用。 而IMenuItem,它的定义是为了能够响应客户端的请求,并将请求内容传到插件中。具体实现如下:
1 /// <summary> 2 /// 所有扩展入口 3 /// </summary> 4 public interface IExtensionService 5 { 6 IMenuItem Item { get; set; } 7 } 8 9 10 public interface IMenuItem 11 { 12 string Header { get; } 13 IEnumerable<IMenuItem> Items { get;} 14 bool IsChecked { get; set; } 15 ICommand CheckChangedCommand { get; } 16 17 GalaSoft.MvvmLight.ViewModelBase ViewModel { get; } 18 }
在整个应用程序中,为了实现插件的载入、执行等操作,需要引用两个主要的命名空间:
using System.ComponentModel.Composition.Hosting; using System.ComponentModel.Composition;
System.ComponentModel.Composition;使我们的程序拥有了指定属性、字段或参数导入特定的导出的能力。即可以[Import]具有指定协定名称、协定类型且标记了[Export]的模块。
System.ComponentModel.Composition.Hosting;则起到一个容器的作用。
这些只是一个货架,具体的货物来源还得看我们的程序如何来加载:
1 #region MEF 2 3 /// <summary> 4 /// 首先,它跟踪哪些部分可用于组合、它们的依赖项,并且充当任何指定组合的上下文。其次,它提供了应用程序可以启动组合的方法、获取组合部件的实例,或填充可组合部件的依存关系。 5 /// </summary> 6 private CompositionContainer _container; 7 8 /// <summary> 9 /// 导入凡是协定名称为Extension.Core.ExtensionPoints.Host.Views,协定类型为ResourceDictionary 的模块 10 /// 此处在于客户端能统一管理扩展插件的现实方式 11 /// </summary> 12 [ImportMany(Extension.Core.ExtensionPoints.Host.Views, typeof(ResourceDictionary))] 13 private IEnumerable<ResourceDictionary> Views { get; set; } 14 15 [ImportMany(Extension.Core.ExtensionPoints.Host.Styles, typeof(ResourceDictionary), AllowRecomposition = true)] 16 private IEnumerable<ResourceDictionary> Styles { get; set; } 17 18 /// <summary> 19 /// 扩展插件入口 20 /// </summary> 21 [ImportMany(Extension.Core.ExtensionPoints.WorkArea.MainMenu.Self, typeof(Extension.Core.IExtensionService))] 22 private IEnumerable<Extension.Core.IExtensionService> Extensions { get; set; } 23 24 public static IEnumerable<Extension.Core.IExtensionService> ExtensionAdds { get; set; } 25 26 #endregion 27 28 /// <summary> 29 /// 组件构成 30 /// 当程序执行到这里,就会将会于应用程序根目录下实现了指定接口的模块进行组合,接下来就可以在程序中直接使用了 31 /// </summary> 32 /// <returns></returns> 33 private bool Compose() 34 { 35 var catalog = new AggregateCatalog(); 36 catalog.Catalogs.Add(new DirectoryCatalog(".")); 37 38 _container = new CompositionContainer(catalog); 39 40 try 41 { 42 _container.ComposeParts(this); 43 } 44 catch (CompositionException compositionException) 45 { 46 MessageBox.Show(compositionException.ToString()); 47 _container.Dispose(); 48 return false; 49 } 50 return true; 51 } 52 53 /// <summary> 54 /// 在满足部件的导入并可安全使用时调用 55 /// </summary> 56 public void OnImportsSatisfied() 57 { 58 foreach (ResourceDictionary r in Views) 59 { 60 this.Resources.MergedDictionaries.Add(r); 61 } 62 ExtensionAdds = Extensions; 63 }
下面我们来构建一个具体的插件,名我PlugOne,功能主要是实现对客户端输入数据的修改、删除或添加新的数据。
首先,为了客户端能够统一管理插件页面并能够被发现,需要将所有要展示出来的页面定义到DataTemplate中。
1 ResourceDictionary x:Class="WPFPlugTwo.AddNewData" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 5 xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 6 xmlns:vm="clr-namespace:WPFPlugTwo.ViewModel" 7 mc:Ignorable="d" > 8 <DataTemplate DataType="{x:Type vm:AddNewDataViewModel}"> 9 <Grid Background="White" MinWidth="300" MinHeight="180"> 10 ......... 11 </Grid> 12 </DataTemplate> 13 </ResourceDictionary>
而在对应的交互逻辑页面中,也要做一些对应的更改。
1 /// <summary> 2 /// AddNewData.xaml 的交互逻辑 3 /// </summary> 4 [Export(Extension.Core.ExtensionPoints.Host.Views,typeof(ResourceDictionary)) ] 5 public partial class AddNewData : ResourceDictionary 6 { 7 public AddNewData() 8 { 9 InitializeComponent(); 10 } 11 }
ok,如果一切正常,这个时候私有属性 Views 已经有值了。但是却还不足以让程序调用。前面已经说过IExtensionService是所有扩展库要实现的必要接口,只有实现了该接口的库才能被发现。
1 [Export(Extension.Core.ExtensionPoints.WorkArea.MainMenu.Self, typeof(IExtensionService))] 2 public class MainMenuItem : IExtensionService 3 { 4 //导入实现了IMenuItem的导出类型,因为触发扩展插件事件的来源定义menuItem的点击事件 , 5 [Import("AddNewDataMenuItem", typeof(IMenuItem))] 6 public Lazy<IMenuItem> AboutMenuItem { set; get; } 7 8 9 public IMenuItem Item 10 { 11 get 12 { 13 return AboutMenuItem.Value; 14 } 15 set 16 { 17 throw new NotImplementedException(); 18 } 19 } 20 } 21 22 23 [Export("AddNewDataMenuItem", typeof(IMenuItem))] 24 public class AddNewDataMenuItem : IMenuItem 25 { 26 //导出AddNewData页面的数据模型,如果有多个页面同样可以在此导出 27 [Import(Extension.Core.CompositionPoints.WorkArea.ViewModel, typeof(AddNewDataViewModel))] 28 public Lazy<AddNewDataViewModel> ViewModelPage { set; get; } 29 30 //在MenuItem中显示的插件名次 31 public string Header 32 { 33 get { return "添加数据"; } 34 } 35 36 //所对应的MenuItem是否还有二级菜单 37 public IEnumerable<IMenuItem> Items 38 { 39 get { return null; } 40 } 41 42 public bool IsChecked 43 { 44 get; 45 set; 46 } 47 48 49 private System.Windows.Input.ICommand _checkChangedCommand; 50 public System.Windows.Input.ICommand CheckChangedCommand 51 { 52 get 53 { 54 if (_checkChangedCommand == null) 55 _checkChangedCommand = new RelayCommand(() => 56 { 57 //定义在触发该命令后要做的操作 58 }); 59 return _checkChangedCommand; 60 } 61 } 62 63 //指定所对应的数据模型 64 public GalaSoft.MvvmLight.ViewModelBase ViewModel 65 { 66 get 67 { 68 return ViewModelPage.Value; 69 } 70 }
此时,一个简单的插件已经诞生了,接下来我们来看客户端是如何来使用它的,前面已经在App.xaml.cs中编写了加载的方法。那么我们要操作的就是静态属性ExtensionAdds了。
1 <Window x:Class="MvvmLightMEFSample.MainWindow" 2 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 3 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 4 xmlns:local="clr-namespace:MvvmLightMEFSample" 5 xmlns:vm="clr-namespace:MvvmLightMEFSample.ViewModel" 6 xmlns:vw="clr-namespace:MvvmLightMEFSample.Views" 7 xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 8 Title="{Binding Welcome}" 9 Height="300" WindowStartupLocation="CenterScreen" 10 Width="500" > 11 12 <Window.Resources> 13 <DataTemplate DataType="{x:Type MenuItem}" x:Key="extensionItem"> 14 <Grid> 15 <MenuItem HorizontalAlignment="Stretch" Header="{Binding Item.Header}" Command="{Binding DataContext.CheckChangedCommand,RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type Menu}}}" CommandParameter="{Binding RelativeSource={RelativeSource self}}"/> 16 </Grid> 17 </DataTemplate> 18 19 <DataTemplate DataType="{x:Type vm:ExtensionWrapperViewModel}"> 20 <vw:ExtensionWrapper /> 21 </DataTemplate> 22 </Window.Resources> 23 24 <Grid x:Name="LayoutRoot"> 25 <Menu Height="30" Name="menu1" VerticalAlignment="Top" > 26 <MenuItem Header="固有操作" Width="80"> 27 <MenuItem Header="删除数据" Command="{Binding FillDataCommand}"/> 28 </MenuItem> 29 <MenuItem Header="扩展操作" x:Name="extensionitem" Width="80" HorizontalAlignment="Center" ItemsSource="{Binding Path=(local:App.ExtensionAdds)}" ItemTemplate="{StaticResource extensionItem}"/> 30 </Menu> 31 <ContentControl Content="{Binding Path=DialogViewModel}" Margin="0,50,0,0"/> 32 33 </Grid> 34 </Window>