C#でプラグイン機能を開発に挑戦してみた(5)
メニューへのプラグインの表示処理

どもです。
…スミマセンデシタ!



…いや、謝ることではないですね。

0. いきなりどうした!?

前回のエントリ前々回のエントリで、あまり適当ではないコードを紹介してしまいました!
具体的には、プラグインの一覧をメニューに表示するためのコードです。
プラグインの一覧をメニューに表示するために、ViewModelからViewに対してイベントを発行し、イベント引数でメニューで表示したいプラグインの名前をコードビハインドで設定する、という実装を紹介しました。
しかしこの機能は、バインディングを適用することで、もっと簡単に実装ができます。

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
データベース LiteDB Ver.5.0.11

2. メニューへのデータバインディング

MenuItemには、「ItemsSource」プロパティがあります。
このプロパティの詳細はMSDNに譲り、ここでは目的に対する実装に着目します。
このプロパティに対して、プラグインの一覧を設定します。
具体的には、以下のようなコードになります。
まずはC#のViewModelのプロパティの実装です。

public class ViewModel
{
	protected ObservableCollection<PluginInfo> _defaultPlugins;

	public ObservableCollection<PluginInfo> DefaultPlugins
		{
			get => this._defaultPlugins;
			protected set
			{
				this._defaultPlugins = value;
				this.RaisePropertyChanged(nameof(DefaultPlugins));
			}
		}
}

ウィンドウのXAML、メニューアイテムは次のように実装します。

<MenuItem Header="生成"
	x:Name="RunMenuRoot"
	Style="{StaticResource MenuItem_MenuBar}"
	>
	<MenuItem Header="スタブ・ドライバ"
		IsTabStop="True"
		ItemsSource="{Binding DefaultPlugins}"
		/>
	/>

しかしながらこの実装では、プラグインの名前がメニューに表示されません。
また、メニューのアイテムが選択された際に実行する処理と、その処理に渡す引数情報が設定できていません。

2.1. プラグイン名の表示

ItemsSourceを設定したメニューに「名前」を表示するためには、「ItemContainerStyle」を使用します。
具体的には、XAMLを以下のように実装します。

<MenuItem Header="生成"
	x:Name="RunMenuRoot"
	Style="{StaticResource MenuItem_MenuBar}"
	>
	<MenuItem Header="スタブ・ドライバ"
		IsTabStop="True"
		ItemsSource="{Binding DefaultPlugins}"
		>
		<MenuItem.ItemContainerStyle>
			<Style TargetType="MenuItem">
				<Setter Property="Header" Value="{Binding Path=Name}"/>
			</Style>
		</MenuItem.ItemContainerStyle>l
	<MenuItem>
/>

このコードでは、「Header」プロパティに対して「Name」をバインドするように設定しています。
「Name」プロパティには、メニューに表示するプラグインの名前が格納されています。
これにより、「MenuItem」にバインディングされたリスト内の要素の「PluginInfo」オブジェクトの「Name」プロパティに設定された値がメニューに表示されます。
結果として、プラグインの名前がメニューに表示されるようになります。

2.2. プラグインの実行処理

メニューのアイテムが選択された際に実行される処理は、「ItemContainerStyle」で「Command」プロパティに値を設定します。
また「CommandProperty」プロパティに、処理に対して渡す引数を設定します。
具体的なコードは、次になります。

<MenuItem Header="生成"
	x:Name="RunMenuRoot"
	Style="{StaticResource MenuItem_MenuBar}"
	>
	<MenuItem Header="スタブ・ドライバ"
		IsTabStop="True"
		ItemsSource="{Binding DefaultPlugins}"
		>
		<MenuItem.ItemContainerStyle>
			<Style TargetType="MenuItem">
				<Setter Property="Header" Value="{Binding Path=Name}"/>
				<Setter Property="Command" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=MenuItem}, Path=DataContext.DefaultPluginCommand}"/>
				<Setter Property="CommandParameter" Value="{Binding}"/>
			</Style>
		</MenuItem.ItemContainerStyle>l
	<MenuItem>
/>

「Command」プロパティについては、DataContext即ちViewModelのCommandプロパティを設定します。
「CommandProperty」には、単純に「{Binding}」とだけ設定します。
これにより、メニューに対応する「PluginInfo」オブジェクトをコマンドの引数に設定ができます。

前回のエントリ紹介したコードでは、選択されたメニューのPluginInfoのIDを渡していました。
これに対して、上述のコードではPluginInfoオブジェクトそのものを渡すことができます。
結果としてViewModel側での「ID」からプラグインの情報を取得して…という処理を省略できます。

3. まとめ

今回は、MenuItemのItemsSourceにBindingを設定することで、動的にメニューの表示を追加する処理について、「よりよい実装」を提案してみました。(言い訳)
前回のエントリ前々回のエントリで紹介したコードは、勿論「間違いではない」と考えています。(言い訳)
しかしながら今回紹介した方法は、それよりもよい方法であると考えています。(言い訳ではない!)
実装する内容などによって「適当な実装」は変化すると思いますので、適宜選択することが肝要です。
一つ事に囚われすぎず、常に「最適」「適当」を目指すようにしたいです。
これまでのエントリが、そのための選択肢の一助になれば幸いです。

ではっ!