C#でプラグイン機能を開発に挑戦してみた(3)
メニューにプラグインの名前を追加する

どもです。

前回のエントリで、データベース(LiteDB)を使用したプラグインの管理を行ってみました。
その際に、「実際にデータベースからプラグイン(DLL)の情報(パス)を取得して読み込み実行する」、という例を示しています。
この例では、「コマンドコンソール」形式のプログラムで、「読み込んだプラグインの関数を実行できること」まで示しました。
しかし実際のアプリケーション/プラグインは、それぞれに名前があり、かつ複数登録(インストール)されていることが一般的です。
前回のエントリでは、この「名前がある」ことと「複数登録(インストール)されている」ことは触れませんでした。
それを受けて今回は、GUIアプリにおいてプラグインの名前をメニューに表示する方法を検討してみたので、その内容と結果について書きます。

1. 開発環境

毎度ですが、開発環境です。

開発/実行環境
項目 内容
OS Windows10 Pro(1909)
CPU i7-8700
メモリ 16GB
IDE Visual Studio Community 2019
Version 16.11.2
言語 C#/WPF
.NET .NET Framework 4.7

なお今回のエントリでは、データベースを使用しません。
あくまで「プラグインの名前をメニューに表示する」方法の検討なので、それに直接関係しない内容については触れないようにします。

2. メニュー

今回の検討では、メニューにプラグインの名前を表示します。
このときプラグインの名前は、メニューバーに直接表示するのではなく、あるメニューの項目の子メニューとして表示します。

2.1. メニューへの子メニューの「動的」追加

C#/WPFにおいて、メニューに子メニューを追加するた際には、「MenuItem」オブジェクトに対して、ItemsプロパティのAddメソッドを用いて「MenuItem」オブジェクトを追加します。
具体的には、以下のようなコードになります。

MenuItem rootMenuItem = FindName("RootMenu");
MenuItem subMenuItem = new MenuItem()
{
	Header = "plugin title"
};
rootMenuItem.Items.Add(subMenuItem);

ここで、プラグインの名前を追加する親メニューについては、WPF/XAML上で「RootMenu」という名前で指定しています。
「FindName」という関数を使用することで、この名前に対応するオブジェクトを取得できます。
なお、「FindName」を使用するので、上記のコードはXAMLに対応するC#のコード(.cs)に実装します。

2.2. プラグインの名前の通知

次に、メニューに表示するプラグインの名前の通知です。
WPFで開発した場合、ViewModel(あるいはModel)に一覧を取得する処理を実装するのが順当な設計です。
ここで問題になるのは、ViewMoldeからViewへのプラグイン(の名前)一覧の通知方法です。
MVVMでは、ViewModelはViewの情報を持つことが無いため、Viewの関数を呼び出して一覧を通知することができません。
ではどうするか?
「delegate」/「event」を使用します。
ViewModelにeventを実装し、View側にeventハンドラ本体を実装します。
以下のようなイメージです。



実装は、以下のようにできます。
まずViewModel側です。

public class DynamicMenuItemViewModel : ViewModelBase
{
	public void LoadDynamicMenuRequestEventHandler(object sender, EventArgs args)
	{
		var menuItems = new List<string>()
		{
			"MenuItem1",
			"MenuItem2",
			"MenuItem3",
			"MenuItem4",
			"MenuItem5"
		};
		var eventArgs = new NotifyMenuItemEventArgs(menuItems);
		this.RaiseNotifyMenuItemEvent?.Invoke(this, eventArgs);
	}
}

今回のエントリで追加する文字列は、「MenuItem1」~「MenuItem5」の固定とします。
次にView側です。

public partial class MainWindow : Window
{
	/// <summary>
	/// Menu item unload event handler.
	/// </summary>
	/// <param name="sender">Event sender.</param>
	/// <param name="args">Event args.</param>
	public void NotifyMenuItemEventHandler(object sender, EventArgs args)
	{
		NotifyMenuItemEventArgs eventArgs = (NotifyMenuItemEventArgs)args;
		IEnumerable<string> menuItemTitles = eventArgs.MenuItems;
		this.LoadMenuItem(menuItemTitles);
	}

