2011-10-11 2 views
15

可能性の重複:私はNotifyPropertyChanged("PropertyName")の形で魔法の文字列を多用苦しんでいる大規模なチームのアプリケーションに取り組んでいます
typesafe NotifyPropertyChanged using linq expressions実装NotifyPropertyChanged

、 - マイクロソフトと相談する際の標準実装。また、数百もの格納された計算プロパティを持つ計算モジュール用のオブジェクトモデルで作業する、誤って命名された多数のプロパティ(UIにバインドされている)が多数あります。

私のチームは、プロパティ名の変更に関連した多くのバグを経験して、不正なマジック文字列とバインディングを破棄しています。私は、マジック文字列を使わずにプロパティ変更通知を実装することで、この問題を解決したいと考えています。 .Net 3.5で見つかった唯一の解決策はラムダ式です。 (例:Implementing INotifyPropertyChanged - does a better way exist?

私のマネージャーは、名前は

protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression) 
{ 
    MemberExpression body = selectorExpression.Body as MemberExpression; 
    if (body == null) throw new ArgumentException("The body must be a member expression"); 
    OnPropertyChanged(body.Member.Name); 
} 

から抽出された

set { ... OnPropertyChanged(() => PropertyName); } 

set { ... OnPropertyChanged("PropertyName"); } 

からの切り替えのパフォーマンスコストについて非常に心配していますスプレッドシートのようなアプリケーションを考えてみましょう。このアプリケーションでは、およそ100の値がリアルタイムでUI上で再計算され、更新されます。この変更を非常に高価にすると、UIの応答性に影響しますか?私はこの変更のテストを正当化することすらできません。なぜなら、さまざまなプロジェクトやクラスでプロパティセッターを更新するのに約2日間かかるからです。

+0

私はこれを反映しています。私のブログ記事はこちらをご覧ください。 [http://tsells.wordpress.com/2011/02/08/using-reflection-with-wpf-and-the-inotifypropertychanged-interface/](http://tsells.wordpress.com/2011/02/08)/use-reflection-with-wpf-and-inotifypropertychanged-interface /)投稿の最後に掲載されているパフォーマンスノートに注意してください。 – tsells

+0

良い記事ですが、パフォーマンスの時間に絶対的な違いがありますが、それは有用ではありません。私は、パフォーマンス時間のパーセンテージの違いにもっと興味を持っています。 200ミリ秒から300ミリ秒、そして0.01ミリ秒から100.01ミリ秒の間には大きな違いがあります。同じ絶対差、異なるパーセント差。 – Alain

+0

彼は10,000のプロパティ変更通知で約1/4秒の差があると述べました。私はそれが気にする十分大きな違いだとは思っていません。もしあなたが本当に一度に10kのプロパティを更新しているなら、私は真剣にデザインを再考します:) – Rachel

答えて

22

私は、ラムダ式への切り替えの影響を確立するためNotifyPropertyChangedの徹底的なテストを行いました。

ここにいた私のテスト結果:

enter image description here

あなたが見ることができるようには、ラムダ式を使用すると、プレーンハードコードされた文字列プロパティの変更の実装よりも約5倍遅くなりますが、ユーザーは心配してはいけませんなぜなら、それでもそれほど特別な作業コンピュータでは毎秒十万回のプロパティ変更をポンピングできるからです。このように、文字列をハードコードする必要がなくなり、すべてのビジネスを世話するワン・ライン・セッターが私にとってパフォーマンス・コストをはるかに上回ることから得られる利点が得られます。

試験1プロパティが実際に変化したことを確認してくださいと、標準的なセッターの実装を使用する:

public UInt64 TestValue1 
    { 
     get { return testValue1; } 
     set 
     { 
      if (value != testValue1) 
      { 
       testValue1 = value; 
       InvokePropertyChanged("TestValue1"); 
      } 
     } 
    } 

