WPF:プロパティがバインディングをサポートしていない場合の対処方法

はじめに



WPFは素晴らしい技術であり、そのすべての欠点にもかかわらず、私は本当に大好きです。 ただし、多くの場合、マークアップではなく、最初のコードが正常に機能するのに役立つコードを記述する必要があります。 これを避けて純粋なXAMLを作成したいと思いますが、これまでのところ、C#で記述されたさまざまなヘルパー(ヘルパークラス)なしで、単純なアプリケーションよりも難しいアプリケーションはありません。 幸いなことに、1人のヘルパーが問題のグループをすぐに解決できる一般的なケースがあります。



以下では、依存関係プロパティではない視覚要素の通常のプロパティのバインドについて説明します。 通常のWPFツールではこれができません。 さらに、 MVVMパターンと矛盾する特別なイベントにサブスクライブすることを除いて、このプロパティの変更について学習することはできません。 各プロパティのこのようなイベントは異なる場合があります。 最も一般的な例は、 PasswordBoxとそのPasswordプロパティです。 できません。



<PasswordBox Password={Binding OtherProperty} />
      
      





PasswordBoxの開発者がpasswordプロパティへのバインドを許可しなかった理由については詳しく説明しません。 ここで何ができるか考えてみましょう。



私の推論を読みたくない場合は、記事の最後に最終コードとGithubへのリンクが公開されています。



実装方法を決定する



それで! このような解決策を、通常のバインディングのように見せたいが、いくつかの追加のパラメーターが必要です。 また、双方向のバインディングができるのは素晴らしいことです。 上記を実装するには、ヘルパーに3つの入力パラメーターが必要です。

  1. 視覚要素プロパティ
  2. レポートイベントの変更
  3. データソースのバインド


たとえば、 PasswordBoxの場合、それぞれPasswordプロパティ、 PasswordChangedイベント、およびOtherPropertyのソースになります。



目的を達成するにはさまざまな方法があります。 そのような場合の標準的なメカニズム-動作について詳しく見てみましょう。



動作は、視覚要素に追加機能を追加するクラスです。 動作には、静的とBehavior <T>クラスから継承された2種類があります。 それらの違いの説明は、記事の範囲外です。 優れた機能を備えているため、2番目のオプションを選択しました。



コードを書く



System.Windows.Interactivity.dllアセンブリへのリンクを追加します。 これはExpression Blend SDKの一部であり、Visual Studioアセンブリ選択ウィンドウの[拡張機能]セクションにあります。



Behavior <T>から継承したクラスを作成します。



 public class DependecyPropertyBehavior : Behavior<DependencyObject> { }
      
      





ジェネリック型DependencyObjec tが最も一般的に選択されています。 結局のところ、 PasswordBoxだけでなく、あらゆる要素に適したユニバーサルクラスを作成しています。



操作アルゴリズムは簡単です。 プロパティからソースにバインドするには:

  1. ビジュアル要素のプロパティの変更に関するイベントにサブスクライブします。
  2. ハンドラーで、更新された値をソースに書き込みます。


バックバインディングの場合:

  1. バインディングを通じて、ソース値が変化する瞬間を決定します。
  2. 更新された値を視覚要素のプロパティに書き込みます。


上記の入力パラメーターに対して、3つのプロパティを作成します。



 public string Property { get; set; } public string UpdateEvent { get; set; } public static readonly DependencyProperty BindingProperty = DependencyProperty.RegisterAttached( "Binding", typeof(object), typeof(DependecyPropertyBehavior), new FrameworkPropertyMetadata { BindsTwoWayByDefault = true } ); public object Binding { get { return GetValue(BindingProperty); } set { SetValue(BindingProperty, value); } }
      
      







この場合、 PropertyプロパティUpdateEventプロパティは正常です。これで十分です。 反対に、 Bindingプロパティは、データソースが接続される場所であるため、依存関係プロパティである必要があります。



すべての入力ができたので、再定義されたOnAttached()メソッドで処理を開始しましょう。 ビヘイビアを視覚要素にアタッチするときに呼び出されます。 後者は、 AssociatedObjectクラスのプロパティを介してアクセスできます。 対照的に、 OnDetaching()は切断されると呼び出されます。



視覚要素のプロパティとリフレクションによるイベントを操作するオブジェクトが必要です。 以下は、ビジュアル要素のプロパティの変更を通知するイベントの受信とサブスクリプションを示しています。



