Laden...

[Artikel] INotifyPropertyChanged implementieren

Erstellt von tom-essen vor 14 Jahren Letzter Beitrag vor 10 Jahren 35.606 Views
tom-essen Themenstarter:in
1.819 Beiträge seit 2005
vor 14 Jahren
[Artikel] INotifyPropertyChanged implementieren

Einleitung

Dieser Artikel entstand als Ableger aus dem [Artikel] Implementierung von IDataErrorInfo.

Die INotifyPropertyChanged-Schnittstelle INotifyPropertyChanged-Schnittstelle (System.ComponentModel) hilft bei der Benachrichtigung anderer Instanzen bzgl. veränderter Properties. Besonders in WPF hilft diese Schnittstelle bei der Trennung zwischen Logik und UI, da erst durch die Implementierung dieser Schnittstelle die Anzeige bei Änderungen aktualisiert wird.

Die Schnittstelle hat nur einen Member:


event PropertyChangedEventHandler PropertyChanged;

Zur empfohlenen Implementierung von Events gehört dann auch eine entsprechende Aufruf-Methode:


protected void OnPropertyChanged(String propertyName)
{
	PropertyChangedEventHandler tempHandler = PropertyChanged;
	if (tempHandler != null)
		tempHandler(this, new PropertyChangedEventArgs(propertyName));
}

Siehe dazu auch:*Gewusst wie: Implementieren von Schnittstellenereignissen (C#-Programmierhandbuch) *Gewusst wie: Veröffentlichen von Ereignissen, die den .NET Framework-Richtlinien entsprechen (C#-Programmierhandbuch) *[Lösung] Problem mit EventHandler

Basis-Implementierung
Nun ein Beispiel, welches die Basis-Funktionalität zur Wiederverwendung in einer abstrakten Klasse anbietet:


public abstract class NotifyPropertyChanged : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler tempHandler = PropertyChanged;
        if (tempHandler != null)
            tempHandler(this, new PropertyChangedEventArgs(propertyName));
    }
}

Hier eine weitere mögliche Standardimplementation von INotifyPropertyChanged.

Übergabe der Property-Namen
Kommen wir nun zum Aufruf des Events. Dieser findet in der Regel innerhalb der Setter statt, wenn sich der Wert tatsächlich verändert:


private int intProperty;

public int IntProperty
{
	get
	{
		return intProperty;
	}
	set
	{
		if (intProperty != value)
		{
			intProperty = value;
			OnPropertyChanged("IntProperty");
		}
	}
}

Hier zeigt sich ein gravierendes Problem: Die Übergabe des Property-Namen als fixem String-Parameter.
Ein Tippfehler oder vergessene Passagen beim Refactoring, und schon geht die Benachrichtigung ins Leere.

Wir brauchen also eine Alternative zu den fixen String-Parametern. Hier gibt es nun drei Möglichkeiten:*Lambda-Expressions *StackFrame()-GetMethod-Methode (System.Diagnostics) *MethodBase.GetCurrentMethod-Methode (System.Reflection)

Im ersten Fall würde anstatt des Property-Namen eine Lambda-Expression an eine entsprechende Prozedur übergeben, welche den Namen der Property aus dem ExpressionBody extrahiert.
Da der Name als Ausdruck übergeben wird, wird er nun auch beim Refactoring geändert bzw. der Compiler liefert eine entsprechende Fehlermeldung, wenn der Name falsch ist.
Nachteilig ist, dass diese Funktionalität in dieser Form nicht bei Properties in abgeleiteten Klassen funktioniert und bei Copy&Paste in andere Setter auch gültig bleibt, wenn man den Namen nicht anpasst.

Im zweiten Fall würde eine Methode aufgerufen werden, in welcher der Name der Property mit Hilfe des StackFrame ermittelt würde.
Diese Methode funktioniert dann aber nur, wenn sie innerhalb des jeweiligen Property-Setters aufgerufen wird.
Zudem wird im [Artikel] Attribute zur Prüfung von Properties verwenden noch ein Problem bzgl. Inlining angesprochen.

Im dritten Fall würde einfach eine .NET-Methode benutzt: System.Reflection.MethodBase.GetCurrentMethod(), welche ein Metadatenobjekt der aufrufenden Methode zurückliefert, u.a. mit der Property Name, welche den Namen der Methode enthält.

Alle Methoden führen zum gewünschten Ergebnis führen, allerdings haben die ersten beiden gravierende Nachteile und sind zudem deutlich lansgamer.

Vorteilhaft an der MethodInfo-Methode ist zudem die einfache, da immer gleiche Implementierung einer Zeile bei Verwendung einer Basis-Klassen mit der entsprechenden Funktionalität, somit sind auch Copy&Paste-Fehler ausgeschlossen.

FAZIT
Somit bleibt hier als einfache und sichere Variante lediglich die MethodInfo-Methode, da diese Refactoring-sicher und an jeder Stelle einsetzbar ist, und das Problem der Übergabe der Namen als Parameter ist gelöst.