試験2イベントを可能にする機能を加えて、非常に類似していました古い値と新しい値を追跡します。この機能は私の新しいベースセッターメソッドで暗黙的になる予定だったので、私はその機能によるものであったどのくらいの新しいオーバーヘッドの見たかった:ゴムが道路を満たしている

public UInt64 TestValue2 
    { 
     get { return testValue2; } 
     set 
     { 
      if (value != testValue2) 
      { 
       UInt64 temp = testValue2; 
       testValue2 = value; 
       InvokePropertyChanged("TestValue2", temp, testValue2); 
      } 
     } 
    } 

テスト3であったが、私BindingObjectBaで

public UInt64 TestValue3 
    { 
     get { return testValue3; } 
     set { SetNotifyingProperty(() => TestValue3, ref testValue3, value); } 
    } 

実装

:と私は1つのライン内のすべての観測可能なプロパティのアクションを実行するため、この新しい美しい構文を披露してもらいますすべてのViewModelが継承するseクラスは、新しい機能を実現する実装にあります。私は明確なので、関数の肉をエラー処理をされて取り除かました:

protected void SetNotifyingProperty<T>(Expression<Func<T>> expression, ref T field, T value) 
{ 
    if (field == null || !field.Equals(value)) 
    { 
     T oldValue = field; 
     field = value; 
     OnPropertyChanged(this, new PropertyChangedExtendedEventArgs<T>(GetPropertyName(expression), oldValue, value)); 
    } 
} 
protected string GetPropertyName<T>(Expression<Func<T>> expression) 
{ 
    MemberExpression memberExpression = (MemberExpression)expression.Body; 
    return memberExpression.Member.Name; 
} 

すべての3つの方法がまだ標準であるOnPropertyChangedをルーチン、で会う:

public virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e) 
{ 
    PropertyChangedEventHandler handler = PropertyChanged; 
    if (handler != null) 
     handler(sender, e); 
} 

ボーナス

誰かが興味があれば、PropertyChangedExtendedEventArgsは標準のPropertyChangedEventArgsを拡張するために出てきたものなので、拡張のインスタンスは常にベースの代わりになります。これは、プロパティがSetNotifyingPropertyを使用して変更されたときに古い値の知識を活用し、この情報をハンドラが利用できるようにします。

public class PropertyChangedExtendedEventArgs<T> : PropertyChangedEventArgs 
{ 
    public virtual T OldValue { get; private set; } 
    public virtual T NewValue { get; private set; } 

    public PropertyChangedExtendedEventArgs(string propertyName, T oldValue, T newValue) 
     : base(propertyName) 
    { 
     OldValue = oldValue; 
     NewValue = newValue; 
    } 
} 
+0

興味深いことに、(2013年)以前のマシンで同様のテストを実行したところ、パフォーマンスの差は何も測定できませんでした(.NET 3.5 SP1、Win7 x64、Core i7) – ChrisWue

+0

@Alain - 新しい[CallerMemberName]属性 - SetNotifyingProperty(記憶域、値); – Axarydax

+0

そうですね、これはMSが最新の.NETバージョンでプロパティ名を取得するより効率的な方法に取り組んでいる一般的な十分なパターンだと思います。私はバージョン3.5または4.0の場合と同じくらい良いことを提案します。 – Alain

3

個人的に私はこの理由でMicrosoft PRISMのNotificationObjectを使用したいと思っています。私はMicrosoftによって作成されているため、コードが合理的に最適化されていると思います。

これは、 "Magic Strings"を保持することに加えて、既存のコードを壊さないように、RaisePropertyChanged(() => this.Value);などのコードを使用することを可能にします。私は反射で自分のコードを見れば

、その実装は

public class ViewModelBase : INotifyPropertyChanged 
{ 
    // Fields 
    private PropertyChangedEventHandler propertyChanged; 

