No more Death March

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

C# ListをラップしたObservableCollectionのイベント

ObservableCollectionはコンストラクタにListを渡して生成することが出来る。が、生成した後にListクラスを通じてコレクションを編集してもコレクション変更イベントは発生しない。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<int>();
            var observable = new ObservableCollection<int>(list);
            observable.CollectionChanged += Observable_CollectionChanged;

            list.Add(1);
            list.Add(2);
            list.Add(3);
            Console.ReadKey();
        }

        private static void Observable_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            Console.WriteLine("Observable_CollectionChanged Called!!");
        }
    }
}
|cs|<

WPF TextBoxの不思議な挙動

忘れる前にメモしておく。上位からみてScrollViewer→StackPanel→Grid→TextBoxの順にビジュアルーツリーを構成し、横スクロール可能な状態でGridの列をStar指定で分割、TextBoxにテキストを入力しつづけて割り当てられたサイズを超えると指定した幅を無視してTextBoxの幅が大きくなってしまう。ScrollViewerのHorizontalScrollBarVIsibilityをHiddenにしてやれば解消可能。

これ仕様・・・?そこそこ複雑な画面で原因が分からずかなり焦った・・・

C# ObservableCollectionに対する操作とCollectionChangedイベントの内容

INotifyCollectionChangedインターフェースのCollectionChangedイベントについてMSDNの記述だけだとどういう通知が来るのかわからなかったのでプログラムを書いて確認してみた。

MSDNのページは以下

docs.microsoft.com

確認用のプログラム

Visual Studioで新規のコンソールアプリケーション作ってProgram.csにべたっとペーストすれば動く

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;

namespace CollectionChangedTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var col = new ObservableCollection<int>();
            col.CollectionChanged += Col_CollectionChanged;
            Debug.WriteLine("var col = new ObservableCollection<int>()");

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Add(1)");
            Debug.WriteLine("==========================================");
            col.Add(1);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Add(2)");
            Debug.WriteLine("==========================================");
            col.Add(2);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Add(3)");
            Debug.WriteLine("==========================================");
            col.Add(3);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Add(4)");
            Debug.WriteLine("==========================================");
            col.Add(4);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",",col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Remove(3)");
            Debug.WriteLine("==========================================");
            col.Remove(3);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.RemoveAt(1)");
            Debug.WriteLine("==========================================");
            col.RemoveAt(1);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Move(0,1)");
            Debug.WriteLine("==========================================");
            col.Move(0,1);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Insert(1,5)");
            Debug.WriteLine("==========================================");
            col.Insert(1, 5);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.OrderBy(x => x)");
            Debug.WriteLine("==========================================");
            Debug.WriteLine("※異なる参照が返るのでイベントは発生しない。");
            col.OrderBy(x => x);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Reverse()");
            Debug.WriteLine("==========================================");
            Debug.WriteLine("※異なる参照が返るのでイベントは発生しない。");
            col.Reverse();
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Clear()");
            Debug.WriteLine("==========================================");
            col.Clear();
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Console.ReadKey();
        }

        private static void Col_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            var sb = new StringBuilder();


            sb.AppendLine("↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓");
            sb.AppendLine(string.Format("e.Action = {0}",e.Action.ToString()));

            if(e.NewItems == null)
            {
                sb.AppendLine("e.NewItems == null");
            }
            if(e.NewItems != null)
            {
                sb.AppendLine(string.Format("e.NewItems.Count = {0}", e.NewItems.Count.ToString()));
                sb.AppendLine(string.Format("e.NewItems = {0}", string.Join(",", e.NewItems.Cast<int>().ToArray().Select(x => x.ToString()))));
            }
            sb.AppendLine(string.Format("e.NewStartingIndex = {0}",e.NewStartingIndex.ToString()));

            if (e.OldItems == null)
            {
                sb.AppendLine("e.Oldtems == null");
            }
            if (e.OldItems != null)
            {
                sb.AppendLine(string.Format("e.OldItems.Count = {0}", e.OldItems.Count.ToString()));
                sb.AppendLine(string.Format("e.OldItems = {0}", string.Join(",", e.OldItems.Cast<int>().ToArray().Select(x => x.ToString()))));
            }
            

            sb.AppendLine(string.Format("e.OldStartingIndex = {0}", e.OldStartingIndex.ToString()));

            Debug.Write(sb.ToString());
    }
    }
}

実行結果

実行すると以下の通りデバッグ出力が得られた。

