The Architect
aka "DotNetMastermind"

Wissenswertes zur Entwicklung hochwertiger grafischer Oberflächen (Rich User Interfaces) in WPF, Silverlight (dotNet, .NET) und Silverlight for Windows Phone 7 (WP7)

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

  1. Der übergebene String-Parameter wird bei der Eingabe nicht überprüft wird und man kann sich somit unbemerkt vertippen => mögliche Fehlerquelle.
  2. Bei einer Änderung des Bezeichners könnte man vergessen den String-Parameter mit anzupassen, dies würde auch unbemerkt bleiben => mögliche Fehlerquelle.
  3. Es muss immer ein extra Feld als "Datenspeicher" implementiert werden.
  4. 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

  1. 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

  1. 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.
  2. Bei einer Änderung des Bezeichners und der automatischen Änderung durch die IDE (Visual Studio) wird der Parameter auch mit angepasst.

Nachteile

  1. Es muss immer ein extra Feld als "Datenspeicher" implementiert werden.
  2. Das PropertyChanged-Event muss manuell gefeuert werden.

 

[4] Die Verwendung einer nahezu 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

  1. Die in den ersten beiden Implementierungen genannten Nachteile als mögliche Fehlerquellen fallen nun weg, da kein Parameter mehr übergeben werden muss.
  2. Eine Änderung des Bezeichners wirkt sich auch nicht problematisch aus, da wie gesagt kein Parameter übergeben wird.

Nachteile

  1. Es muss immer noch ein extra Feld als "Datenspeicher" implementiert werden.
  2. Das PropertyChanged-Event muss weiterhin 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

  1. 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
  2. Bei einer Änderung des Bezeichners und der automatischen Änderung durch die IDE (Visual Studio) wird der Parameter auch mit angepasst
  3. 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
  4. Das Event muss nicht mehr manuell gefeuert werden, da dieses automatisch in der SetValue-Methode der ViewModelBase-Klasse erfolgt
  5. 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

  1. 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( lambdaBLOCKED EXPRESSION;

 

        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( lambdaBLOCKED EXPRESSION;

 

        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" :

kick it on dotnet-kicks.de

Kommentare

dotnet-kicks.de sagte:

Sie wurden gekickt (eine gute Sache) - Trackback von dotnet-kicks.de

# November 1, 2012 9:56

DotNetKicks.com sagte:

You've been kicked (a good thing) - Trackback from DotNetKicks.com

# November 1, 2012 10:06

Redimensão de imagens (C# + WFP + MVVM + Asynchronism + Comentários) | Fernando Henrique Ferreira sagte:

Ping Antwort von  Redimens&atilde;o de imagens (C# + WFP + MVVM + Asynchronism + Coment&aacute;rios) | Fernando Henrique Ferreira

# Januar 5, 2014 3:25
Kommentar abgeben

(verpflichtend) 

(verpflichtend) 

(optional)

(verpflichtend)