none
依存プロパティのPropertyChangedCallbackの動きについて RRS feed

  • 質問

  • こんにちは。依存プロパティにあるPropertyChangedCallbackで呼び出される動きについて質問です。
    下記のSampleTextプロパティを変更したときに、その通知をバインディングしている先のオブジェクトに毎回通知したいです。
    自分の作ったプログラムだとFor文の最後の1回、Test9という結果だけがPropertyChangedCallbackで登録されたメソッドで通知されています。これをFor文1回1回、毎回通知するわけにはいかないのでしょうか?

    入力されたログをすべて保存したいと考えたのですが、バインディングの理解が浅くてこの挙動がどういったものなのかよくわからないでいます。わかる方いましたら、お手数ですが教えてください。

    よろしくお願いします。

    問題の起こるサンプルのプロジェクト
    http://1drv.ms/1VYjPDJ

    public partial class MainWindow : Window, INotifyPropertyChanged
    {
    	public event PropertyChangedEventHandler PropertyChanged;
    
    	private string _SampleText;
    
    	public string SampleText
    	{
    	    get { return _SampleText; }
    	    set
    	    {
    	        _SampleText = value;
    	        RaiseChangedProperty(this, "SampleText");
    	    }
    	}
    
    	public MainWindow()
    	{
    	    InitializeComponent();
    
    	    this.DataContext = this;
    
    	    var testCount = 10;
    	    for (var i = 0; i < testCount; i++)
    	    {
    	        SampleText = "Test" + i.ToString();
    	    }
    
    	}
    
    	/// <summary>
    	/// <see cref="System.ComponentModel.INotifyPropertyChanged.PropertyChanged"/> イベントを発生させます。
    	/// </summary>
    	/// <param name="sender">イベントを発生させる元になるオブジェクト。</param>
    	/// <param name="propertyName">変化が発生したプロパティ名のテキスト。</param>
    	public void RaiseChangedProperty(object sender, string propertyName)
    	{
    	    if (PropertyChanged != null)
    	        PropertyChanged(sender, new PropertyChangedEventArgs(propertyName));
    	}
    }
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Diagnostics;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
    
    namespace _160111__LogSample
    {
        /// <summary>
        /// LogBar.xaml の相互作用ロジック
        /// </summary>
        public partial class LogBar : UserControl
        {
            private List<Tuple<string, DateTime>> _conditions = new List<Tuple<string, DateTime>>();
    
            public static readonly DependencyProperty ConditionProperty =
                DependencyProperty.Register(
                "Condition",
                typeof(string),
                typeof(LogBar),
                new FrameworkPropertyMetadata("Condition", new PropertyChangedCallback(OnConditionChanged))
            );
    
            #region Properties
    
            /// <summary>
            /// <see cref="Condition"/> プロパティに設定した状態ログを取得します。
            /// </summary>
            public IEnumerable<Tuple<string, DateTime>> Conditions
            {
                get { return _conditions; }
            }
    
            /// <summary>
            /// ウィンドウなどの状態を表すテキストを取得または設定します。
            /// </summary>
            [LocalizabilityAttribute(LocalizationCategory.NeverLocalize)]
            [BindableAttribute(true)]
            public string Condition
            {
                get
                {
                    var condition = (string)GetValue(ConditionProperty);
                    return condition;
                }
                set
                {
                    SetValue(ConditionProperty, value);
                }
            }
    
            public string ConditionDateTime { get; set; }
    
            #endregion
    
            public LogBar()
            {
                InitializeComponent();
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
    
            /// <summary>
            /// <see cref="Condition"/> プロパティの値が変更されたとき、変更されたプロパティの値をコレクションに記録します。
            /// </summary>
            /// <param name="obj"></param>
            /// <param name="e"></param>
            private static void OnConditionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
            {
                Debug.WriteLine("OnConditionChangedが呼び出されました。");
    
                var nowTime = DateTime.Now;
    
                // オブジェクトを取得して処理する
                var control = obj as LogBar;
                if (control == null) return;
    
                // 記録しない例外
                if (e.NewValue == null || string.IsNullOrEmpty(e.NewValue.ToString())) return;
    
                // ログをコレクションに追加する
                control._conditions.Add(Tuple.Create<string, DateTime>(control.Condition, nowTime));
    
                // 時間
                control.ConditionDateTime = nowTime.ToString("yyyy/MM/dd HH:mm:ss");
                control.OnPropertyChanged("ConditionDateTime");
            }
    
            protected virtual void OnPropertyChanged(string propertyName)
            {
                RaisePropertyChanged(propertyName);
            }
    
            private void RaisePropertyChanged(string propertyName)
            {
                var e = PropertyChanged;
                if (e != null) e(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
    


    2016年1月11日 6:45

回答

  • バインド元のプロパティが変化したら、ウィンドウの所属しているメインスレッドで、Dispatcherの優先度(DispatcherPriority)がDataBindの時に、バインド元から値を取り出してバインド先に値を渡されます。

    そのため優先度の高い処理が詰まっていた場合、DataBindの処理が遅れるため、バインド元のプロパティの変化した時刻ではなく、バインド処理で取り出された時刻になり、想定しているよりもズレがでます。

    別スレッドでバインド元が変化した場合に、メインスレッドでDataBindが動いて現在の値が取り出される前に次の値が入ってしまうと、すべての値の変化を検出できなくなります。
    この場合は確実に得られるのは最後の値だけです。(最初のデータのみというのは間違いのはず)

    データバインドはこのように優先度やスレッドの影響を受けるので、全ての値の変化を正確に記録することは難しくなります。
    PropertyChangedイベントを直接捕まえるならばきちんと取れるでしょう。


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク ichiethel 2016年1月26日 0:22
    2016年1月11日 9:45

すべての返信

  • MainWindowのコンストラクタで代入処理を行っているのが問題ですね。
    バインドできている前に代入を繰り返してもバインド先に値が入らないのでCallbackもされません。

    どうしても必要なら以下のようにしてやればバインドが動きますが。

    public MainWindow()
    {
        InitializeComponent();
    
        this.DataContext = this;
    
        //優先順位の低いダミー処理を行われるのを待つことで先にバインド処理が行われるようにする
        Action a = () => { };
        Dispatcher.Invoke(a, System.Windows.Threading.DispatcherPriority.DataBind);
    
        var testCount = 10;
        for (var i = 0; i < testCount; i++)
        {
            SampleText = "Test" + i.ToString();
        }
    }

    #他の場合でもDispatcherの優先順位の問題や別スレッドでプロパティを変えたりした場合にどうするのとか考える必要がでてくるかもね。


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答の候補に設定 星 睦美 2016年1月22日 4:46
    2016年1月11日 8:03
  • gekkaさん回答ありがとうございました。

    バインドできている前に代入を繰り返してもバインド先に値が入らないのでCallbackもされません。

    ボタンを追加して、ボタンを押下したときにFor文を繰り返すと全ての変更の通知が発生しました。ありがとうございます。

    補足頂いている別スレッドでプロパティを変更したときや、Dispathcerの優先順位の問題というのはどういうことでしょうか。たしかに、別スレッドで動かしてみると今度はFor文の最初のデータしか通知されないような動きになりました。

    別スレッドの場合は、どのように考えるのでしょうか。

    2016年1月11日 8:42
  • バインド元のプロパティが変化したら、ウィンドウの所属しているメインスレッドで、Dispatcherの優先度(DispatcherPriority)がDataBindの時に、バインド元から値を取り出してバインド先に値を渡されます。

    そのため優先度の高い処理が詰まっていた場合、DataBindの処理が遅れるため、バインド元のプロパティの変化した時刻ではなく、バインド処理で取り出された時刻になり、想定しているよりもズレがでます。

    別スレッドでバインド元が変化した場合に、メインスレッドでDataBindが動いて現在の値が取り出される前に次の値が入ってしまうと、すべての値の変化を検出できなくなります。
    この場合は確実に得られるのは最後の値だけです。(最初のデータのみというのは間違いのはず)

    データバインドはこのように優先度やスレッドの影響を受けるので、全ての値の変化を正確に記録することは難しくなります。
    PropertyChangedイベントを直接捕まえるならばきちんと取れるでしょう。


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク ichiethel 2016年1月26日 0:22
    2016年1月11日 9:45
  • 回答ありがとうございます。なんとか実装できました。

    データバインドが優先度やスレッドの影響を受けるというのを知りませんでした。どこかに基本として書いてあったのでしょうか?

    PropertyChangedイベントを直接捕まえるなら大丈夫、というところが私の理解が今も半解です。
    ConditionsをObservableCollection<string>のコレクションにして追加する方法に切り替えました。コレクションで値の変化の履歴を残るようにしました。

    画面の表示は、以下よりコレクションの一番新しいデータを表示する別のプロパティを用意して、コレクションへの追加に紐づくイベントで切り替えるようにしました。今のところうまく動いています。

    >確実に得られるのは最後の値だけです。

    回答が遅くなり、申し訳ありませんでした。とても助かりました。ありがとうございました。

    2016年1月26日 0:22