Hier nun der Code für eine mögliche Referenzimplementierung:


using System.ComponentModel;
using System.Reflection;

public abstract class NotifyPropertyChanged : INotifyPropertyChanged
{
	#region INotifyPropertyChanged Member

	/// <summary>
	/// Occurs when a property value changes.
	/// </summary>
	public event PropertyChangedEventHandler PropertyChanged;

	/// <summary>
	/// Called when property changed.
	/// </summary>
	/// <param name="name">The name.</param>
	protected void OnPropertyChanged(string name)
	{
		PropertyChangedEventHandler tempHandler = PropertyChanged;
		if (tempHandler != null)
			tempHandler(this, new PropertyChangedEventArgs(name));
	}

	/// <summary>
	/// Called when property changed.
	/// </summary>
	/// <param name="methodBase">The method base.</param>
	protected void OnPropertyChanged(MethodBase methodBase)
	{
		OnPropertyChanged(methodBase.Name.Substring(4));
	}

	#endregion
}

Der Aufruf erfolgt dann so:


OnPropertyChanged(MethodInfo.GetCurrentMethod());

Wer noch mehr Informationen haben will, sollte sich den Artikel INotifyPropertyChanged – Varianten für die Implementierung durchlesen, dort werden weitere Varianten zur INotifyPropertyChanged-Implementierung vorgeschlagen, u.a. auch eine aspektorientierte Möglichkeit.

Im Anhang befindet sich noch eine Beispiel-Anwendung, welche Performance-Tests mit allen Varianten durchführt.

Abschließende Worte

Ganz herzlich bedanken möchte ich mich an dieser Stelle bei winSharp93, herbivore, JuyJuka, talla und Programmierhans für Ihre hilfreichen Kommentare und Anmerkungen bedanken.

Nobody is perfect. I'm sad, i'm not nobody 🙁

328 Beiträge seit 2006
vor 14 Jahren

Vielen Dank für deinen tollen Artikel, hab wieder einiges gelernt. 👍

Ich werde jetzt auch noch etwas zu deinem Artikel beitragen, denn ich denke dass das ganz gut hier herein passt:
Es kommt oft vor, dass eine Property von einer anderen abhängig ist. Wenn man also die eine Property ändert, ändert sich automatisch eine andere Property. Am besten ich zeige mal ein kurzes Beispiel:

