【WPF】変更通知をサポートしないCLRプロパティの変更通知

  • 2009.02.12 Thursday
  • 02:23
ReflectPropertyDescriptor

Source and Project(TextBox)
Source and Project(ReflectPropertyDescriptorを再現)

さて思いっきりタイトルが矛盾していますが、今回は「変更通知をサポートしないCLRプロパティの変更通知」に関しての投稿をします。いきなりですがクラスを紹介します。

 public class Demo
 {
  public string Value { get; set; }
 }

このDemoクラスは変更通知をサポートしていません。メンバはValueと言う名前のCLRプロパティが一つです。このオブジェクトを使用して下記のXAMLを組んでみました。

<Window x:Class="Art55.TextBoxDemo20090211_001.Window1"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 xmlns:local="clr-namespace:Art55.TextBoxDemo20090211_001"
 Title="Window1" Height="300" Width="300">
 <Window.Resources>
  <local:Demo x:Key="source" />
  <local:DebugConverter x:Key="converter" />
 </Window.Resources>
 <Grid>
  <Grid.RowDefinitions>
   <RowDefinition Height="*" />
   <RowDefinition Height="*" />
  </Grid.RowDefinitions>
  <TextBox Text="{Binding Path=Value, Source={StaticResource source}, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource converter}}"
     x:Name="textBox1"
     Grid.Row="0"
     HorizontalAlignment="Center"
     VerticalAlignment="Center"
     Width="200"></TextBox>
  <TextBox Text="{Binding Path=Value, Source={StaticResource source}, UpdateSourceTrigger=PropertyChanged, Converter={StaticResource converter}}"
     x:Name="textBox2"
     Grid.Row="1"
     HorizontalAlignment="Center"
     VerticalAlignment="Center"
     Width="200"></TextBox>
 </Grid>
</Window>

Windowの上下にTextBoxを配置します。TextBoxのText依存関係プロパティはDemoオブジェクトのデータソースとしてValueにバインドしています。TextBox.Textの変更通知のタイミングはTextBox.Textが変更されたタイミングで発砲するようにUpdateSourceTrigger=PropertyChangedを指定しています。IValueConverterを挟んでいますが、これはデバッグ用です。実装は下記の通りです。

 public class DebugConverter : IValueConverter
 {
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
   return value;
  }

  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
   return value;
  }
 }

特に何もせずにそのまま値を返します。使用目的はブレイクポイントを設定して、デバッグする事が目的です。

では、実行してみます。起動しました。

ReflectPropertyDescriptor


文字を入力します。

ReflectPropertyDescriptor


なんと下の行のTextBoxまで値が変わっています!さて、これを不思議と思うか、思わないかは意見が分かれるところかもしれませんが、私はこれを不思議と思っていました。過去形で書いたのは半年前くらいに見つけて放置していたからです。理由が分からなくても、実際にこのような場面では必ずINotifyChangedPropertyを実装していたからです。ただ半年間も夜も眠れない状況なのは、精神衛生上良くないと思い今回調べて見ることにしました。(一部うそをいいました)

さて、ではどうやって調べるかですが、今回はスタックトレースで犯人さがしから入ります。

まず、起動させます。それからDebugConverterのConvertメソッドにブレイクポイントを設置します。そしてTextBoxに適当な文字を入力します。すると下記の画面になります。

ReflectPropertyDescriptor

ReflectPropertyDescriptor

Convertにブレイクポイントを置いたのは、Bindingオブジェクトのデータソースからの変更通知の結果、別のTextBox.Textプロパティに値の書き換えが発生するタイミングをとらえたかったからです。一応とらえることができたので呼び出し履歴を見てみます。

そのままアップすると

