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);
}
}
}
行っている内容は、以下の通りです。
- イベントハンドラでイベントを受け取る
- イベント引数に格納された文字列一覧を取得する
- 子メニューオブジェクト(MenuItemオブジェクト)を生成する。
- MenuItemオブジェクトに、表示したい文字列をセットする。
- 親メニュー(rootMenuItem)に追加する
- これを、文字列一覧のそれぞれの要素に対して実施する
最後に、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. 公開しています
今回のエントリで紹介したコードは、ほんの一部です。
全コードは、GitHubのDynamicMenuItemで公開しています。
より詳細な内容を見たい場合には、そちらを参照して下さい。
ディスカッション
コメント一覧
まだ、コメントがありません