これまで、10回にわたってMEFについて書いてきました。まだ細かい所は書いてない部分もありますが、これで一区切りにしたいと思います。後は、MEFのドキュメントを見てなんとかなると思います。
ということで、今回は、ちょっと頑張って拡張可能なアプリケーションのサンプルを書いてみました。
このアプリケーションはShellと呼ばれる以下のようなウィンドウだけで構成されるアプリケーションです。
このアプリケーションのexeの下にPluginsフォルダを作って、そこにこのアプリケーションに対応した拡張機能のdllを置いた状態で起動すると以下のように画面の左側に拡張機能で定義されたメニューが表示されます。
メニューのリンクをクリックすると、画面の右側にタブで画面が表示されます。
簡単な説明
まず、プラグインに必要なインターフェースと属性を設計します。今回はプラグインが持つべき機能と、プラグインで表示するための画面で実装すべきインターフェースを作成しました。
IMEFShellPluginインターフェース
プラグインが実装するインターフェースです。プラグインが提供する機能としては画面を作成するという機能だけにしました。
namespace MEFShell.Plugins { /// <summary> /// プラグインが実装するインターフェース /// </summary> public interface IMEFShellPlugin { /// <summary> /// Viewを作成する /// </summary> /// <returns></returns> IShellView CreateView(); } }
IShellViewインターフェース
プラグインで表示するViewが実装するインターフェース。タブに表示するタイトルを取得するプロパティを定義しています。
namespace MEFShell.Plugins { /// <summary> /// プラグインのViewが実装するインターフェース /// </summary> public interface IShellView { string Title { get; } } }
MEFPluginExport属性
Shellにプラグイン可能な部品としてExportする属性です。リンクに表示する文字列をメタデータで設定するようにしています。
namespace MEFShell.Plugins { using System; using System.ComponentModel.Composition; /// <summary> /// プラグインとしてExportするための属性 /// </summary> [MetadataAttribute] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public class MEFPluginExportAttribute : ExportAttribute, IMEFPluginMetadata { /// <summary> /// コンストラクタ /// </summary> /// <param name="label">プラグインの表示名</param> public MEFPluginExportAttribute(string label) : base(typeof(IMEFShellPlugin)) { this.Label = label; } /// <summary> /// プラグインの表示名 /// </summary> public string Label { get; private set; } } /// <summary> /// プラグインのメタデータ /// </summary> public interface IMEFPluginMetadata { string Label { get; } } }
エントリポイント
アプリケーションのエントリポイントとなるAppクラスのStartupイベントではプラグインフォルダのDLLを読み込んでコンテナを作成しています。そして、SatisfyImportsOnceでプラグインをViewModelに読み込んでいます。
namespace MEFShell { using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using System.IO; using System.Windows; /// <summary> /// App.xaml の相互作用ロジック /// </summary> public partial class App : Application { private void Application_Startup(object sender, StartupEventArgs e) { // Pluginsフォルダが無い場合は作成する if (!Directory.Exists("Plugins")) { Directory.CreateDirectory("Plugins"); } // コンテナを作成する var catalog = new AggregateCatalog(); catalog.Catalogs.Add(new DirectoryCatalog(@".\Plugins")); var container = new CompositionContainer(catalog); // Shellを作成する var viewModel = new ShellViewModel(); // コンテナからプラグインを設定する container.SatisfyImportsOnce(viewModel); new Shell { DataContext = viewModel }.Show(); } } }
ShellViewModel
プラグインをロードするShellViewModelでは、プラグインがロードされたタイミングで初期化を行っています。
namespace MEFShell { using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel.Composition; using System.Linq; using MEFShell.Plugins; public class ShellViewModel : ViewModelBase, IPartImportsSatisfiedNotification { private ObservableCollection<IShellView> shellViews = new ObservableCollection<IShellView>(); private ObservableCollection<PluginCommandViewModel> pluginCommands = new ObservableCollection<PluginCommandViewModel>(); private IShellView selectedTab; /// <summary> /// プラグイン /// </summary> [ImportMany] public IEnumerable<Lazy<IMEFShellPlugin, IMEFPluginMetadata>> Plugins { get; set; } public ObservableCollection<PluginCommandViewModel> PluginCommands { get { return this.pluginCommands; } } /// <summary> /// 現在作成したShellのView(ViewをVMでもつってどうなの!?) /// </summary> public ObservableCollection<IShellView> ShellViews { get { return this.shellViews; } } public IShellView SelectedTab { get { return this.selectedTab; } set { this.selectedTab = value; this.RaisePropertyChanged("SelectedTab"); } } public void OnImportsSatisfied() { // プラグインからPluginCommandViewModelへ変換する this.PluginCommands.Clear(); var viewModels = this.Plugins.Select(p => new PluginCommandViewModel { Label = p.Metadata.Label, CreateViewCommand = new DelegateCommand( () => { // プラグインからViewを取得したタブに追加する var view = p.Value.CreateView(); this.ShellViews.Add(view); // 現在選択中のタブを設定する this.SelectedTab = view; }) }); foreach (var vm in viewModels) { this.PluginCommands.Add(vm); } } } }
Shell.xaml
ShellViewModelを表示するViewです。
<Window x:Class="MEFShell.Shell" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" xmlns:local="clr-namespace:MEFShell" xmlns:plugin="clr-namespace:MEFShell.Plugins;assembly=MEFShell.Plugins" Title="Shell"> <d:DesignProperties.DataContext> <local:ShellViewModel /> </d:DesignProperties.DataContext> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition MinWidth="150" Width="*" /> <ColumnDefinition Width="220*" /> </Grid.ColumnDefinitions> <Border BorderThickness="3" CornerRadius="5" BorderBrush="#FF7070BA"> <ItemsControl ItemsSource="{Binding Path=PluginCommands}" Margin="5"> <ItemsControl.ItemTemplate> <DataTemplate> <TextBlock> <Hyperlink Command="{Binding Path=CreateViewCommand}"> <Run Text="{Binding Path=Label}" /> </Hyperlink> </TextBlock> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Border> <TabControl Grid.Column="1" ItemsSource="{Binding Path=ShellViews}" SelectedItem="{Binding Path=SelectedTab}"> <TabControl.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Path=Content.Title, RelativeSource={RelativeSource FindAncestor, AncestorType=TabItem}}" /> </DataTemplate> </TabControl.ItemTemplate> </TabControl> </Grid> </Window>
以上で、アプリケーションの土台になるShellが出来ました。プラグイン側のコードは以下のような感じになります。
テキストエディタプラグイン
プラグインを作成するにはIMEFShellPluginを実装してMEFPluginExport属性をつけます。そしてCreateViewで、このプラグインが提供するViewを作成します。
namespace TextEditorPlugin { using MEFShell.Plugins; /// <summary> /// テキストエディタプラグイン /// </summary> [MEFPluginExport("テキストエディタ")] public class TextEditorPlugin : IMEFShellPlugin { /// <summary> /// TextEditorViewを作成する /// </summary> /// <returns></returns> public IShellView CreateView() { return new TextEditorView(); } } }
ViewはIShellViewインターフェースを実装したUserControlです。
namespace TextEditorPlugin { using System.Windows.Controls; using MEFShell.Plugins; /// <summary> /// TextEditorView.xaml の相互作用ロジック /// </summary> public partial class TextEditorView : UserControl, IShellView { public TextEditorView() { InitializeComponent(); } public string Title { get { return "メモ"; } } } }
XAML側は好きなように作れます。これはプロジェクトをダウンロードして確認してみてください。
まとめ
ということで、雑な説明でしたが、たったこれだけのコードでDLLを置くだけで拡張可能なGUIのアプリケーションが作れます。拡張すべきポイントを設計してMEFでさくっと実装できるのはステキですね。ということで、連載終了です!良いMEFライフを。
プロジェクトのダウンロード
以下のリンクからダウンロードできます。Releaseモードでビルドすると、プラグインが無い状態で起動します。DebugモードでビルドするとプラグインがPluginsフォルダにコピーされた状態で起動します。
過去の記事
- Managed Extensibility Framework入門 その1「はじめに」
- Managed Extensibility Framework入門 その2「使うに当たって覚えておきたいこと」
- Managed Extensibility Framework入門 その3「Export」
- Managed Extensibility Framework入門 その4「もっとExport」
- Managed Extensibility Framework入門 その5「Import」
- Managed Extensibility Framework入門 その6「拡張可能なアプリケーション作成」
- Managed Extensibility Framework入門 その7「クラス以外のExportとImport」
- Managed Extensibility Framework入門 その7.5「遅延初期化のLazy」
- Managed Extensibility Framework入門 その8「遅延初期化」
- Managed Extensibility Framework入門 その9「メタデータ」
- Managed Extensibility Framework入門 その10「初期化処理」