No more Death March

あるSEのチラシの裏 C# WPF

WPF MultiBindingのコールバックの挙動について

WPFのMultiBindingを初めて使った時に若干嵌ったのでメモしておく。

テスト用ビヘイビア

コードビハインドでMultiBindingクラスを生成しバインディングする。マルチと言いつつTextプロパティしかバインディングしていないが確認するのには十分、コンバーター内で呼び出された旨をデバッグ出力し、ターゲットの添付プロパティもコールバック関数内で同様にデバッグ出力する。

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace CodeBehindBindingSample
{
    public static class TextBoxBehavior
    {


        public static bool GetIsAttached(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsAttachedProperty);
        }

        public static void SetIsAttached(DependencyObject obj, bool value)
        {
            obj.SetValue(IsAttachedProperty, value);
        }

        public static readonly DependencyProperty IsAttachedProperty = DependencyProperty.RegisterAttached("IsAttached", typeof(bool), typeof(TextBoxBehavior), new PropertyMetadata(false,OnIsAttachedChanged));

        private static void OnIsAttachedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var multiBinding =
                new MultiBinding()
                {
                    Mode = BindingMode.OneWay,
                    Converter = new MultiBindingConverter()
                };
            multiBinding.Bindings.Add(
                new Binding()
                {
                    Source = d,
                    Path = new PropertyPath(TextBox.TextProperty),
                }
                );

            BindingOperations.SetBinding(d, TextBoxBehavior.MultiBindingTargetProperty, multiBinding);
        }



        public static object[] GetMultiBindingTarget(DependencyObject obj)
        {
            return (object[])obj.GetValue(MultiBindingTargetProperty);
        }

        public static void SetMultiBindingTarget(DependencyObject obj, object[] value)
        {
            obj.SetValue(MultiBindingTargetProperty, value);
        }

        public static readonly DependencyProperty MultiBindingTargetProperty = DependencyProperty.RegisterAttached("MultiBindingTarget", typeof(object[]), typeof(TextBoxBehavior), new PropertyMetadata(new object[] { },OnMultiBindingTargetChanged));

        private static void OnMultiBindingTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("OnMultiBindingTargetChanged Called");
        }

        private class MultiBindingConverter
            : IMultiValueConverter
        {
            public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
            {
                System.Diagnostics.Debug.WriteLine("Convert Called");
                return values;
            }

            public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
            {
                throw new InvalidOperationException();
            }
        }
    }
}

画面

先のビヘイビアを有効にしたテキストボックスを画面におく。

<Window x:Class="CodeBehindBindingSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CodeBehindBindingSample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <TextBox local:TextBoxBehavior.IsAttached="True"/>
    </StackPanel>
</Window>

何が問題なのか

この画面を起動すると起動直後に以下のように出力される。

Convert Called
OnMultiBindingTargetChanged Called


ここまでは良い。SetBindingを実行したタイミングでバインディング先の添付プロパティに値が書き込まれることがわかる。問題はここからで、以降ソースとなっているTextBoxをいくら書き換えても以下の通りコンバーターのみ動作し、コールバック関数が呼ばれなくなる。

Convert Called
Convert Called
Convert Called

解決方法

解消するには以下の通り、コンバーター内で新しいインスタンスが戻り値として返るようにすればよい。

        private class MultiBindingConverter
            : IMultiValueConverter
        {
            public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
            {
                System.Diagnostics.Debug.WriteLine("Convert Called");
                // ToArrayメソッドで常に参照が変わるように
                return values.ToArray();
            }

            public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
            {
                throw new InvalidOperationException();
            }
        }


再度実行するとTextBoxのTextプロパティが変わる度にコールバック関数が呼ばれるようになる。

Convert Called
OnMultiBindingTargetChanged Called
Convert Called
OnMultiBindingTargetChanged Called
Convert Called
OnMultiBindingTargetChanged Called
Convert Called
OnMultiBindingTargetChanged Called

おそらくだが、MultiBindingは内部で同じ参照の配列を保持しソースの値が変わったらインスタンスを変えずに配列を編集しているのだろう。今回のケースだと初回のバインディング時に参照が渡されて以降参照が変わっていないため、コールバックが呼ばれなくなっていたと思われる。MultiBinding自体あまり使う機会もないだろうし、使ったとしてもなんらかの加工を加えインスタンスが生成された結果を利用すると思われるのであまり気にすることはないのだけど、運良く(?)こういう事象を踏み抜く場合もありますよと・・・・