Art55.TextBoxDemo20090211_001.exe!Art55.TextBoxDemo20090211_001.DebugConverter.Convert(object value = "inpu", System.Type targetType = {Name = "String" FullName = "System.String"}, object parameter = null, System.Globalization.CultureInfo culture = {en-US}) 行 11 C#
PresentationFramework.dll!System.Windows.Data.BindingExpression.TransferValue(object newValue, bool isASubPropertyChange = false) + 0x22a バイト
PresentationFramework.dll!System.Windows.Data.BindingExpression.ScheduleTransfer(bool isASubPropertyChange) + 0x3b バイト
PresentationFramework.dll!MS.Internal.Data.ClrBindingWorker.NewValueAvailable(bool dependencySourcesChanged, bool initialValue, bool isASubPropertyChange) + 0x4f バイト
PresentationFramework.dll!MS.Internal.Data.PropertyPathWorker.UpdateSourceValueState(int k, System.ComponentModel.ICollectionView collectionView, object newValue, bool isASubPropertyChange) + 0x158 バイト
PresentationFramework.dll!MS.Internal.Data.ClrBindingWorker.OnSourcePropertyChanged(object o, string propName) + 0x7f バイト
PresentationFramework.dll!MS.Internal.Data.PropertyPathWorker.System.Windows.IWeakEventListener.ReceiveWeakEvent(System.Type managerType, object sender, System.EventArgs e) + 0x127 バイト
WindowsBase.dll!System.Windows.WeakEventManager.DeliverEventToList(object sender = {Art55.TextBoxDemo20090211_001.Demo}, System.EventArgs args = {MS.Internal.Data.ValueChangedEventArgs}, System.Windows.WeakEventManager.ListenerList list = {System.Windows.WeakEventManager.ListenerList}) + 0x59 バイト
PresentationFramework.dll!MS.Internal.Data.ValueChangedEventManager.ValueChangedRecord.OnValueChanged(object sender, System.EventArgs e) + 0x81 バイト
System.dll!System.ComponentModel.PropertyDescriptor.OnValueChanged(object component, System.EventArgs e) + 0x51 バイト
System.dll!System.ComponentModel.ReflectPropertyDescriptor.OnValueChanged(object component, System.EventArgs e) + 0x65 バイト
System.dll!System.ComponentModel.ReflectPropertyDescriptor.SetValue(object component = {Art55.TextBoxDemo20090211_001.Demo}, object value = "inpu") + 0x140 バイト
PresentationFramework.dll!MS.Internal.Data.PropertyPathWorker.SetValue(object item, object value) + 0x186 バイト
PresentationFramework.dll!MS.Internal.Data.ClrBindingWorker.UpdateValue(object value) + 0x70 バイト
PresentationFramework.dll!System.Windows.Data.BindingExpression.UpdateSource(object value = "inpu") + 0x80 バイト
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.UpdateValue() + 0x62 バイト
PresentationFramework.dll!System.Windows.Data.BindingExpression.Update(bool synchronous) + 0x60 バイト
PresentationFramework.dll!System.Windows.Data.BindingExpressionBase.Dirty() + 0x33 バイト
PresentationFramework.dll!System.Windows.Data.BindingExpression.SetValue(System.Windows.DependencyObject d, System.Windows.DependencyProperty dp, object value) + 0x27 バイト
WindowsBase.dll!System.Windows.DependencyObject.SetValueCommon(System.Windows.DependencyProperty dp = {Text}, object value = "inpu", System.Windows.PropertyMetadata metadata = {System.Windows.FrameworkPropertyMetadata}, bool coerceWithDeferredReference = true, System.Windows.OperationType operationType = Unknown, bool isInternal) + 0x397 バイト
WindowsBase.dll!System.Windows.DependencyObject.SetDeferredValue(System.Windows.DependencyProperty dp, System.Windows.DeferredReference deferredReference) + 0x27 バイト
PresentationFramework.dll!System.Windows.Controls.TextBox.OnTextContainerChanged(object sender = {System.Windows.Documents.TextContainer}, System.Windows.Documents.TextContainerChangedEventArgs e = {System.Windows.Documents.TextContainerChangedEventArgs}) + 0xaf バイト


上記は下から読み上げる必要があります。

簡単に説明していくと、

1.上のTextBox.Textの値が書き換えられ、BindingExpressionを通じて
  BindingオブジェクトのUpdateSourceが呼び出されます。
2.ReflectPropertyDescriptorというオブジェクトのSetValueメソッドを
  使用してDemoオブジェクトのValueの値を書き換えます。
3.ValueChangedEventManagerに設定されているリスナーがこの変更イベントを
  とらえ、登録されているリスナー(下のBindingオブジェクト)へ通知します。
4.下のBindingオブジェクトはBindingExpressionを使用して下の
  TextBox.Textの値を書き換えます。


という現象が発生しているようです。

さて、ReflectPropertyDescriptorとValueChangedEventManagerという見たことも聞いたこともないクラスが出てきます。これは.NET Frameworkで用意されているinternalなクラスなので、非公開なクラスです。でも動きを調べたいので今回はリフレクションを使用して実験してみました。かなり無茶をやってます。

書いたコードはこんな感じ。

using System;
using System.Reflection;
using System.Windows;

namespace Art55.ValueChangedEventManagerDemo20090211_001
{
 class Program
 {
  [STAThread]
  static void Main(string[] args)
  {
   Demo source = new Demo();

   object reflectPropertyDescriptor = GetReflectPropertyDescriptor();
   var listener = new DummyWeakEventListener();
   AddListener(source, listener, reflectPropertyDescriptor);

   object valueChangedEeventManager = GetValueChangedEventManager();
   StartListening(valueChangedEeventManager, source);

   SetValue(reflectPropertyDescriptor, source, "New Value");
  }

  private static void SetValue(object reflectPropertyDescriptor, object source, object value)
  {
   Type type = reflectPropertyDescriptor.GetType();
   MethodInfo methodInfo = type.GetMethod("SetValue");
   methodInfo.Invoke(reflectPropertyDescriptor, new[] { source, value });
  }

