2011-10-11 17 views
15

Eventuali duplicati:
typesafe NotifyPropertyChanged using linq expressionsImplementazione NotifyPropertyChanged senza fili magici

sto lavorando su una grande domanda di squadra che soffre di un uso pesante di stringhe di magia sotto forma di NotifyPropertyChanged("PropertyName"), - l'implementazione standard quando si consulta Microsoft. Stiamo anche soffrendo di un gran numero di proprietà errate (che funzionano con un modello a oggetti per un modulo di calcolo che ha centinaia di proprietà calcolate) - tutte collegate all'IU.

La mia squadra ha molti bug relativi alle modifiche del nome della proprietà che portano a stringhe magiche scorrette e vincoli di rottura. Desidero risolvere il problema implementando notifiche di proprietà modificate senza utilizzare stringhe magiche. Le uniche soluzioni che ho trovato per .Net 3.5 riguardano espressioni lambda. (Ad esempio: Implementing INotifyPropertyChanged - does a better way exist?)

mio direttore è estremamente preoccupato per il costo delle prestazioni di commutazione da

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

a

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

dove il nome viene estratto da

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); 
} 

Considera un'applicazione come un foglio di calcolo in cui, quando un parametro cambia, circa un centinaio di valori vengono ricalcolati e aggiornati sull'interfaccia utente in tempo reale. Rendere questo cambiamento così costoso da influire sulla reattività dell'interfaccia utente? Non posso nemmeno giustificare il test di questo cambiamento in questo momento, perché ci vorrebbero circa 2 giorni per aggiornare i setter di proprietà in vari progetti e classi.

+0

Uso la riflessione per questo. Vedi il mio post sul blog qui su questo. [http://tsells.wordpress.com/2011/02/08/using-reflection-with-wpf-and-the-inotifypropertychanged-interface/](http://tsells.wordpress.com/2011/02/08/using-reflection-with-wpf-and-the-inotifypropertychanged-interface /) Presta molta attenzione alla nota sulle prestazioni in fondo al post. – tsells

+0

Buon articolo, ma hai dato una differenza assoluta in termini di prestazioni, ma non è utile. Sarei molto più interessato alla differenza percentuale nei tempi di esecuzione. C'è un'enorme differenza tra passare da 200 ms a 300 ms e andare da 0,01 ms a 100,01 ms. Stessa differenza assoluta, differenza percentuale diversa. – Alain

+0

Ha detto che la differenza era di circa 1/4 di secondo per 10.000 notifiche di modifica delle proprietà. Non penso che questa sia una differenza abbastanza grande da preoccuparsene, e se davvero aggiorni più di 10.000 proprietà in una volta, riconsidererei seriamente il design :) – Rachel

risposta

22

ho fatto un test approfondito di NotifyPropertyChanged per stabilire l'impatto del passaggio alle espressioni lambda.

Qui erano i miei risultati dei test:

enter image description here

Come si può vedere, utilizzando l'espressione lambda è circa 5 volte più lento rispetto l'attuazione pianura modifica della proprietà di stringa hard-coded, ma gli utenti non devono preoccuparsi perché anche allora è in grado di pompare centomila cambi di proprietà al secondo sul mio computer di lavoro non così speciale. Pertanto, il vantaggio derivante dal non dover più stringere hard-code e poter disporre di setter a riga singola che si occupano di tutti gli affari supera di gran lunga il costo delle prestazioni.

Test 1 utilizzato l'implementazione standard setter, con un controllo per verificare che la proprietà aveva effettivamente cambiato:

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

Test 2 era molto simile, con l'aggiunta di una funzione che consente l'evento per tracciare il vecchio valore e il nuovo valore.Perché questo funzioni stava per essere implicita nel mio nuovo metodo di base di setter, ho voluto vedere quanto di nuovo in testa era dovuto a questa caratteristica:

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

Test 3 era dove la gomma incontra la strada, e arrivare a sfoggiare questo nuovo bel sintassi per l'esecuzione di tutte le azioni di proprietà osservabili in una sola riga:

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

Attuazione

nel mio BindingObjectBa Se la classe, che tutti i ViewModel finiscono per ereditare, si trova l'implementazione che guida la nuova funzionalità. Ho spogliato la gestione in modo che la carne della funzione di errore è chiaro:

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; 
} 

Tutti e tre i metodi si incontrano al di routine OnPropertyChanged, che è ancora lo standard:

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

Bonus

Se qualcuno è curioso, PropertyChangedExtendedEventArgs è qualcosa che mi è appena venuto in mente per estendere lo standard PropertyChangedEventArgs, quindi un'istanza dell'estensione può sempre essere al posto della base. Sfrutta la conoscenza del vecchio valore quando una proprietà viene modificata utilizzando SetNotifyingProperty e rende queste informazioni disponibili al gestore.

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

Interessante, ho eseguito un test simile sulla mia macchina qualche tempo fa (2013) e non sono stato in grado di misurare alcuna differenza di prestazioni (.NET 3.5 SP1, Win7 x64, Core i7) – ChrisWue

+0

@Alain - potrebbe essere migliorato per una versione ancora più bella utilizzando il nuovo attributo [CallerMemberName] - SetNotifyingProperty (ref storage, value); – Axarydax

+0

Sì, penso che questo fosse un modello abbastanza comune che MS ha lavorato su modi più efficienti per ottenere i nomi delle proprietà nelle ultime versioni .NET. Ho intenzione di suggerire che questo è buono come si ottiene solo per le versioni 3.5 o 4.0. – Alain

3

Personalmente mi piace utilizzare Microsoft PRISM NotificationObject per questo motivo, e suppongo che il loro codice sia ragionevolmente ottimizzato poiché è stato creato da Microsoft.