var col = new ObservableCollection<int>()
==========================================
col.Add(1)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Add
e.NewItems.Count = 1
e.NewItems = 1
e.NewStartingIndex = 0
e.Oldtems == null
e.OldStartingIndex = -1
col = 1
==========================================
col.Add(2)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Add
e.NewItems.Count = 1
e.NewItems = 2
e.NewStartingIndex = 1
e.Oldtems == null
e.OldStartingIndex = -1
col = 1,2
==========================================
col.Add(3)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Add
e.NewItems.Count = 1
e.NewItems = 3
e.NewStartingIndex = 2
e.Oldtems == null
e.OldStartingIndex = -1
col = 1,2,3
==========================================
col.Add(4)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Add
e.NewItems.Count = 1
e.NewItems = 4
e.NewStartingIndex = 3
e.Oldtems == null
e.OldStartingIndex = -1
col = 1,2,3,4
==========================================
col.Remove(3)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Remove
e.NewItems == null
e.NewStartingIndex = -1
e.OldItems.Count = 1
e.OldItems = 3
e.OldStartingIndex = 2
col = 1,2,4
==========================================
col.RemoveAt(1)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Remove
e.NewItems == null
e.NewStartingIndex = -1
e.OldItems.Count = 1
e.OldItems = 2
e.OldStartingIndex = 1
col = 1,4
==========================================
col.Move(0,1)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Move
e.NewItems.Count = 1
e.NewItems = 1
e.NewStartingIndex = 1
e.OldItems.Count = 1
e.OldItems = 1
e.OldStartingIndex = 0
col = 4,1
==========================================
col.Insert(1,5)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Add
e.NewItems.Count = 1
e.NewItems = 5
e.NewStartingIndex = 1
e.Oldtems == null
e.OldStartingIndex = -1
col = 4,5,1
==========================================
col.OrderBy(x => x)
==========================================
※異なる参照が返るのでイベントは発生しない。
col = 4,5,1
==========================================
col.Reverse()
==========================================
※異なる参照が返るのでイベントは発生しない。
col = 4,5,1
==========================================
col.Clear()
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Reset
e.NewItems == null
e.NewStartingIndex = -1
e.Oldtems == null
e.OldStartingIndex = -1
col = 

イベント発生時に通知されるNotifyCollectionChangedEventArgsの各メンバーについてメモ

Action

・操作の内容によって値が変わる。
・Add→Add
・Insert→Add
・Move→Move
・Remove→Remove
・RemoveAt→Remove
・Clear→Reset
・SetItem→Replace(?) ※SetItemはprotected、今回は確認していない。

NewItems

・Addメソッド、Insertメソッド、Moveメソッドで対象要素のインスタンスが格納される。
・これ以外のメソッドはnullとなる。
・たぶんSetItemもここに入るだろう。

NewStartingIndex

・NewItemsの開始位置を示す
・NewItemsにインスタンスが格納されていない場合-1を取る

OldItems

・Removeメソッド、RemoveAtメソッド、Moveメソッドで削除、移動前の要素のインスタンスが格納される
・これ以外のメソッドはnullとなる
・SetItemもはいる??

OldStartingIndex

・OldItemsの開始位置を示す。
・つまり「削除・移動前に要素が入っていた位置」
・OldItemsにインスタンスが格納されていない場合-1となる

Clearメソッドについて補足

・ActionメンバーがResetを取る。
・NewItemとOldItemはnull
・NewStartingIndexとOldStartingIndexはともに-1

OrderByメソッドやReverseメソッドについて補足

・異なる参照が返るので元のコレクションの内容は変わらない
・したがってイベントも発生しない

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自体あまり使う機会もないだろうし、使ったとしてもなんらかの加工を加えインスタンスが生成された結果を利用すると思われるのであまり気にすることはないのだけど、運良く(?)こういう事象を踏み抜く場合もありますよと・・・・

WPF Window起動時のイベント発生順位をメモ

余り複雑な制御を行っているコントロールを作るのは好ましくないが、どうしてもやらなければならない場合が結構あったりする。そんな時に毎回イベントの発生順位ってどうだっけ?と忘れてしまうことがあるのでメモしておく。後述の通りサンプル画面を作ってビヘイビアで各イベントが着火したタイミングで発生したイベントをデバッグプリントしてやる。

<Window x:Class="WpfEventTracking.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:WpfEventTracking"
        mc:Ignorable="d"
        local:WindowBehavior.IsAttached="True"
        Title="MainWindow" Height="450" Width="800"
        >
    <GroupBox Header="GroupBox" local:ControlBehavior.IsAttached="True">
        <StackPanel>
            <TextBox Text="TextBox" local:ControlBehavior.IsAttached="True"/>
            <Button Content="Button" local:ControlBehavior.IsAttached="True"/>
        </StackPanel>
    </GroupBox>
</Window>

全てのイベントを網羅すると大変なので今回はUIElement、FrameworkElement、Control、Windowのイベントのみ出力する。

Window起動時

TextBox Initialized
Button Initialized
GroupBox Initialized
MainWindow Initialized
MainWindow IsVisibleChanged
GroupBox IsVisibleChanged
TextBox IsVisibleChanged
Button IsVisibleChanged
MainWindow SizeChanged
GroupBox SizeChanged
Button SizeChanged
TextBox SizeChanged
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
MainWindow Activated
MainWindow PreviewGotKeyboardFocus
MainWindow IsKeyboardFocusWithinChanged
MainWindow IsKeyboardFocusedChanged
MainWindow GotKeyboardFocus
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
MainWindow Loaded
GroupBox Loaded
TextBox Loaded
Button Loaded
MainWindow ContentRendered
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated

LayoutUpdatedイベントまで出すと見難い・・・

Window起動時(LayoutUpdate無しver)

TextBox Initialized
Button Initialized
GroupBox Initialized
MainWindow Initialized
MainWindow IsVisibleChanged
GroupBox IsVisibleChanged
TextBox IsVisibleChanged
Button IsVisibleChanged
MainWindow SizeChanged
GroupBox SizeChanged
Button SizeChanged
TextBox SizeChanged
MainWindow Activated
MainWindow PreviewGotKeyboardFocus
MainWindow IsKeyboardFocusWithinChanged
MainWindow IsKeyboardFocusedChanged
MainWindow GotKeyboardFocus
MainWindow Loaded
GroupBox Loaded
TextBox Loaded
Button Loaded
MainWindow ContentRendered

LayoutUpdatedイベントを非表示にした再度確認

Window終了時

MainWindow Closing
MainWindow IsVisibleChanged
GroupBox IsVisibleChanged
TextBox IsVisibleChanged
Button IsVisibleChanged
MainWindow Deactivated
MainWindow IsKeyboardFocusWithinChanged
MainWindow IsKeyboardFocusedChanged
MainWindow LostKeyboardFocus
MainWindow Closed

ついでなのでWindow終了時の結果もメモ

まとめ

フォーカス取得系を省いて順番を整理すると
Initialize→IsVisibleChanged→SizeChanged→Activated→Loaded→ContentRendered→(画面表示完了)→Closing→IsVisibleChanged→Deactivec→Closed

バブルイベントとトンネルイベントを分けると

バブルイベント
Initialized

トンネルイベント
IsVisibleChanged
SizeChanged
Loaded

といった具合になる。

少し検索してみるとここで確認した通り、Windowのイベント発生順位がまとめられてた。

stackoverflow.com

WPF DependencyPropertyDescriptorのAddValueChanged

前回投稿した記事の中で触れていたDependencyPropertyDescriptorで実際にメモリリークするかどうかを試してみた。

前回の記事はこちら↓
nomoredeathmarch.hatenablog.com

画面側のコードは省くが、StackPanelとButtonを二つ配置、片方のボタンを押したらStackPanelに後述のビヘイビアを添付したTextBoxを1000個追加、もう一つのボタンを押したらStackPanelのChildrenをClearメソッドでクリアする。別途TextBlockを配置し、こちらはDespatcherTimerを使って周期的にメモリ使用量を取得して表示する。

ビヘイビアは以下の通り、TextChangedイベントを購読した場合とDependencyPropertyDescriptorのAddValueChangedメソッドを使う二通りの処理を用意しメモリの状況を確認してみた。

using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;

namespace BehaviorMemoryLeak
{
    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(IsAttachedChanged));

        private static void IsAttachedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            /*
             * TextChangeを購読
            (d as TextBox).TextChanged += (sender, args) => { System.Diagnostics.Debug.WriteLine("TextChanged"); };
             */

            /* 
             * AddValueChangedでプロパティの変更を検知
            DependencyPropertyDescriptor
                .FromProperty(TextBox.TextProperty, typeof(TextBox))
                .AddValueChanged(d, (sender, args) => { System.Diagnostics.Debug.WriteLine("TextChanged"); });
             */
        }

    }
}


結果やはりDependencyPropertyDescriptorを使った方法はメモリリークしていた。WPFの基本となる依存関係プロパティの仕組み上、この辺りの制御は一通り弱参照を駆使しているのだろうなと勝手に思っていたのだがどうやら違う様子。

WPF TextBlockのTextChangedイベント

TextBlockにはTextChangedというイベントは無いのだけど変更されたタイミングを知る方法はある。検索すれば色々やり方が出てくるけどいくつか種類があるので要点をメモ

TargetUpdatedイベントを使う

TargetUpdateイベントはバインディングのターゲットが更新された時に着火するイベント、BindingのオプションにあるNotifyTargetUpdatedにtrueを指定する必要がある。イベント引数の中を探ればどのプロパティをターゲットにしたバインディングが更新されたかを知ることが出来る。おそらく検索すると一番最初に目に付くかと思うが、TargetUpdateイベントの中でプロパティを判定しなければいけないところが余り好きじゃない。

PropertyDescriptorを使う

PropertyDescriptorというstaticクラスにあるAddValueChangedにイベントハンドラーを登録し変更通知を受け取ってやる。ちゃんと登録を解除してやらないとメモリリークするか。

依存関係プロパティを作成し、変更通知を受け取りたいプロパティをバインディングする

自分が使うことが多いのはこのやり方、ビヘイビアの中にstring型の添付プロパティを作成しTextBlockのTextプロパティをソースにバインディングしてやる。


どの方法を採用すべきかはお好みで、TextBlockのTextプロパティに限らず、TextBoxのIsReadOnlyやCanvasのLeftなんかも〇〇Changedといったイベントが用意されていないので、応用出来る。地味だけど重宝するテクニックです。