    // Events 
    public event PropertyChangedEventHandler PropertyChanged 
    { 
     add 
     { 
      PropertyChangedEventHandler handler2; 
      PropertyChangedEventHandler propertyChanged = this.propertyChanged; 
      do 
      { 
       handler2 = propertyChanged; 
       PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler)Delegate.Combine(handler2, value); 
       propertyChanged = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this.propertyChanged, handler3, handler2); 
      } 
      while (propertyChanged != handler2); 
     } 
     remove 
     { 
      PropertyChangedEventHandler handler2; 
      PropertyChangedEventHandler propertyChanged = this.propertyChanged; 
      do 
      { 
       handler2 = propertyChanged; 
       PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler)Delegate.Remove(handler2, value); 
       propertyChanged = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this.propertyChanged, handler3, handler2); 
      } 
      while (propertyChanged != handler2); 
     } 
    } 

    protected void RaisePropertyChanged(params string[] propertyNames) 
    { 
     if (propertyNames == null) 
     { 
      throw new ArgumentNullException("propertyNames"); 
     } 
     foreach (string str in propertyNames) 
     { 
      this.RaisePropertyChanged(str); 
     } 
    } 

    protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression) 
    { 
     string propertyName = PropertySupport.ExtractPropertyName<T>(propertyExpression); 
     this.RaisePropertyChanged(propertyName); 
    } 

    protected virtual void RaisePropertyChanged(string propertyName) 
    { 
     PropertyChangedEventHandler propertyChanged = this.propertyChanged; 
     if (propertyChanged != null) 
     { 
      propertyChanged(this, new PropertyChangedEventArgs(propertyName)); 
     } 
    } 
} 

public static class PropertySupport 
{ 
    // Methods 
    public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression) 
    { 
     if (propertyExpression == null) 
     { 
      throw new ArgumentNullException("propertyExpression"); 
     } 
     MemberExpression body = propertyExpression.Body as MemberExpression; 
     if (body == null) 
     { 
      throw new ArgumentException("propertyExpression"); 
     } 
     PropertyInfo member = body.Member as PropertyInfo; 
     if (member == null) 
     { 
      throw new ArgumentException("propertyExpression"); 
     } 
     if (member.GetGetMethod(true).IsStatic) 
     { 
      throw new ArgumentException("propertyExpression"); 
     } 
     return body.Member.Name; 
    } 
} 
+0

PRISMはPropertyChangedイベントの追加/削除メソッドで何をしていますか?とにかく、彼らは私が上に持っているものを効果的にやっているように見えます: 'Expression body.Member.Name.'パフォーマンスの問題を助けません。 PRISMは明らかにパフォーマンスが最適化されていません。もし、 'ExtractPropertyName'メソッドの最後に' 'body.Member'"を呼び出すのではなく、定義された変数 '' member''を再利用したとします。 – Alain

+0

「コードxxxはマイクロソフトのためにできるだけ最適化されている」と言っているのは、暴言です。私はそれが真実だとは思わないが、真実は間にどこかにある。 –

+0

@Alainまた、正しく反映されていない可能性があります。私はそれをコンパイルするためにコードを微調整する必要がありました。おそらく、このような変更を基本クラスに簡単に行い、Magic Stringsを使ってパフォーマンステストを実行し、正規表現を使ってfind/replaceを実行してPropertyChange呼び出しを新しいラムダに置き換えて、パフォーマンステスト。 – Rachel

1

以下のコードを再作成することができます実際に私たちは私たちのプロジェクトのために、このaswellを議論し、長所と短所についてたくさん話しました。最終的には、通常の方法を維持することに決めましたが、フィールドを使用しました。

public class MyModel 
{ 
    public const string ValueProperty = "Value"; 