以下の例では、注意を散らかさないように、さまざまなヌルチェックを省略しています 。 クラスの最終バージョンには存在します。



 private Delegate _handler; private EventInfo _eventInfo; private PropertyInfo _propertyInfo; protected override void OnAttached() { Type elementType = AssociatedObject.GetType(); //     _propertyInfo = elementType.GetProperty(Property, BindingFlags.Instance | BindingFlags.Public); //  ,      _eventInfo = elementType.GetEvent(UpdateEvent); //       _handler = CreateDelegateForEvent(_eventInfo, EventFired); //  _eventInfo.AddEventHandler(AssociatedObject, _handler); } protected override void OnDetaching() { //  _eventInfo.RemoveEventHandler(AssociatedObject, _handler); }
      
      





上記のコードにはCreateDelegateForEvent()メソッドがあります。 実行時に指定されたイベントのデリゲートオブジェクトをコンパイルします。 結局のところ、事前にイベントハンドラーの署名がわかりません。 コンパイル時に、デリゲートはアクションメソッド(この例ではEventFired())を呼び出します。 その中で、データソースの値を更新するために必要なアクションを実行します。



 private static Delegate CreateDelegateForEvent(EventInfo eventInfo, Action action) { ParameterExpression[] parameters = eventInfo .EventHandlerType .GetMethod("Invoke") .GetParameters() .Select(parameter => Expression.Parameter(parameter.ParameterType)) .ToArray(); return Expression.Lambda( eventInfo.EventHandlerType, Expression.Call(Expression.Constant(action), "Invoke", Type.EmptyTypes), parameters ) .Compile(); }
      
      





この操作は非常にリソースを消費しますが、動作が接続されたときに一度だけ実行されます。 イベントはRoutedEventにしかできないと想定して柔軟性を犠牲にすることで最適化できます。 次に、高価なコンパイルの代わりに、シグネチャをRoutedEventHandlerと互換性のあるものに変更した後、 EventFired()ハンドラーを示すイベントをサブスクライブするだけで十分です。 しかし、ここに元のバージョンを残しましょう。 時期尚早の最適化は悪です。



EventFired()メソッドは非常に単純で、データソースに新しい値を書き込みます。



 private void EventFired() { Binding = _propertyInfo.GetValue(AssociatedObject, null); }
      
      





残っている唯一の小さなことは、データソースが変更されたときに視覚要素のプロパティの値を変更することです。 これには、クラスの依存関係プロパティへの変更を報告するオーバーライド可能なOnPropertyChanged()メソッドが適しています。 データソースが変更されるとBindingプロパティも変更されるため、新しい値を追跡するだけです。



 protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { if (e.Property.Name != "Binding") return; if (_propertyInfo.CanWrite) _propertyInfo.SetValue(AssociatedObject, e.NewValue, null); base.OnPropertyChanged(e); }
      
      





すべてがうまくいくようです。 視覚要素のプロパティに新しい値を設定し、... StackOverflowExceptionを取得します。



問題は、プロパティが変更されると、通知イベントが自動的にトリガーされ、サブスクライブされることです。 ソース値はイベントで変更され、ソースが変更されると、 Bindingプロパティが変更され、 OnPropertyChanged()メソッドに戻ります。 再帰。



最も簡単な解決策は、古いプロパティ値と新しいプロパティ値の比較を追加することです。



 protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { if (e.Property.Name != "Binding") return; object oldValue = _propertyInfo.GetValue(AssociatedObject, null); if (oldValue.Equals(e.NewValue)) return; if (_propertyInfo.CanWrite) _propertyInfo.SetValue(AssociatedObject, e.NewValue, null); base.OnPropertyChanged(e); }
      
      





ここでは、型のEquals()が実装されるべきであり、常にfalseを返すわけではないという前提を立てています



ヘルパーの準備ができました!



結果



使用例:



 <StackPanel xmlns:local="clr-namespace:DependecyPropertyBehaviorNamesapce" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" > <PasswordBox> <i:Interaction.Behaviors> <local:DependecyPropertyBehavior UpdateEvent="PasswordChanged" Property="Password" Binding="{Binding Text, ElementName=TestTextBox}" /> </i:Interaction.Behaviors> </PasswordBox> <TextBox x:Name="TestTextBox" /> </StackPanel>
      
      





この場合、 TextBoxPasswordBoxは同期して値を変更します。







おわりに



視覚要素のプロパティのバインド作業を達成しましたが、当初はサポートしていませんでした。 バインディングは両方の方法で機能し、プロパティ名とイベントの違いを心配することなく、任意の要素に動作クラスを使用できます。



最初の記事であるテキストの不正確さを事前に謝罪します。

約束どおり、最終コードは次のとおりです。



クイックリファレンスのnullチェックはありません
 using System; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Windows; using System.Windows.Interactivity; using Expression = System.Linq.Expressions.Expression; namespace DependecyPropertyBehaviorNamesapce { public class DependecyPropertyBehavior : Behavior<DependencyObject> { private Delegate _handler; private EventInfo _eventInfo; private PropertyInfo _propertyInfo; public static readonly DependencyProperty BindingProperty = DependencyProperty.RegisterAttached( "Binding", typeof(object), typeof(DependecyPropertyBehavior), new FrameworkPropertyMetadata { BindsTwoWayByDefault = true } ); public object Binding { get { return GetValue(BindingProperty); } set { SetValue(BindingProperty, value); } } public string Property { get; set; } public string UpdateEvent { get; set; } protected override void OnAttached() { Type elementType = AssociatedObject.GetType(); _propertyInfo = elementType.GetProperty(Property, BindingFlags.Instance | BindingFlags.Public); _eventInfo = elementType.GetEvent(UpdateEvent); _handler = CreateDelegateForEvent(_eventInfo, EventFired); _eventInfo.AddEventHandler(AssociatedObject, _handler); } protected override void OnDetaching() { _eventInfo.RemoveEventHandler(AssociatedObject, _handler); } protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { if (e.Property.Name != "Binding") return; object oldValue = _propertyInfo.GetValue(AssociatedObject, null); if (oldValue.Equals(e.NewValue)) return; if (_propertyInfo.CanWrite) _propertyInfo.SetValue(AssociatedObject, e.NewValue, null); base.OnPropertyChanged(e); } private static Delegate CreateDelegateForEvent(EventInfo eventInfo, Action action) { ParameterExpression[] parameters = eventInfo .EventHandlerType .GetMethod("Invoke") .GetParameters() .Select(parameter => Expression.Parameter(parameter.ParameterType)) .ToArray(); return Expression.Lambda( eventInfo.EventHandlerType, Expression.Call(Expression.Constant(action), "Invoke", Type.EmptyTypes), parameters ) .Compile(); } private void EventFired() { Binding = _propertyInfo.GetValue(AssociatedObject, null); } } }
      
      







すべてのチェックを含む最終バージョン
 using System; using System.Diagnostics; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Windows; using System.Windows.Interactivity; using Expression = System.Linq.Expressions.Expression; namespace DependecyPropertyBehaviorNamesapce { public class DependecyPropertyBehavior : Behavior<DependencyObject> { private Delegate _handler; private EventInfo _eventInfo; private PropertyInfo _propertyInfo; public static readonly DependencyProperty BindingProperty = DependencyProperty.RegisterAttached( "Binding", typeof(object), typeof(DependecyPropertyBehavior), new FrameworkPropertyMetadata { BindsTwoWayByDefault = true } ); public object Binding { get { return GetValue(BindingProperty); } set { SetValue(BindingProperty, value); } } public string Property { get; set; } public string UpdateEvent { get; set; } protected override void OnAttached() { Type elementType = AssociatedObject.GetType(); // Getting property. if (Property == null) { PresentationTraceSources.DependencyPropertySource.TraceData( TraceEventType.Error, 1, "Target property not defined." ); return; } _propertyInfo = elementType.GetProperty(Property, BindingFlags.Instance | BindingFlags.Public); if (_propertyInfo == null) { PresentationTraceSources.DependencyPropertySource.TraceData( TraceEventType.Error, 2, string.Format("Property \"{0}\" not found.", Property) ); return; } // Getting event. if (UpdateEvent == null) return; _eventInfo = elementType.GetEvent(UpdateEvent); if (_eventInfo == null) { PresentationTraceSources.MarkupSource.TraceData( TraceEventType.Error, 3, string.Format("Event \"{0}\" not found.", UpdateEvent) ); return; } _handler = CreateDelegateForEvent(_eventInfo, EventFired); _eventInfo.AddEventHandler(AssociatedObject, _handler); } protected override void OnDetaching() { if (_eventInfo == null) return; if (_handler == null) return; _eventInfo.RemoveEventHandler(AssociatedObject, _handler); } protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) { if (e.Property.Name != "Binding") return; if (AssociatedObject == null) return; if (_propertyInfo == null) return; object oldValue = _propertyInfo.GetValue(AssociatedObject, null); if (oldValue.Equals(e.NewValue)) return; if (_propertyInfo.CanWrite) _propertyInfo.SetValue(AssociatedObject, e.NewValue, null); base.OnPropertyChanged(e); } private static Delegate CreateDelegateForEvent(EventInfo eventInfo, Action action) { ParameterExpression[] parameters = eventInfo .EventHandlerType .GetMethod("Invoke") .GetParameters() .Select(parameter => Expression.Parameter(parameter.ParameterType)) .ToArray(); return Expression.Lambda( eventInfo.EventHandlerType, Expression.Call(Expression.Constant(action), "Invoke", Type.EmptyTypes), parameters ) .Compile(); } private void EventFired() { if (AssociatedObject == null) return; if (_propertyInfo == null) return; Binding = _propertyInfo.GetValue(AssociatedObject, null); } } }
      
      







Github



All Articles