  private static void AddListener(object source, DummyWeakEventListener listener, object reflectPropertyDescriptor)
  {
   Assembly pfAssembly = Assembly.GetAssembly(typeof(FrameworkElement));
   Type type = pfAssembly.GetType("MS.Internal.Data.ValueChangedEventManager");
   MethodInfo methodInfo = type.GetMethod("AddListener");
   object[] parameter = new[] { source, listener, reflectPropertyDescriptor };
   methodInfo.Invoke(null, parameter);
  }

  private static object GetValueChangedEventManager()
  {
   Assembly pfAssembly = Assembly.GetAssembly(typeof(FrameworkElement));
   Type type = pfAssembly.GetType("MS.Internal.Data.ValueChangedEventManager");
   PropertyInfo propertyInfo = type.GetProperty("CurrentManager", BindingFlags.NonPublic | BindingFlags.Static);
   MethodInfo methodInfo = propertyInfo.GetGetMethod(true);
   return methodInfo.Invoke(null, null);
  }

  private static void StartListening(object valueChangedEeventManager, object source)
  {
   Type managerType = valueChangedEeventManager.GetType();
   MethodInfo methodInfo = managerType.GetMethod("StartListening", BindingFlags.NonPublic | BindingFlags.Instance);
   methodInfo.Invoke(valueChangedEeventManager, new [] {source});
  }

  private static object GetReflectPropertyDescriptor()
  {
   Assembly systemAssembly = Assembly.GetAssembly(typeof(System.ComponentModel.Container));
   Type reflectPropertyDesctiptor = systemAssembly.GetType("System.ComponentModel.ReflectPropertyDescriptor");
   ConstructorInfo constructorInfo = reflectPropertyDesctiptor.GetConstructor(new[]
                       {
                        typeof (Type), typeof (string), typeof (Type),
                        typeof (PropertyInfo), typeof (MethodInfo),
                        typeof (MethodInfo), typeof (Attribute[])
                       });
   Type demoType = typeof(Demo);
   PropertyInfo demoValuePorpertyInfo = demoType.GetProperty("Value");
   return constructorInfo.Invoke(new object[]
                   {
                    typeof (Demo), "Value", typeof (string), demoValuePorpertyInfo,
                    demoValuePorpertyInfo.GetGetMethod(), demoValuePorpertyInfo.GetSetMethod(), null
                   });
  }
 }

 public class DummyWeakEventListener : IWeakEventListener
 {
  public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
  {
   Console.WriteLine(sender.ToString());
   return true;
  }
 }

 public class Demo
 {
  public string Value { get; set; }
 }
}


さて、コードで何をやっているか説明します。Demoクラスは前と同じです。DummyWeakEventListenerはIWeakEventListenerを継承するクラスです。見立てBindingオブジェクトを想定しています。

メインメソッドを見ていくと

GetReflectPropertyDescriptorメソッドでReflectPropertyDescriptorインスタンスを入手します。AddListenerメソッドでデータソースとリスナーとリフレクションを利用したPropertyDescriptorを関連づけます。リスナーはStartListeningを呼び出さないと開始しないので、StartListeningメソッドを呼び出してリッスンを開始します。最後にリフレクションを利用したPropertyDescriptor経由でDemoインスタンスのValueプロパティの値を書き換えます。それがSetValueメソッドの部分です。

これを実行すると

ReflectPropertyDescriptor

と、まあ、EventManagerが変更通知を受け取る事がわかります。EventManager自身はコンソールに出力するプロセスしか記述していないのでこれ以外の事は今回はしません。

ではリスナーを2つに増やしてみます。

  [STAThread]
  static void Main(string[] args)
  {
   Demo source = new Demo();

   object reflectPropertyDescriptor = GetReflectPropertyDescriptor();

   AddListener(source, new DummyWeakEventListener(), reflectPropertyDescriptor);
   AddListener(source, new DummyWeakEventListener(), reflectPropertyDescriptor);

   object valueChangedEeventManager = GetValueChangedEventManager();
   StartListening(valueChangedEeventManager, source);

   SetValue(reflectPropertyDescriptor, source, "New Value");
  }


実行すると2行出力されます。つまり2つのリスナーが反応している事がわかります。

ReflectPropertyDescriptor

では元の2つTextBoxの話にもどちます。まず、二つのTextBoxにはそれぞれBindingオブジェクトが用意されていました。つまりEvnetManagerに2つのリスナーが登録された事になります。片方のTextBox.Textの変更により内部ではReflectPropertyDescriptor.SetValueメソッドが呼び出され、DemoオブジェクトのValueの値が書き換えられます。そのときにEventManagerに登録されたリスナーが反応します。片方のリスナーは自分が呼び出した結果なので無視されるのか(・・・ここは実装がよくわかりませんが)、片方のリスナーはその変更を受け取りTextBox,Textの値を書き換える。