    public int Value 
    { 
     get{return mValue;} 
     set{mValue = value; RaisePropertyChanged(ValueProperty); 
    } 
} 

これはリファクタリングするとき、役立ちます私たちのパフォーマンスを維持し、我々は再びハードコードされた文字列が必要になりPropertyChangedEventManagerを、使用するときに特に便利です。

public bool ReceiveWeakEvent(Type managerType, object sender, System.EventArgs e) 
{ 
    if(managerType == typeof(PropertyChangedEventManager)) 
    { 
     var args = e as PropertyChangedEventArgs; 
     if(sender == model) 
     { 
      if (args.PropertyName == MyModel.ValueProperty) 
      { 

      } 

      return true; 
     } 
    } 
} 
+0

ああ、少なくともあなたがなぜdownvoteときに、なぜ。私の答えは問題の有効な代替案です。 Lambdaの魔法に対してハードコードされた文字列を使用すると、パフォーマンスが低下する可能性があります。私のソリューションはパフォーマンスを傷つけず、ハードコーディングされた文字列よりも保守が簡単です。 – dowhilefor

+1

私はダウンボートしませんでしたが、これは役に立ちません。それでもハードコーディングされた文字列が含まれます。実際には100個のプロパティがある場合、それらの名前を格納するのに100個の新しい文字列が必要になります。そのプロパティの名前を変更すると、名前、ハードコードされた文字列、名前を変更する必要があります。ハードコーディングされた文字列 – Alain

+0

私たちのために名前の変更は心配されませんでした。なぜなら私たちはresharperを使用しているからかもしれません。第二に、私たちのモデルが生成されるので、すべてのプロパティが簡単に識別子を取得できます。最後は重要なのは、パフォーマンスは私たちにとって最も重要な部分でした。もちろんラムダほど素晴らしいものではありませんでしたが、私たちにとってはうまくいきます。特にWeakEventManagerに関する部分。だから最後に私は、別のアプローチは、手元のソリューションの長所と短所を考慮し、重視するのには少なくとも役立つかもしれないと考えました。しかし、私はそれがあなたにとって有益でないならば尊敬します。 – dowhilefor

2

ラムダ式ツリーソリューションが遅すぎる可能性がある場合は、プロファイルして調べてください。私は、表現ツリーを開かせるのに費やされる時間が、UIが応答して爽やかに過ごす時間よりもかなり小さいと思う。

INotifyPropertyChangedを実装して、基本クラスを作成し、それを与える:

あなたはそれが遅すぎることを見つけ、あなたの性能基準を満たすためにリテラル文字列を使用する必要がある場合は、その後、ここで私が見てきた一つの方法ですRaisePropertyChanged方法。このメソッドは、イベントがヌルかどうかをチェックし、PropertyChangedEventArgsを作成し、イベントを起動します。

しかし、このメソッドにはいくつかの特別な診断も含まれています。クラスが本当にその名前のプロパティを持っていることを確認するために、Reflectionを実行します。プロパティが存在しない場合は、例外がスローされます。プロパティが存在する場合は、その結果をメモします(たとえば、プロパティ名をスタティックHashSet<string>に追加することによって)、再度Reflectionチェックを行う必要はありません。

これで、プロパティの名前を変更してもマジック文字列の更新に失敗すると、自動的にテストが失敗します。 MVVMを使用する主な理由は、ViewModelsの自動テストがあると仮定しているからです。

本番環境では騒がしくならないようにするには、余分な診断コードを#if DEBUG

+0

lambda-expression-treeがパニングしない場合の良い提案です。 – Alain

1

コンパイル前にすべてのファイルを単純に前処理し、set {...}ブロックで定義されているOnPropertyChanged呼び出しを検出し、プロパティ名を決定し、それに応じて名前パラメータを修正します。

アドホックツール(これは私の推奨事項)を使用するか、実際のC#(またはVB.NET)パーサーを使用してください(Parser for C#)。

私はそれを行うには合理的な方法だと思います。もちろん、あまり優雅でスマートではありませんが、実行時の影響はゼロです。マイクロソフトのルールに従います。

あなたには、いくつかのコンパイル時間を節約したい場合、あなたはこのように、コンパイルディレクティブを使用して、両方の方法を持つことができます:

set 
{ 
#if DEBUG // smart and fast compile way 
    OnPropertyChanged(() => PropertyName); 
#else // dumb but efficient way 
    OnPropertyChanged("MyProp"); // this will be fixed by buid process 
#endif 
}