2015/09/20 追記: 背景の説明に、xamlでの属性値の処理についての説明を追加
背景
- MenuItem.InputGestureText と KeyBinding.Gesture に同じ文字列を二回記述したくない
- x:StaticにしてもStaticResourceにしてもマークアップ拡張を通す場合は、 XAML Syntax In Detail | Microsoft Learn の"Processing of Attribute Values"にあるようにTypeConverterが使用されないため、string型をKeyBinding.Gestureに設定しようとしてXamlParseExceptionが発生する
- http://stackoverflow.com/questions/2382178/is-it-possible-to-supply-a-type-converter-for-a-static-resource-in-wpf に、依存プロパティならBinding.Sourceで変換できるとあったけれど、KeyBinding.Gestureは通常のプロパティなので使えない
- 同じ質問の他の回答で自作マークアップ拡張を使う方法が紹介されている、この方法なら出来るんじゃ
なおこの記事では「StaticResourceにTypeConverterを適用」にしていますが、同様に「x:StaticにIValueConverterを適用」なども可能だと思います(最後に記述していますが、xamlパーサーのバグには注意)。
(改善前)Gestureに同じ文字列を二回記述してしまっている
C#コードで確認用の適当なコマンドを定義します。
public class TestCommand : ICommand { public event EventHandler CanExecuteChanged; public bool CanExecute(object parameter) => true; public void Execute(object parameter) { MessageBox.Show("Test"); } }
xamlでMenuItemとKeyBindingにCommandを割り当てます。
DataContextにTestCommandそのものを指定しているため、Bindingマークアップ拡張は引数無しで動作します。
<Window x:Class="WpfTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfTest" Title="MainWindow"> <Window.DataContext> <local:TestCommand /> </Window.DataContext> <Window.ContextMenu> <ContextMenu> <MenuItem Command="{Binding}" Header="Test" InputGestureText="Ctrl+T" /> </ContextMenu> </Window.ContextMenu> <Window.InputBindings> <KeyBinding Command="{Binding}" Gesture="Ctrl+T" /> </Window.InputBindings> </Window>
(改善後)自作マークアップ拡張を通して、Gesture用文字列を使い回す
C#コードでStaticResourceの結果をKeyGestureに変換するマークアップ拡張を定義します。
public class StaticResourceKeyGestureExtension : StaticResourceExtension { public StaticResourceKeyGestureExtension() { } public StaticResourceKeyGestureExtension(object resourceKey) : base(resourceKey) { } public override object ProvideValue(IServiceProvider serviceProvider) { var resource = base.ProvideValue(serviceProvider); return new KeyGestureConverter().ConvertFrom(resource); } }
xamlで、リソースとしてGesture用文字列を定義し、MenuItemとKeyBindingでそれぞれ使用します。
MenuItem.InputGestureTextはstring型なのでStaticResourceを使用し、KeyBinding側は自作マークアップ拡張を使用します。
<Window ... xmlns:sys="clr-namespace:System;assembly=mscorlib"> <Window.Resources> <sys:String x:Key="GestureString">Ctrl+T</sys:String> </Window.Resources> ... <MenuItem ... InputGestureText="{StaticResource GestureString}" /> ... <KeyBinding ... Gesture="{local:StaticResourceKeyGesture GestureString}" /> ... </Window>
xamlパーサーのバグについて
上記の自作マークアップ拡張で、使用するTypeConverterをxamlから指定可能にすれば、より汎用的になるでしょう。
しかし、 *同一アセンブリで定義している自作マークアップ拡張の使い方によっては* コンパイルエラーとなってしまします。
C#コード、使用するTypeConverterをxamlから設定できるようにプロパティを追加しています。
public class MyStaticResourceExtension : StaticResourceExtension { public MyStaticResourceExtension() { } public MyStaticResourceExtension(object resourceKey) : base(resourceKey) { } public Type TypeConverterType { get; set; } public override object ProvideValue(IServiceProvider serviceProvider) { var resource = base.ProvideValue(serviceProvider); var typeConverter = (TypeConverter)Activator.CreateInstance(this.TypeConverterType); return typeConverter.ConvertFrom(resource); } }
xaml、KeyBindingにて、KeyGestureConverterをプロパティに設定しています。
<Window ...> ... <!--自作マークアップ拡張の中に、さらにx:Typeマークアップ拡張がネストしている--> <KeyBinding ... Gesture="{local:MyStaticResource GestureString, TypeConverterType={x:Type KeyGestureConverter}}" /> ... </Window>
コンパイルエラーは次の二つです。
(英語表記) Unknown property 'TypeConverterType' for type 'MS.Internal.Markup.MarkupExtensionParser+UnknownMarkupExtension' encountered while parsing a Markup Extension. Line 23 Position 21.
(日本語表記) Markup Extension の解析時に、型 'MS.Internal.Markup.MarkupExtensionParser+UnknownMarkupExtension' に対する不明なプロパティ 'TypeConverterType' が見つかりました。 行 23 位置 21.
(英語表記) The name "MyStaticResource" does not exist in the namespace "clr-namespace:WpfTest".
(日本語表記) 名前 "MyStaticResource" は名前空間 "clr-namespace:WpfTest" に存在しません。
このバグについては次の記事で紹介されています。(VS2008から存在するらしいですが、2015になっても残っているとは)
- http://www.hardcodet.net/2008/04/nested-markup-extension-bug
- http://stackoverflow.com/questions/11785248/error-when-binding-using-markup-extensions-unknown-property-encountered-while-p
上記で紹介されている解決方法には次のものがありますが、それぞれ欠点があります。
- 自作マークアップ拡張を別アセンブリで定義する → 別DLLが出来てしまう
- Property Element構文を使用する → Attribute構文より冗長になる
- コンストラクタ引数で、ネストしたマークアップ拡張を受け取る → コンパイルは通りますが、デザイナでエラーが出てしまう
ひとまず今回の目的では、KeyGestureへの変換に特化し、Typeは受け取らないことで回避することにします。