私は、NotifyPropertyChanged("PropertyName")
の形式のマジックストリングの多用に苦しんでいる大規模なチームアプリケーションに取り組んでいます。これは、Microsoftに相談するときの標準的な実装です。また、多数の誤った名前のプロパティ(何百もの計算プロパティが格納されている計算モジュールのオブジェクトモデルを操作する)に悩まされています。これらはすべてUIにバインドされています。
私のチームは、プロパティ名の変更に関連する多くのバグを経験しており、マジックストリングが正しくなく、バインディングが壊れています。マジックストリングを使用せずにプロパティ変更通知を実装することで問題を解決したいと思います。 .Net 3.5で私が見つけた唯一の解決策は、ラムダ式です。 (例: INotifyPropertyChangedの実装-より良い方法はありますか? )
私のマネージャーは、から切り替えることのパフォーマンスコストについて非常に心配しています
set { ... OnPropertyChanged("PropertyName"); }
に
set { ... OnPropertyChanged(() => PropertyName); }
名前が抽出された場所
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);
}
パラメータが変更されると、約100個の値が再計算され、UIでリアルタイムに更新されるスプレッドシートのようなアプリケーションについて考えてみます。この変更を行うと、UIの応答性に影響を与えるほどの費用がかかりますか?さまざまなプロジェクトやクラスでプロパティセッターを更新するのに約2日かかるため、現時点でこの変更をテストすることを正当化することすらできません。
NotifyPropertyChangedの徹底的なテストを行って、ラムダ式への切り替えの影響を確認しました。
これが私のテスト結果です:
ご覧のとおり、ラムダ式の使用は、ハードコードされた単純な文字列プロパティ変更の実装よりも約5倍遅くなりますが、それでも1秒あたり10万のプロパティ変更をポンプで出力できるため、ユーザーは心配する必要はありません。とても特別な仕事用コンピュータ。そのため、文字列をハードコーディングする必要がなくなり、すべてのビジネスを処理する1行のセッターを使用できることで得られるメリットは、パフォーマンスコストをはるかに上回ります。
テスト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);
}
}
}
テストゴムが道路に出会った場所であり、観察可能なすべてのプロパティアクションを1行で実行するためのこの新しい美しい構文を披露することができます。
public UInt64 TestValue3
{
get { return testValue3; }
set { SetNotifyingProperty(() => TestValue3, ref testValue3, value); }
}
実装
すべてのViewModelが最終的に継承するBindingObjectBaseクラスには、新しい機能を駆動する実装があります。エラー処理を取り除いたので、関数の要点は明確です。
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;
}
}
個人的には、この理由からMicrosoftPRISMのNotificationObject
を使用するのが好きです。また、Microsoftによって作成されているため、コードはかなり最適化されていると思います。
「MagicStrings」を保持することに加えて、RaisePropertyChanged(() => this.Value);
などのコードを使用できるため、既存のコードを壊すことはありません。
Reflectorでコードを見ると、以下のコードで実装を再作成できます。
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;
}
}
ラムダ式ツリーのソリューションが遅すぎるのではないかと心配な場合は、プロファイルを作成して調べてください。式ツリーを開くのに費やす時間は、UIがそれに応じて更新するのに費やす時間よりもかなり短いと思います。
遅すぎることがわかり、パフォーマンス基準を満たすためにリテラル文字列を使用する必要がある場合は、これが私が見た1つのアプローチです。
INotifyPropertyChanged
を実装する基本クラスを作成し、それにRaisePropertyChanged
メソッドを指定します。このメソッドは、イベントがnullかどうかをチェックし、PropertyChangedEventArgs
を作成して、イベントを発生させます。これは通常のすべてのものです。
ただし、このメソッドにはいくつかの追加の診断も含まれています。クラスが実際にその名前のプロパティを持っていることを確認するために、いくつかのリフレクションを実行します。プロパティが存在しない場合は、例外がスローされます。プロパティが存在する場合は、その結果をメモ化するため(たとえば、プロパティ名を静的HashSet<string>
に追加することにより)、リフレクションチェックを再度実行する必要はありません。
プロパティの名前を変更するとすぐに自動テストが失敗し始めますが、マジックストリングの更新に失敗します。 (MVVMを使用する主な理由は、ViewModelの自動テストがあることを前提としています。)
本番環境でそれほど騒々しく失敗したくない場合は、追加の診断コードを#if DEBUG
内に配置できます。
実際、私たちはプロジェクトについてもこれについて話し合い、賛否両論についてたくさん話しました。結局、通常の方法を維持することにしましたが、フィールドを使用しました。
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;
}
}
}
簡単な解決策の1つは、コンパイル前にすべてのファイルを前処理し、set {...}ブロックで定義されているOnPropertyChanged
呼び出しを検出し、プロパティ名を決定し、それに応じてnameパラメーターを修正することです。
これは、アドホックツール(私の推奨事項)を使用するか、実際のC#(またはVB.NET)パーサー(ここにあるようなもの)を使用して行うことができます: C#のパーサー ) 。
それは合理的な方法だと思います。もちろん、それはあまりエレガントでもスマートでもありませんが、実行時の影響はなく、Microsoftのルールに従います。
コンパイル時間を節約したい場合は、次のようにコンパイルディレクティブを使用して両方の方法を使用できます。
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
}