という事がわかりました。おそらくはINotifyPropertyChangedを実装しているクラスのオブジェクトがデータソースになっている場合は、全く別の仕組みが動くものと考えられます。そして、今回のようなリフレクションが使用されないため、パフォーマンス的にも良い結果をもたらすと予想されます。
-------------------------------
修正)
未確認なので斜線を引かせていただきました。
-------------------------------

今回はここまでで終わります。

まとめ。

1.CLRオブジェクトをBindingオブジェクトのデータソースとして使用する場合は、
  なるべく変更通知機能を実装する方がパフォーマンスが向上する。

------------------------------------
修正)バインディングターゲットからバインディングソースへの値変更の仕組みが変更通知をサポートしている場合としていない場合で違いがあるかどうか未確認なので、斜線を引きました。
------------------------------------

※ReflectPropertyDescriptorやValueChangedEeventManagerはインターナルなので内部の仕様が変わることもあるので言及しません。

Source and Project(TextBox)
Source and Project(ReflectPropertyDescriptorを再現)
コメント
管理者の承認待ちコメントです。
  • -
  • 2018/06/09 2:08 PM
コメントする








    
この記事のトラックバックURL
トラックバック

calendar

S M T W T F S
 123456
78910111213
14151617181920
21222324252627
282930    
<< April 2019 >>

あわせて読みたい

あわせて読みたいブログパーツ

selected entries

categories

archives

recent comment

  • 【キーボード】6年前のRealForceを復活させることはできる!?その3
    art55 (05/22)
  • 【キーボード】6年前のRealForceを復活させることはできる!?その3
    分解大好き (05/18)
  • 【.NET Framework 4.5】 IListがIReadOnlyListを継承してない理由。
    art55 (02/04)
  • 【.NET Framework 4.5】 IListがIReadOnlyListを継承してない理由。
    Gen (02/04)
  • 【キーボード】RealForce が壊れて帰ってきた。
    art55 (04/29)
  • 【.NET Framework 4.5】 IListがIReadOnlyListを継承してない理由。
    art55 (02/23)
  • 【.NET Framework 4.5】 IListがIReadOnlyListを継承してない理由。
    かるあ (02/22)
  • 【C#】Dictionaryの実装・データ構造・アルゴリズムを観察する。
    art55 (01/16)
  • 【C#】Dictionaryの実装・データ構造・アルゴリズムを観察する。
    karuakun (01/16)
  • 【NetOffice】【Excel】死なないExcelプロセスをKillする。
    art55 (12/05)

recent trackback

recommend

recommend

recommend

C#プログラマのための.NETアプリケーション最適化技法 (Programmer's SELECTION)
C#プログラマのための.NETアプリケーション最適化技法 (Programmer's SELECTION) (JUGEMレビュー »)
Sasha Goldshtein,Dima Zurbalev,Ido Flatow,サシャ・ゴルドシュタイン,ディマ・ズルバレフ,イド・フラトー

recommend

ろんりと集合
ろんりと集合 (JUGEMレビュー »)
中内 伸光
とてもわかりやすいです。

recommend

recommend

シャノン・ノイマン・ディジタル世界
シャノン・ノイマン・ディジタル世界 (JUGEMレビュー »)
市川 忠男
4章がリレーショナルデータベースな内容になってます。ページ数があまりありませんが、ポイントがものすごく的確にまとまっていて、感動します。

recommend

recommend

東プレ Realforce91UBK-S 静音キーボード 静電容量無接点方式 変荷重 ブラック NG01BS
東プレ Realforce91UBK-S 静音キーボード 静電容量無接点方式 変荷重 ブラック NG01BS (JUGEMレビュー »)

テンキーレス、静音のRealForce91UBK-S。スコスコ感がたまらなく気持ちいいです。家と会社で2台持ってます。

recommend

recommend

プログラミング.NET Framework 第4版 (プログラミングシリーズ)
プログラミング.NET Framework 第4版 (プログラミングシリーズ) (JUGEMレビュー »)
Jeffrey Richter
発売予定美 2013年10月10日。.NET Frameworkとお付き合いする人のバイブルですね。

recommend

recommend

キャット・シッターの君に。
キャット・シッターの君に。 (JUGEMレビュー »)
喜多嶋 隆
私のイラストレータデビュー本です。

recommend

Essential .NET ― 共通言語ランタイムの本質
Essential .NET ― 共通言語ランタイムの本質 (JUGEMレビュー »)
ドン・ボックス,クリス・セルズ,Don Box,Chris Sells,吉松 史彰

links

profile

search this site.

others

mobile

qrcode

powered

無料ブログ作成サービス JUGEM