添付プロパティとBindingMode

2024年12月30日月曜日

C#

記事のカテゴリー: C#、.NET 9、WPF

自作の添付プロパティを使用するとき、XAMLで指定するBindingModeによって変わる挙動についてまとめてみました。

サンプル

例として、TextBoxのSelectedTextプロパティ(選択範囲の文字を取得、設定するプロパティ)にバインドできるようにする添付ビヘイビアを用意します。OnBindableSelectedTextChangedメソッドでBindingModeを判断して必要な処理だけをする、という内容になっています。実際に動かしてみると、BindingModeがOneWayToSourceの場合は思った通りに動いてくれません。

C#:

public static class TextBoxExtensions
{
    private static List<TextBox> attachingTargets = [];
    
    // TextBox の SelectedText プロパティにバインドできるようにする添付プロパティ
    public static readonly DependencyProperty BindableSelectedTextProperty
        = DependencyProperty.RegisterAttached(
            "BindableSelectedText",
            typeof(string),
            typeof(TextBoxExtensions),
            new PropertyMetadata(string.Empty, OnBindableSelectedTextChanged));

    public static string GetBindableSelectedText(DependencyObject obj)
    {
        return (string)obj.GetValue(BindableSelectedTextProperty);
    }

    public static void SetBindableSelectedText(DependencyObject obj, string value)
    {
        obj.SetValue(BindableSelectedTextProperty, value);
    }

    // 添付プロパティの値が変わったときに呼ばれる処理
    private static void OnBindableSelectedTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not TextBox textBox)
        {
            return;
        }
        
        BindingMode mode = BindingOperations.GetBinding(d, BindableSelectedTextProperty).Mode;

        // BindingMode が TwoWay か OneWayToSource の場合は TextBox の変更を監視する
        if ((mode == BindingMode.TwoWay) || (mode == BindingMode.OneWayToSource))
        {
            if (!attachingTargets.Contains(textBox))
            {
                attachingTargets.Add(textBox);
                textBox.SelectionChanged += TextBox_SelectionChanged;
            }
        }

        // BindingMode が OneWayToSource でない場合は TextBox に値を反映する
        if (mode != BindingMode.OneWayToSource)
        {
            if ((e.NewValue is string value) && (value != textBox.SelectedText))
            {
                textBox.SelectedText = value;
            }
        }
    }

    // TextBox の選択範囲のインデックスや文字が変わったときに呼ばれる処理
    private static void TextBox_SelectionChanged(object sender, RoutedEventArgs e)
    {
        if ((sender is TextBox textBox) && (textBox.SelectedText != GetBindableSelectedText(textBox)))
        {
            SetBindableSelectedText(textBox, textBox.SelectedText);
        }
    }
}

XAML:

<TextBox local:TextBoxExtensions.BindableSelectedText="{Binding SelectedText, Mode=TwoWay}" />

実際の挙動

XAMLで指定するBindingModeによって挙動は次のようになりました。 OneWayToSourceの場合は思った通りの挙動をしません。回避策としては、BindableSelectedText添付プロパティとは別にTextBoxの監視をするための添付プロパティを用意して、そちらからBindableSelectedTextの値を変更する方法があります(参照先にコードがあります)。

  • TwoWay: 思った通りの挙動をする
    • OnBindableSelectedTextChanged メソッドは呼ばれる
    • TextBox_SelectionChanged メソッドは呼ばれる
  • OneWay: 思った通りの挙動をする
    • OnBindableSelectedTextChanged メソッドは呼ばれる
    • TextBox_SelectionChanged メソッドは呼ばれない(未登録なので当然)
  • OneTime: 思った通りの挙動をする
    • OnBindableSelectedTextChanged メソッドは1回だけ呼ばれる
    • TextBox_SelectionChanged メソッドは呼ばれない(未登録なので当然)
  • OneWayToSource: 思った通りの挙動をしない
    • OnBindableSelectedTextChanged メソッドは呼ばれない 
    • TextBox_SelectionChanged メソッドは呼ばれない(未登録なので当然)

参照