添付プロパティとBindingMode

2024年12月30日月曜日

C# WPF

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

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

サンプル

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

C#:

public static class TextBoxExtensions
{
    // バインドできる SelectedText の添付プロパティ
    public static readonly DependencyProperty BindableSelectedTextProperty = DependencyProperty.RegisterAttached(
        "BindableSelectedText",
        typeof(string),
        typeof(TextBoxExtensions),
        new FrameworkPropertyMetadata(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;

        // モードが TwoWay か OneWayToSource なら TextBox のイベントを購読する (ターゲット -> ソース) 
        if ((mode == BindingMode.TwoWay) || (mode == BindingMode.OneWayToSource))
        {
            WeakEventManager<TextBox, RoutedEventArgs>.AddHandler(
                textBox, nameof(textBox.SelectionChanged), TextBox_SelectionChanged);
        }

        // モードが OneWayToSource 以外なら TextBox に反映する (ソース -> ターゲット) 
        if (mode != BindingMode.OneWayToSource)
        {
            if ((e.NewValue is string text) && (text != textBox.SelectedText))
            {
                textBox.SelectedText = text;
            }
        }
    }

    private static void TextBox_SelectionChanged(object? sender, RoutedEventArgs e)
    {
        if (sender is not TextBox textBox)
        {
            return;
        }

        if (textBox.SelectedText != GetBindableSelectedText(textBox))
        {
            SetBindableSelectedText(textBox, textBox.SelectedText);
        }
    }
}

XAML:

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

実際の挙動

添付ビヘイビアのOnBindableSelectedTextChangedメソッドをA、TextBox_SelectionChangedメソッドをBとしたとき、BindingModeによって挙動は次のように変わりました。

OneWayToSourceの場合は思った通りの挙動をしません。回避策としては、BindableSelectedText添付プロパティとは別にTextBoxのイベントを購読するための添付プロパティを用意して、そちらからBindableSelectedText添付プロパティの値を変更します(参照先にコードがあります)。

  • TwoWay: 思った通りの挙動をする
    • Aは呼ばれる
    • Bは呼ばれる
  • OneWay: 思った通りの挙動をする
    • Aは呼ばれる
    • Bは呼ばれない(未購読なので当然)
  • OneTime: 思った通りの挙動をする
    • Aは1回だけ呼ばれる
    • Bは呼ばれない(未購読なので当然)
  • OneWayToSource: 思った通りの挙動をしない
    • Aは呼ばれない
    • Bは呼ばれない(未購読なので当然)

参照