public class TestClass : INotifyPropertyChanged
    {
        private int _width;
        private int _height;

        public int Height
        {
            get { return _height; }
            set { _height = value; NotifyPropertyChanged(MethodInfo.GetCurrentMethod()); }
        }

        public int Width
        {
            get { return _width; }
            set { _width = value; NotifyPropertyChanged(MethodInfo.GetCurrentMethod()); }
        }

        public int Area
        {
            get { return Height * Width; }
        }

Wie man erkennen kann ist Area von _Height _und _Width _abhängig. Also eigentlich müsste man, sobald man _Width _geändert, bekanntgegeben werden dass sich _Area _geändert hat. Ich habe mich demletzt mit diesem Problem auseinander gesetzt und bin dabei auf 2 Lösungen gekommen:

1. Die einfache Variante:
Man ändert die Setter der jeweiligen Properties folgendermaßen um:

        public int Height
        {
            get { return _height; }
            set
            {
                _height = value;
                NotifyPropertyChanged(MethodInfo.GetCurrentMethod());
                NotifyPropertyChanged("Area");
            }
        }

        public int Width
        {
            get { return _width; }
            set
            {
                _width = value;
                NotifyPropertyChanged(MethodInfo.GetCurrentMethod());
                NotifyPropertyChanged("Area");
            }
        }

        public int Area
        {
            get { return Height * Width; }
        }

Also sobald sich _Width _oder _Height _ändert, wird auch bekanntgegeben dass _Area _sich geändert hat.
Dass Problem bzw. unschöne an dieser Variante ist meiner Meinung jedoch, dass _Width _ eigentlich gar nichts von Area wissen sollte. Denn wenn wir aus irgendeinem Grund die Area nicht mehr brauchen und entfernen würden, dann sollten wir den Aufruf der NotifypropertyChanged() aus _Width _und Height auch entfernen (was man leicht vergessen kann). Außrdem kann es vorkommen, dass man mehrere Properties hat, welche von _Width _abhängig sind, und dann würden wir den setter nur unnötig aufblasen.

2. Die schönere Variante (mittels Attribute):
Die Nachteile aus Variante 1 löst man, indem man Attribute verwendet. Ich zeige euch mal einen Codeausschnitt, der das gleiche Ziel wie in Variante 1 verfolgt:

public int Height
        {
            get { return _height; }
            set { _height = value; NotifyPropertyChanged(MethodInfo.GetCurrentMethod()); }
        }

        public int Width
        {
            get { return _width; }
            set { _width = value; NotifyPropertyChanged(MethodInfo.GetCurrentMethod()); }
        }

        [DependsUpon("Height", "Width")]
        public int Area
        {
            get { return Height * Width; }
        }

Wie man erkennen kann hat _Area _jetzt ein Attribut erhalten. Mittels _DependsUpon _wird jetzt gesagt, dass Area von _Width _und Height abhängig ist. Sobald sich _Width _oder _Height _ändert wird automatisch auch das Event für _Area _gefeuert. Da dass aber nicht automatisch passiert brauchen wir noch ein wenig mehr Code. Hier ist ersmal der Code für das Attribut:

[global::System.AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
    sealed class DependsUponAttribute : Attribute
    {
        #region Private Members
        private readonly string[] _properties;
        #endregion

        #region Public Properties
        public string[] Properties
        {
            get { return _properties; }
        }
        #endregion

        #region Constructors
        public DependsUponAttribute(params string[] properties)
        {
            this._properties = properties;
        }
        #endregion
    }

Und hier ist die gesamt Klasse für das obige Beispiel:

public class TestClass : INotifyPropertyChanged
    {
        private Dictionary<string, string[]> _propertyDependencies;

        private int _width;
        private int _height;

        public int Height
        {
            get { return _height; }
            set { _height = value; NotifyPropertyChanged(MethodInfo.GetCurrentMethod()); }
        }

        public int Width
        {
            get { return _width; }
            set { _width = value; NotifyPropertyChanged(MethodInfo.GetCurrentMethod()); }
        }

        [DependsUpon("Height", "Width")]
        public int Area
        {
            get { return Height * Width; }
        }

        public TestClass()
        {
            #region Save depend Properties in the dictionary
            _propertyDependencies = new Dictionary<string, string[]>();

            foreach (var item in this.GetType().GetProperties())
            {
                var query = (from prop in this.GetType().GetProperties()
                             where prop.GetCustomAttributes(typeof(DependsUponAttribute), false)
                                       .Cast<DependsUponAttribute>()
                                       .Any((x) => x.Properties.Any((y) => y == item.Name))
                             select prop.Name).ToArray<string>();
                if (query.Count() > 0)
                {
                    _propertyDependencies.Add(item.Name, query);
                }

            }
            #endregion
        }


        #region INotifyPropertyChanged Implementation (+ Notify Dependend Properties)

        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged(String propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }

            #region Notify other Properties
            if(_propertyDependencies.ContainsKey(propertyName))
            {
                string[] depended = _propertyDependencies[propertyName];
                foreach (var propName in depended) NotifyPropertyChanged(propName);
            }
            #endregion
        }
        public void NotifyPropertyChanged(System.Reflection.MethodBase methodBase)
        {
            NotifyPropertyChanged(methodBase.Name.Substring(4));
        }
        #endregion
    }

Im Konstruktor der Klasse werden die Abhängigkeiten der Properties untereinander ermittelt und in eine Dictionary abgespeichert.
Sobald sich jetzt eine Property ändert, wird in NotifyPropertyChanged ersteinmal das event abgefeuert und danach wird in der Dictionary geschaut, ob für das Properties Abhängigkeiten bestehen und für jene wird dann auch eine Benachrichtigung gesendet.

Fazit:

  • Die zweite Variante ist zwar langsamer und braucht auch mehr Overhead als die erste, doch man sieht viel schneller welche Eigenschaften voneinander abhängig sind. Außerdem ist es für mich logischer, dass Width gar nicht weiß dass Area von ihr abhängig ist.
  • Das Problem mit den sog. "Magic Strings" ist leider bei beiden varianten vorhanden. Dadurch kann es Probleme beim Refactoring geben.

MfG TripleX

Träume nicht dein Leben sondern lebe deinen Traum.
Viele Grüße, David Teck

D
100 Beiträge seit 2008
vor 10 Jahren

Hi!

Seit dem .Net Framework 4.5 gibt es das CallerInformationAttribute. Das kann für eine INotifyPropertyChanged-Implementierung gut verwendet werden.

Beispiel-Implementierung (geänderter Code aus Posting #1 von tom-essen):

        
protected void OnPropertyChanged<T>([CallerMemberName] string propertyName = "")
{
    PropertyChangedEventHandler tempHandler = PropertyChanged;
    if (tempHandler != null)
        tempHandler(this, new PropertyChangedEventArgs(propertyName));
}

Im Setter der Property genügt dann ein Aufruf von:

OnPropertyChanged();

Das funktioniert natürlich nur, wenn der Aufrufer auch die Property ist, für die das PropertyChanged geworfen werden soll.

In einer typischen Basisklasse für ViewModels kann dann auch eine Methode implementiert werden, welche das Backing Field direkt mit setzt. Siehe INotifyPropertyChanged, The .NET 4.5 Way (Dan Rigby)

Der Aufruf wäre dann recht elegant und schlank:

        
private string _myProperty;
public string MyProperty
{
    get { return _myProperty; }
    set { SetProperty(ref _myProperty, value); }
}

Grüße,
Daniel