No more Death March

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

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といったイベントが用意されていないので、応用出来る。地味だけど重宝するテクニックです。

WPF Bindingの解除は必要なのか

StackOverFlowから、ざっくりいうと

質問が「Bindingって解除しないとメモリリークする?」ということで、
それに対する回答が
「DependencyPropertyかINotifyPropertyChangedを実装しているなら大丈夫、CLRオブジェクトだとメモリリークするかも。」
とのこと。

c# - Can bindings create memory leaks in WPF? - Stack Overflow

変更チェックのためにPropertyDescriptorが対象のオブジェクトに対する強参照を保持してしまうようだけど、OneTimeなら大丈夫みたい。一回こっきりで値がわかれば良いからね。

WPF ScrollViewerとPreviewKeyDown

画面上にButtonやToggleButtonを並べて配置すると矢印キーの操作でフォーカスを移動させることが出来るが、この連続したボタンをScrollViewerでラップするとフォーカス移動が効かなくなり、代わりにScrollViewerのスクロールバーが押した矢印の方向に動くようになる。

スクロールバーの動きを無効化してボタン間のフォーカス移動を復活させようとしたとき、ボタン側のKeyDownイベント等でフォーカスを操作しようとしても上手くいかない、というのもボタンでKeyDownイベントが着火する前にスクロールバーのPreviewKeyDownイベントでスクロール操作が処理され、下位のUI要素には処理済みとしてイベントが通知されなくなるからだ。

WPF PreviewKeyDownとPreviewKeyUpの動き

またWPFについてちょとしたメモ、プログラムで嵌ったことはいつかの自分のためなるべくメモしておきたい。

今回はPreviewKeyDownイベントとPreviewKeyUpイベントについて書いておく。当初、キーを押したらKeyDownイベントがキーを離したらKeyUpイベントが着火する程度の認識だったんだけど、コントロールをカスタマイズしててなかなか上手く行かないことがあり、よくよく調べて見れば思ってたのと大分違う動き方をしていた。

端的に結論だけ、まずキーを押下するとKeyDownイベントが着火するのだが、キーを押しっぱなしにしている限りイベントは連続で着火し続ける。でキーを離すとKeyUpイベントが着火する。加えて、例えばShiftキーとAltキーを同時押ししていた場合、キーを押しっぱなしにしている間はKeyDownイベントが連続で着火するのだが、どちらかのキーを離し、KeyUpイベントが着火すると、新たにキーを押下するまでKeyDownイベントは着火しなくなる。

つまり、KeyDownはKeyUpが着火するまで連続で着火し続け、KeyUpが発生すると押しているキーが残っているか否かを問わず、次のKeyDown操作が行われるまでイベントは発生しなくなるということ。

WPF TextBoxのIsReadOnlyとIsEnabledの組み合わせについて

WPFに関するちょっとしたメモ、TextBoxを修正させたくない場合基本的にはIsReadOnlyプロパティにTrueを設定するかIsEnabledにFalseを設定すると思われる。

が、IsReadOnly=False、IsEnabled=Trueの場合に限って、少し困ったことになる場合がある。それは、確かにキーボードによる文字入力自体は出来なくなるのだけど、既にテキストが入っている状態だと右クリックによるテキストの変換が動いてしまうということ。

TextBoxをカスタマイズして数字入力コントロールを作ったりした時、この仕様を理解していないとコードビハインドの制約をすり抜けて数字が漢数字に変換されてしまったりする。なので、TextBoxにIsEnabled=Falseと指定するのであれば、合わせてIsReadOnly=Trueを設定するようにしたい。

他にもコンテキストメニュー自体を無効化したり、入れ替えたりとやり方は色々あるのだけど、とにかくこういう動きが「仕様」ってことを抑えて置かないと意図しない文字が設定されることがあるので注意が必要だ。

ソースの記事自体はどこで見たか忘れたしまったのだけど確かMSDNでもこれに関して説明があってIsEnabled=Falseにするなら一緒にIsReadOnly=Trueにしましょう、という旨の解説がされていたと思う。

直感的にはIsEnabled=Falseであればコードで直接Textプロパティを操作する以外、テキストが変更されるようなことはないと思うが、これは「仕様」らしい・・・