プログラム系統備忘録ブログ

記事中のコードは自己責任の下でご自由にどうぞ。

xamlでStaticResourceの結果にTypeConverterを適用する方法

2015/09/20 追記: 背景の説明に、xamlでの属性値の処理についての説明を追加

背景

  1. MenuItem.InputGestureTextKeyBinding.Gesture に同じ文字列を二回記述したくない
  2. x:StaticにしてもStaticResourceにしてもマークアップ拡張を通す場合は、 XAML Syntax In Detail | Microsoft Learn の"Processing of Attribute Values"にあるようにTypeConverterが使用されないため、string型をKeyBinding.Gestureに設定しようとしてXamlParseExceptionが発生する
  3. http://stackoverflow.com/questions/2382178/is-it-possible-to-supply-a-type-converter-for-a-static-resource-in-wpf に、依存プロパティならBinding.Sourceで変換できるとあったけれど、KeyBinding.Gestureは通常のプロパティなので使えない
  4. 同じ質問の他の回答で自作マークアップ拡張を使う方法が紹介されている、この方法なら出来るんじゃ

なおこの記事では「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になっても残っているとは)

上記で紹介されている解決方法には次のものがありますが、それぞれ欠点があります。

  • 自作マークアップ拡張を別アセンブリで定義する → 別DLLが出来てしまう
  • Property Element構文を使用する → Attribute構文より冗長になる
  • コンストラクタ引数で、ネストしたマークアップ拡張を受け取る → コンパイルは通りますが、デザイナでエラーが出てしまう

ひとまず今回の目的では、KeyGestureへの変換に特化し、Typeは受け取らないことで回避することにします。