myCSharp.de - DIE C# und .NET Community
Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 
 | Suche | FAQ

» Hauptmenü
myCSharp.de
» Startseite
» Forum
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Suche
   » Plugin für Firefox
   » Plugin für IE
   » Gadget für Windows
» Regeln
» Wie poste ich richtig?
» Datenschutzerklärung
» wbb-FAQ

Mitglieder
» Liste / Suche
» Stadt / Anleitung dazu
» Wer ist wo online?

Angebote
» ASP.NET Webspace
» Bücher
» Zeitschriften
   » dot.net magazin

Ressourcen
» guide to C#
» openbook: Visual C#
» openbook: OO
» MSDN Webcasts
» Search.Net

Team
» Kontakt
» Übersicht
» Wir über uns
» Impressum

» Unsere MiniCity
MiniCity
» myCSharp.de Diskussionsforum
Du befindest Dich hier: Community-Index » Diskussionsforum » Knowledge Base » Artikel » [Artikel] INotifyPropertyChanged implementieren
Letzter Beitrag | Erster ungelesener Beitrag Druckvorschau | Thema zu Favoriten hinzufügen

Antwort erstellen
Zum Ende der Seite springen  

[Artikel] INotifyPropertyChanged implementieren

 
Autor
Beitrag « Vorheriges Thema | Nächstes Thema »
tom-essen tom-essen ist männlich
myCSharp.de-Poweruser/ Experte

images/avatars/avatar-2140.png


Dabei seit: 15.05.2005
Beiträge: 1.813
Entwicklungsumgebung: VS.NET 2013
Herkunft: NRW


tom-essen ist offline

[Artikel] INotifyPropertyChanged implementieren

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

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:

C#-Code (INotifyPropertyChanged-Member):
event PropertyChangedEventHandler PropertyChanged;

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

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

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

C#-Code:
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:

C#-Code:
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: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:

C#-Code (INotifyPropertyChanged-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:

C#-Code:
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.


Dateianhang:
zip INotifyPropertyChangedTests.zip (60 KB, 784 mal heruntergeladen)
16.01.2010 13:17 Beiträge des Benutzers | zu Buddylist hinzufügen
TripleX TripleX ist männlich
myCSharp.de-Mitglied

images/avatars/avatar-3071.jpg


Dabei seit: 30.09.2006
Beiträge: 328
Entwicklungsumgebung: Visual Studio 2010
Herkunft: Nürtingen


TripleX ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

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

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:

C#-Code:
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:

C#-Code:
        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:

C#-Code:
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:

C#-Code:
[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:

C#-Code:
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

Dieser Beitrag wurde 1 mal editiert, zum letzten Mal von TripleX am 17.01.2010 01:02.

17.01.2010 00:56 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Zwischen diesen beiden Beiträgen liegen mehr als 3 Jahre.
Daniel_3_17 Daniel_3_17 ist männlich
myCSharp.de-Mitglied

Dabei seit: 11.01.2008
Beiträge: 94
Entwicklungsumgebung: VS2013 Pro


Daniel_3_17 ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

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

C#-Code:
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:

C#-Code:
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:

C#-Code:
private string _myProperty;
public string MyProperty
{
    get { return _myProperty; }
    set { SetProperty(ref _myProperty, value); }
}

Grüße,
Daniel
07.08.2013 10:09 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Baumstruktur | Brettstruktur       | Top 
myCSharp.de | Forum Der Startbeitrag ist älter als 7 Jahre.
Der letzte Beitrag ist älter als 4 Jahre.
Antwort erstellen


© Copyright 2003-2017 myCSharp.de-Team. Alle Rechte vorbehalten. 18.08.2017 23:59