	/// <summary>
	/// Load menu item.
	/// </summary>
	/// <param name="menuItemTitles">Collection of menu item title.</param>
	protected void LoadMenuItem(IEnumerable<string> menuItemTitles)
	{
		this.UnloadMenuItem();

		MenuItem rootMenuItem = (MenuItem)this.FindName("MenuRoot");
		foreach (var menuItemTitle in menuItemTitles)
		{
			var menuItem = new MenuItem()
			{
				Header = menuItemTitle
			};
			rootMenuItem.Items.Add(menuItem);
		}
	}
}

行っている内容は、以下の通りです。

  1. イベントハンドラでイベントを受け取る
  2. イベント引数に格納された文字列一覧を取得する
  3. 子メニューオブジェクト(MenuItemオブジェクト)を生成する。
  4. MenuItemオブジェクトに、表示したい文字列をセットする。
  5. 親メニュー(rootMenuItem)に追加する
  6. これを、文字列一覧のそれぞれの要素に対して実施する

最後に、ViewMoldeのevent(delegate)にViewのイベントハンドラの登録です。
イベントハンドラの登録は、View側にDataContext(要はViewModel)の登録が完了したタイミングで実施します。
具体的には、View側で「DataContextChanged」イベントが発生したタイミングです。
イベントハンドラは、以下のような実装になります。

public partial class MainWindow : Window
{
	/// <summary>
	/// Data context changed event handler
	/// </summary>
	/// <param name="sender">Event sender.</param>
	/// <param name="e">Event args.</param>
	private void MainWindows_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
	{
		DynamicMenuItemViewModel menuItemViewModel = (DynamicMenuItemViewModel)e.NewValue;
		menuItemViewModel.RaiseNotifyMenuItemEvent += this.NotifyMenuItemEventHandler;

		this.RaiseLoadMenuItemRequestEvent += menuItemViewModel.LoadDynamicMenuRequestEventHandler;
	}
}

3. 動かしてみます

ここまで紹介したコードを元に、アプリケーションを作成してみます。
アプリケーションの画面は、以下のようにします。



ここで、[Load]ボタンを押下すると[Root]の下にサブメニューが追加されます。
その結果がコチラ!



※「ボタンを押下したらメニューに追加される」動作は「プラグインの名前をメニューに表示する」方法の検討に直接関係ないコードであるため、先述のコードには記載していません。
「Unload」については、何度でも動作を確認できるようにするためのオマケ的な実装です。

4. まとめ

今回は、メニューにプラグインの名前を動的に追加の検討を行いました。
結果として、MenuItemオブジェクトに子メニュー(MenuItemオブジェクト)を追加することで、動的にメニューアイテムを追加できることが分かりました。
delegate/eventにより、ViewModelのイベントをView側で受け取るようにすることで、追加/表示する処理をView側にまとめることができました。
これにより、(今回は書いていませんが)MVVMの構造を崩すことなくプラグインの情報をデータベースから取得し、メニューに追加することができます。

今回示した方法は、一般的な方法でも無ければ、正解でもありません(多分…)。
あくまで個人的な、「こうすればできる」という方法にすぎません。
それでも、1つの手段ではあると思います。
問題は多々あると思いますが、何かの参考、誰かの一助になれば嬉しいです。

さて次は、今回の検討結果を踏まえて、実際にデータベースからプラグインを取得してメニューに表示、そこから実行する処理に繋げる方法について考えます。
ではっ!

ex. 公開しています

今回のエントリで紹介したコードは、ほんの一部です。
全コードは、GitHubDynamicMenuItemで公開しています。
より詳細な内容を見たい場合には、そちらを参照して下さい。