Mi consente di utilizzare codice come RaisePropertyChanged(() => this.Value);, oltre a mantenere le "stringhe magiche" in modo da non infrangere alcun codice esistente.

Se guardo il loro codice con il riflettore, la loro attuazione può essere ricreato con il codice qui sotto

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

Che cosa nel mondo sta facendo PRISM con l'evento PropertyChanged aggiungi/rimuovi metodi? Ad ogni modo, sembra che stiano facendo esattamente quello che ho sopra: 'Expression body.Member.Name'. Non aiuta comunque con la domanda sulle prestazioni. Ovviamente PRISM non è ottimizzato per le prestazioni - se fosse, avrebbero riutilizzato la loro variabile definita ''member'' piuttosto che chiamare" 'body.Member'" una seconda volta nella parte inferiore del loro metodo 'ExtractPropertyName'. – Alain

+0

Alcuni sostengono che dire "Codice xxx è il più ottimizzato possibile perché Microsoft" è un ossimoro. Non penso che sia vero, ma la verità _ giace in un punto intermedio. –

+0

@Alain Potrebbe anche essere stato riflesso in modo errato. Ho dovuto apportare alcune modifiche minori al codice per farlo compilare.Probabilmente sarebbe facile apportare una modifica come questa alla tua classe base, eseguire il test delle prestazioni con le tue stringhe magiche, quindi eseguire una ricerca/sostituzione usando le espressioni regolari per sostituire le chiamate PropertyChange con il nuovo lambda e rieseguire il tuo test di prestazione. – Rachel

1

In realtà, abbiamo discusso di questo aswell per i nostri progetti e ha parlato molto circa i pro ei contro. Alla fine, abbiamo deciso di mantenere il metodo normale ma abbiamo usato un campo per questo.

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

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

questo aiuta durante il refactoring, mantiene le nostre prestazioni ed è particolarmente utile quando si usa PropertyChangedEventManager, dove avremmo bisogno di nuovo le stringhe hardcoded.

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

Oh, vieni, quando decidi di dire almeno perché. La mia risposta è una valida alternativa al problema. Uso di stringhe hardcoded contro la magia Lambda, che potrebbe danneggiare le prestazioni. La mia soluzione non danneggia le prestazioni ed è più facile da gestire rispetto alle stringhe codificate. – dowhilefor

+1

Non ho fatto downvote, ma non è utile. Comprende ancora stringhe codificate in modo rigido. È peggio, infatti, perché se ho 100 proprietà, richiede 100 nuove stringhe per memorizzare quei nomi, e ora quando cambio il nome di quella proprietà, devo cambiare nome, la stringa hard coded e il nome di la stringa hard codificata. – Alain

+0

Per noi la ridenominazione non è mai stata una preoccupazione, forse perché usiamo il resharper e siamo un po 'viziati a causa di ciò. In secondo luogo, i nostri modelli sono generati, quindi tutte le proprietà possono facilmente ottenere un identificatore. Ultimo ma non meno importante, la performance è stata per noi la parte più importante, ovviamente non è bella come la cosa lambda ma per noi funziona abbastanza bene. Soprattutto la parte relativa a WeakEventManager. Quindi alla fine ho pensato che un approccio diverso potesse essere almeno utile da considerare e valutare i pro e i contro delle soluzioni a portata di mano. Ma io rispetto se non è utile per te. – dowhilefor

2

Se sei preoccupato che la soluzione lambda-espressione-albero potrebbe essere troppo lento, poi il profilo it e scoprire. Sospetto che il tempo speso per aprire la struttura delle espressioni sia un po 'più piccolo della quantità di tempo che l'interfaccia utente spenderà in risposta.

Se si scopre che è troppo lento, e avete bisogno di usare le stringhe letterali per soddisfare i criteri di rendimento, allora ecco uno approccio che ho visto:

Creare una classe base che implementa INotifyPropertyChanged, e dare un metodo RaisePropertyChanged. Questo metodo controlla se l'evento è nullo, crea lo PropertyChangedEventArgs e attiva l'evento, tutto il solito.

Ma il metodo contiene anche alcuni extra diagnostici: fa qualche riflessione per assicurarsi che la classe abbia realmente una proprietà con quel nome. Se la proprietà non esiste, genera un'eccezione. Se la proprietà esiste, memorizza tale risultato (ad es. Aggiungendo il nome della proprietà a un valore statico HashSet<string>), quindi non è necessario eseguire nuovamente il controllo Reflection.

Ed ecco fatto: i test automatici inizieranno non appena si rinomina una proprietà ma non si aggiorna la stringa magica. (Suppongo tu abbia eseguito test automatici per i tuoi ViewModels, poiché questo è il motivo principale per utilizzare MVVM.)

Se non si vuole fallire abbastanza rumorosamente in produzione, è possibile inserire il codice diagnostico aggiuntivo all'interno di #if DEBUG .

+0

Un buon suggerimento nel caso in cui l'albero di espressione lambda non venga visualizzato. – Alain

1

Una soluzione semplice è quella di semplicemente pre-processo di tutti i file prima della compilazione, rilevare le OnPropertyChanged chiamate che sono definiti nel set {...} blocchi, determinare il nome della proprietà e fissare il parametro del nome di conseguenza.

Si potrebbe fare questo utilizzando uno strumento ad hoc (che sarebbe la mia raccomandazione), o utilizzare un vero e proprio # C (o VB.NET) parser (come quelli che si possono trovare qui: Parser for C#).

Penso che sia un modo ragionevole per farlo. Certo, non è molto elegante né intelligente, ma ha un impatto zero sul runtime e segue le regole Microsoft.

Se si vuole risparmiare un po 'di tempo di compilazione, si potrebbe avere entrambi i modi utilizzando le direttive di compilazione, in questo modo:

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 
}