Die optimale Implementierung des INotifyPropertyChanged - Interfaces
Es existieren viele unterschiedliche Implementierungen des INotifyPropertyChanged - Interfaces.
Ausgehend von der allgemein bekannten Standard-Implementierung werden in diesem Beitrag die gängigsten Implementierungen analysiert sowie die Vor- und Nachteile aufgelistet. Zusätzlich wird eine eigene Implementierung, welche bereits in diversen größeren dotNet-Projekten verwendet wurde, vorgestellt. Außerdem kann am Ende des Beitrages eine ViewModelBase-Klasse (C#-Datei welche alle Implementierungen enthält) heruntergeladen werden. Diese darf darüber hinaus auch in eigenen Projekten verwendet werden.
[1] Die Verwendung der typischen Standard-Implementierung
public string someProperty;
public string SomeProperty
{
get { return someProperty; }
set
{
if (someProperty != value)
{
someProperty = value;
RaisePropertyChangedEvent( "SomeProperty" );
}
}
}Nachteile
- Der übergebene String-Parameter wird bei der Eingabe nicht überprüft wird und man kann sich somit unbemerkt vertippen => mögliche
Fehlerquelle
- Bei einer Änderung des Bezeichners könnte man vergessen den String-Parameter mit anzupassen, dies würde auch unbemerkt bleiben => mögliche
Fehlerquelle
- Es muss immer ein extra Feld als "Datenspeicher" implementiert werden
- Das PropertyChanged-Event muss manuell gefeuert werden
[2] Die Verwendung einer verbesserten Standard-Implementierung
Hierbei wird zusätzlich überprüft, ob eine Eigenschaft mit der Bezeichnung des übergebenen String-Parameters existiert :
[Conditional( "DEBUG" )]
private void checkPropertyName(string propertyName)
{
PropertyDescriptor propertyDescriptor = TypeDescriptor.GetProperties( this )[propertyName];
if (propertyDescriptor == null)
{
string message = string.Format( null, "The property with the propertyName '{0}' doesn't exist.", propertyName );
Debug.Fail( message );
}
}
Nachteile
Alle Nachteile der Standardlösung, jedoch mit kleinen Unterschied, dass beim Debuggen festgestellt werden kann, ob eine Eigenschaft mit dem übergebenen Bezeichner auch wirklich existiert
[3] Die Verwendung einer nochmals verbesserten Standard-Implementierung
Hierbei wird der Parameter als Lambda-Expression übergeben :
public string someProperty;
public string SomeProperty
{
get { return someProperty; }
set
{
if (someProperty != value)
{
someProperty = value;
RaisePropertyChangedEvent( () => SomeProperty );
}
}
}
Vorteile
Die in den ersten beiden Implementierungen genannten Nachteile als mögliche Fehlerquellen fallen nun weg, weil der Bezeichner in der Lambda Expression eine existierende Eigenschaft sein muss, da ansonsten die IDE (Visual Studio) bzw. spätestens der Compiler feststellen würde, dass eine Eigenschaft mit diesem Bezeichner nicht existiert
Bei einer Änderung des Bezeichners und der automatischen Änderung durch die IDE (Visual Studio) wird der Parameter auch mit angepasst
Nachteile
Es muss immer ein extra Feld als "Datenspeicher" implementiert werden
Das PropertyChanged-Event muss manuell gefeuert werden
[4] Die Verwendung einer optimalen Standard-Implementierung
Hierbei wird das Event ohne der Übergabe eines Parameters gefeuert (identisch zu der Vorgehensweise im dotNet-Framework 4.5 bzw. C# 5.0) :
public string someProperty;
public string SomeProperty
{
get { return someProperty; }
set
{
if (someProperty != value)
{
someProperty = value;
RaisePropertyChangedEvent();
}
}
}
Implementierung
protected void RaisePropertyChangedEvent()
{
// Get the call stack
StackTrace stackTrace = new StackTrace();
// Get the calling method name
string callingMethodName = stackTrace.GetFrame( 1 ).GetMethod().Name;
// Check if the callingMethodName contains an underscore like in "set_SomeProperty"
if (callingMethodName.Contains( "_" ))
{
// Extract the property name
string propertyName = callingMethodName.Split( '_' )[1];
if (this.PropertyChanged != null && propertyName != String.Empty)
{
this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
}
}
}
Vorteile
Die in den ersten beiden Implementierungen genannten Nachteile als mögliche Fehlerquellen fallen nun weg, da kein Parameter mehr übergeben werden muss
Eine Änderung des Bezeichners wirkt sich auch nicht problematisch aus, da wie gesagt kein Parameter übergeben wird
Nachteile
Es muss immer ein extra Feld als "Datenspeicher" implementiert werden
Das PropertyChanged-Event muss manuell gefeuert werden
[5] Die (meiner Meinung nach) optimale Implementierung
Hierbei wird jede Property auf die gleiche Weise, wie die zu den DependencyProperties gehörenden CLR-Wrapper implementiert. Bei den (zu den DependencyProperties) gehörenden CLR-Wrappern wird durch die Verwendung der Methoden GetValue bzw. SetValue das Property-System des .NET-Frameworks aktiv und führt diverse Evaluierungen im Hintergrund durch. Eine identische Vorgehensweise habe ich für eine optimale Implementierung des INotifyPropertyChanged-Interfaces verwendet.
Die Verwendung
public string SomeProperty
{
get { return GetValue( () => SomeProperty ); }
set { SetValue( () => SomeProperty, value ); }
}
Vorteile
Die in den ersten beiden Standard-Implementierungen genannten Nachteile als mögliche Fehlerquellen fallen nun weg, weil der Bezeichner in der Lambda Expression eine existierende Eigenschaft sein muss, da ansonsten die IDE (Visual Studio) bzw. spätestens der Compiler feststellen würde, dass eine Eigenschaft mit diesem Bezeichner nicht existiert
Bei einer Änderung des Bezeichners und der automatischen Änderung durch die IDE (Visual Studio) wird der Parameter auch mit angepasst
Ein extra Feld als "Datenspeicher" muss nicht mehr implementiert werden, da die Werte der einzelnen Eigenschaften automatische in einem Dictionary der ViewModelBase-Klasse verwaltet werden
Das Event muss nicht mehr manuell gefeuert werden, da dieses automatisch in der SetValue-Methode der ViewModelBase-Klasse erfolgt
Im Gegensatz zu den DependencyProperties des .NET-Frameworks muss der Rückgabewert der GetValue-Methode nicht mehr gecastet werden, da der Rückgabewert typensicher aus der Lambda-Expression ermittelt wird
Nachteile
KEINE
Da die (meiner Ansicht nach) optimale Implementierung ähnlich der Implementierung von DependencyProperties funktioniert, ist eine noch bessere Konsistenz in der Code Basis gewährleistet.
Implementierung
public abstract class ViewModelBase : INotifyPropertyChanged
{
#region < INotifyPropertyChanged > Members
/// <summary>
/// Is connected to a method which handle changes to a property (located in the WPF Data Binding Engine)
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raise the [PropertyChanged] event
/// </summary>
/// <param name="propertyName">The name of the property</param>
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
}
}
#endregion
private Dictionary<string, object> propertyValueStorage;
#region Constructor
public ViewModelBase()
{
this.propertyValueStorage = new Dictionary<string, object>();
}
#endregion
/// <summary>
/// Set the value of the property and raise the [PropertyChanged] event
/// (only if the saved value and the new value are not equal)
/// </summary>
/// <typeparam name="T">The property type</typeparam>
/// <param name="property">The property as a lambda expression</param>
/// <param name="value">The new value of the property</param>
protected void SetValue<T>(Expression<Func<T>> property, T value)
{
LambdaExpression lambdaExpression = property as LambdaExpression;
if (lambdaExpression == null)
{
throw new ArgumentException( "Invalid lambda expression", "Lambda expression return value can't be null" );
}
string propertyName = this.getPropertyName( lambdaExpression );
T storedValue = this.getValue<T>( propertyName );
if (!object.Equals( storedValue, value ))
{
this.propertyValueStorage[propertyName] = value;
this.OnPropertyChanged( propertyName );
}
}
/// <summary>
/// Get the value of the property
/// </summary>
/// <typeparam name="T">The property type</typeparam>
/// <param name="property">The property as a lambda expression</param>
/// <returns>The value of the given property (or the default value)</returns>
protected T GetValue<T>(Expression<Func<T>> property)
{
LambdaExpression lambdaExpression = property as LambdaExpression;
if (lambdaExpression == null)
{
throw new ArgumentException( "Invalid lambda expression", "Lambda expression return value can't be null" );
}
string propertyName = this.getPropertyName( lambdaExpression );
return getValue<T>( propertyName );
}
/// <summary>
/// Try to get the value from the internal dictionary of the given property name
/// </summary>
/// <typeparam name="T">The property type</typeparam>
/// <param name="propertyName">The name of the property</param>
/// <returns>Retrieve the value from the internal dictionary</returns>
private T getValue<T>(string propertyName)
{
object value;
if (propertyValueStorage.TryGetValue( propertyName, out value ))
{
return (T)value;
}
else
{
return default( T );
}
}
/// <summary>
/// Extract the property name from a lambda expression
/// </summary>
/// <param name="lambdaExpression">The lambda expression with the property</param>
/// <returns>The extracted property name</returns>
private string getPropertyName(LambdaExpression lambdaExpression)
{
MemberExpression memberExpression;
if (lambdaExpression.Body is UnaryExpression)
{
var unaryExpression = lambdaExpression.Body as UnaryExpression;
memberExpression = unaryExpression.Operand as MemberExpression;
}
else
{
memberExpression = lambdaExpression.Body as MemberExpression;
}
return memberExpression.Member.Name;
}
}
Die ViewModelBase-Klasse kann man sich hier herunterladen :
ViewModelBase.zip

Falls euch der Artikel gefallen sollte, dann bitte "kicken" :
