Laden...

INotifyPropertyChanged oder Custom Event? Grundsatzfrage?

Erstellt von GeneVorph vor 5 Jahren Letzter Beitrag vor 5 Jahren 1.879 Views
G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 5 Jahren
INotifyPropertyChanged oder Custom Event? Grundsatzfrage?

Hallo,
ich versuche gerade schlau zu werden aus INotifyPropertyChanged. Folgenden Code habe ich:


public class PropertyOfInterest : INotifyBase
{
private int _importantProperty;
public int ImportantProperty
{
get { return _importantProperty; }
set
{
_importantProperty = value;
OnPropertyChanged(nameof(ImportantProperty);
}
} 
}

[INotifyBase ist die Klasse, in der INotifyPropertyChanged implementiert ist; PropertyOfInterest erbt von INotifyBase]

Nun kann ich z. B. in einer WinForms-Anwendung ein Label an dieses Property binden, z. B.

PropertyOfInterest myProperty = new PropertyOfInterest();
label1.DataBindings.Add(„Text“, myProperty, „ImportantProperty“ );

Soweit so gut.

Frage1: Aber nehmen wir mal an, ich möchte (wie in meinem Beispiel) ein int Property verändern, und je nachdem ob der Wert größer oder kleiner 0 ist, soll der Labeltext grün oder rot sein. Wie hilft mir IPropertyNotifyChanged hier weiter – kann der Wert übergeben werden?
(Ich kann mir momentan nur eine „wilde“ Methode vorstellen, die auf Label1_TextChanged reagiert, den String gefährlicherweise konvertiert und dann den Wert prüft – aber das ist gefühlt wrrrggggs…)

Frage2: ich habe im Step-Modus beim Debugging festgestellt, dass bei OnPropertyChanged(nameof()) scheinbar alle Properties der Klasse durchlaufen werden – tatsächlich wirkt sich das dann aber nur auf das Property aus, das mit nameof() übergeben wurde; wozu also das „Abklappern“ der anderen Properties? (an meinem obigen Beispiel so nicht darstellbar – komme auch jetzt erst beim Schreiben auf die Idee, es mal mit diesem Beispiel zu versuchen und mehrere Properties einzufügen… in meiner App sind es ca. 20 Properties die von OnPropertyChanged Gebrauch machen; da geht das step-by-step Debugging bei jedem Aufruf durch alle 20, obwohl natürlich nur jenes, das in nameof() deklariert ist, auch behandelt wird.)

Oder aber: ich schreibe gleich ein Event:

public class PropertyOfInterest 
{
	public delegate OnPropertyChangingDelegate(int newValue);
	public event OnPropertyChangingDelegate OnPropertyHasChanged;

	private int _importantProperty;
	public int ImportantProperty
	{
		get { return _importantProperty; }
		set
		{
			_importantProperty = value;
			OnPropertyHasChanged?.Invoke(_importantProperty);
}
} 
}

Dieses Event aboniere ich dann einfach in der Klasse, die auf das Event reagieren soll:


myProperty.OnPropertyHasChanged += PropertyHasChanged;

private void PropertyHasChanged(int newValue)
{
	If (newValue > 0)
{
	//blablabla
}
else
{
	//blubbblubbblubb
}
}

Frage3: Mit dem DataBinding sieht es da natürlich anders aus – schließlich sprechen wir einmal von einem Interface, einmal von einem Event; wobei ich im Falle des Labels natürlich auch hier dieselbe Funktionalität wie mein obiges Interface bereitstellen kann (prinzipiell zumindest). Sollte das der einzige Unterschied sein?

Da stellt sich mir dann schon die Frage: was macht man nun mit INotifyPropertyChanged? Sollte man in der Praxis von Fall zu Fall abwägen, ob man besser mit INotifyPropertyChanged bedient ist oder mit einem Custom Event? Oder ist INotifyChangedProperty der „bessere“ (sicherere?) Weg für UI-Updates? Sorry, aber tiefer reicht mein „Fachwissen“ (hüstel) derzeit nicht – die technischen Zusammenhänge würden mich dennoch interessieren.

Vielen Dank schonmal,
Vorph

2.080 Beiträge seit 2012
vor 5 Jahren

INotifyPropertyChanged bzw. das Event darin ist erst einmal nur eins: Ein Event, das angibt, dass eine Property sich geändert hat. Was mit dieser Info passiert, interessiert das Event nicht.

Frage 1
Zwei Möglichkeiten:1.Ein wilder EventHandler, der den Text parst und entsprechend entscheidet 1.Im CodeBehind auf das Event horchen und entsprechend reagieren 1.Eine weitere Property, welche die Farbe angibt

Was von Beidem Du verwendest, musst Du wissen. Ich würde zu Option 1 oder 2 tendieren, da die Farbe grundsätzlich nur für die View und für nichts weiter von Bedeutung ist. Daher gehört es mMn. auch weder in Controller, Presenter, ViewModel, what ever.

Bei WPF gibt's dafür Converter oder Trigger. Da müsstest Du den Text allerdings auch parsen, interpretieren oder direkt mit dem String der Zahl arbeiten.
Um das Parsen bzw. Text interpretieren kommst Du also nicht drum herum, daher sollte dieser Code auch so gebaut sein, dass er mit jedem Text klarkommt.

Frage 2
Dass WinForms alle Properties durchsucht, kann ich nicht bestätigen (ich arbeite mit WPF), aber das ist wenn dann ein Problem von WinForms. Bei WPF gibt es dieses Verhalten jedenfalls nicht, dort kannst Du aber als PropertyName einen Leerstring übergeben, das interpretiert WPF in etwa wie "Alle Properties haben sich geändert".

Grundsätzlich kannst (und solltest) Du dieses Verhalten aber auch abfangen. Du musst immer davon ausgehen, dass eine Property auch mal unnötig oft abgerufen wird, z.B. wenn irgendwo eine Schleife ständig darauf zugreift. Wenn die Property aufwändige Daten zurückgeben soll, dann kannst Du diese Daten über eine Reload-Methode (neu) laden und über die Property nur noch stumpf zurück geben.

Frage 3
Du kannst natürlich dein eigenes Event basteln, aber es bringt dir nichts, da weder WinForms, WPF oder Andere dieses Event nutzen. Diese Frameworks brauchen die zusätzlichen Infos nicht, daher gehe ich nicht davon aus, dass da noch etwas kommen wird.

Wenn Du ein eigenes Event bauen willst, solltest Du allerdings noch darauf achten, dass Du dich an das Pattern hältst (zwei Parameter: object sender, EventArgs-Ableitung e), der mitgelieferte Wert ein object ist und auch ein Property-Name dabei ist, da das Event ja unabhängig für alle Properties funktionieren soll. Gleichzeitig würde ich aber voraussetzen, dass auch das Original implementiert wird, das könnte dann die Basis-Klasse intern realisieren. In deinem Code kannst Du dann dein Event dann nutzen und WinForms, WPF, etc. nutzen das Original.

Beispiel:

public class PropertyChangedEventArgsEx : PropertyChangedEventArgs
{
    public object OldValue { get; }
    public object NewValue { get; }

    public PropertyChangedEventArgsEx(string propertyName, object oldValue, object newValue)
            : base(propertyName)
    {
        OldValue = oldValue;
        NewValue = newValue;
    }
}
public interface INotifyPropertyChangedEx : INotifyPropertyChanged
{
    new event EventHandler<PropertyChangedEventArgsEx> PropertyChanged;
}

Dank dem new erzwingt das Interface, dass das PropertyChanged vom Original explizit implementiert wird. Im Code siehst Du dann nur deine Variante, während WinForms sich um das Original kümmert. Und da die EventArgs vom Original ableiten, kannst Du genau diese EventArgs auch beim Original-Event nutzen.

Du kannst natürlich auch für jede Property ein eigenes Event erstellen, das hab ich in einem produktiven WinForms-Projekt schon gesehen. Allerdings wäre das zusätzlicher Aufwand an allen Stellen und meiner Meinung nach ist dieser Aufwand denkbar unnötig und überflüssig.

PS:
Ich würde die Basis-Klasse nicht "INotifyBase" nennen. Interfaces sollten laut Konvention mit einem "I" beginnen und da halten sich auch die Meisten dran, daher würde dein Name zu Verwirrungen führen.
Bei mir heißt diese Klasse "ObservableObject".

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 5 Jahren

Hallo Palladin,

vielen Dank für deine ausführliche Antwort. Tatsächlich habe ich etwas ähnliches vermutet; auf jeden Fall werde ich das in Bezug auf Frage 1 so handhaben, dass sich (sofern ich dich richtig verstanden habe) mit meinem Custom Event reagiere, um die View zu handeln; das INotifyPropertyChanged habe ich in meinem Projekt dann auch nur dazu verwendet, um DataBindings und einfache "Hallo-hier-hat-sich-ein-Propery-geändert"-Aktionen durchzuführen.

Kurzfristig hatte ich überlegt, ob ich nicht ein eigenes NotifyInterface anlegen sollte, habe es dann aber aus für mich naheliegenden Gründen verworfen.

Zu Frage 2 hätte ich noch eine Nachfrage:

... Du aber als PropertyName einen Leerstring übergeben, das interpretiert WPF in etwa wie "Alle Properties haben sich geändert".

Grundsätzlich kannst (und solltest) Du dieses Verhalten aber auch abfangen. Du musst immer davon ausgehen, dass eine Property auch mal unnötig oft abgerufen wird, z.B. wenn irgendwo eine Schleife ständig darauf zugreift.

Das mit dem Leerstring kenne ich noch nicht, bzw. werde mir das für WinForms mal ansehen. A) Habe ich dich so richtig verstanden, dass man generell die Übergabe des Leerstrings abfangen sollte? (Ich kann mir gerade bei Leibe keine Funktionalität vorstellen, bei der die Übergabe des Leerstrings überhaupt Sinn macht. Durch nameOf dürfte das doch gar nicht möglich sein, oder?)

B) Was mich tierisch nervt bei INPC und DataBinding - die Schreibweise! Gibt es etwas sichereres als TextBox1.DataBindigs.Add("Text", Class.Properties, "Name")? Die beiden Strings finde ich "bäh" im Sinne von fehleranfällig. Gibbets da nix besseres?

Ich würde die Basis-Klasse nicht "INotifyBase" nennen. Interfaces sollten laut Konvention mit einem "I" beginnen und da halten sich auch die Meisten dran, daher würde dein Name zu Verwirrungen führen.
Bei mir heißt diese Klasse "ObservableObject".

Ob du's glaubst oder nicht: dieser Schnitzer ist mir echt die ganze Zeit komplett durch die Lappen gegangen!! 8o
Danke für den Hinweis! Normalerweise versuche ich da immer drauf zu achten - zumal man ja von meinem INotifyBase erben kann und ich derjenige bin, der immer mit den Augen rollt, wenn mir jemand erzählt man könne von Interfaces erben, bzw. "Multiinheritance"... ja, ja, so kann's gehen 😁

schöne Grüße

4.942 Beiträge seit 2008
vor 5 Jahren

Zu 2 B)
Du kannst natürlich auch dort nameof(Control.Text) sowie nameof(Control.Name) benutzen (bzw. als Konstante anlegen und benutzen).

2.080 Beiträge seit 2012
vor 5 Jahren

Ich würde das Binding von WinForms nicht umgehen, nur wenn es wirklich nicht das kann, was Du brauchst. Für das simple aneinander Binden von Properties ist das ja kein Problem, da braucht's kein Old- oder NewValue. Auch dass es alle Properties auf einmal liest, sollte eigentlich kein Problem sein, wenn Du deine Properties richtig baust 😉
Für alle EventHandler, die Du selber schreibst, kannst Du natürlich dein eigenes Event nutzen.

Mir fällt auch kein Scenario ein, wo es Sinn macht, auf einen Schlag alle Properties zu aktualisieren, mir ist das nur mal aus Zufall aufgefallen. Es gibt aber bestimmt Situationen, wo das durchaus Sinn macht, z.B. wenn Du ein ViewModel beibehalten willst, die Datengrundlage aber änderst. Dadurch würden sich alle Properties ändern, also warum alle einzeln aktualisieren, wenn es auch auf einmal geht? Ob das irgendeinen Vorteil (z.B. im Bezug auf die Performance) hat, weiß ich aber nicht.

Ja, die Schreibweise von WinForms-Bindings finde ich auch ein wenig ... bescheiden 😄
Einfach geht es, wie Th69 schreibt, dazu nur eine Korrektur: Konstanten sind überflüssig 😉
nameof wird vom Compiler interpretiert und der ersetzt den Ausdruck dann durch den Namen des Members, sprich: nameof bzw. das Ergebnis funktioniert so oder so als Konstante.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

2.080 Beiträge seit 2012
vor 5 Jahren

PS:

Ich hab testweise eine kleine Anwendung gebaut, vier TextBoxen und vier Text-Properties.

public class ViewModel : INotifyPropertyChanged
{
    private readonly IDictionary<string, object> _propertyValues = new Dictionary<string, object>();

    public event PropertyChangedEventHandler PropertyChanged;

    public string Text1
    {
        get => GetValue<string>(nameof(Text1));
        set => SetValue(nameof(Text1), value);
    }
    public string Text2
    {
        get => GetValue<string>(nameof(Text2));
        set => SetValue(nameof(Text2), value);
    }
    public string Text3
    {
        get => GetValue<string>(nameof(Text3));
        set => SetValue(nameof(Text3), value);
    }
    public string Text4
    {
        get => GetValue<string>(nameof(Text4));
        set => SetValue(nameof(Text4), value);
    }

    private T GetValue<T>(string propertyName)
    {
        Debug.WriteLine("Get Property: " + propertyName);

        return _propertyValues.TryGetValue(propertyName, out var value)
            ? (T)value
            : default(T);
    }
    private void SetValue<T>(string propertyName, T value)
    {
        Debug.WriteLine("Set Property: " + propertyName);

        _propertyValues[propertyName] = value;
        RaisePropertyChanged(propertyName);
    }

    protected void RaisePropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

Die Get- und SetValue-Methoden schreiben jeweils eine Zeile in den Output.

Und die Bindings:

textBox1.DataBindings.Add(nameof(TextBox.Text), _viewModel, nameof(_viewModel.Text1), false, DataSourceUpdateMode.OnPropertyChanged);
textBox2.DataBindings.Add(nameof(TextBox.Text), _viewModel, nameof(_viewModel.Text2), false, DataSourceUpdateMode.OnPropertyChanged);
textBox3.DataBindings.Add(nameof(TextBox.Text), _viewModel, nameof(_viewModel.Text3), false, DataSourceUpdateMode.OnPropertyChanged);
textBox4.DataBindings.Add(nameof(TextBox.Text), _viewModel, nameof(_viewModel.Text4), false, DataSourceUpdateMode.OnPropertyChanged);

Sobald ich ein Zeichen in Zeile 1 schreibe, bekomme ich diese Ausgabe:

Set Property: Text1
Get Property: Text4
Get Property: Text3
Get Property: Text2
Get Property: Text1
Get Property: Text4
Get Property: Text3
Get Property: Text2
Get Property: Text1

Wenn es dafür eine sinnvolle Erklärung gibt, bin ich sehr gespannt darauf 😄
Ich finde das jedenfalls ziemlich unnötig, nicht nur einmal alle Bindings zu aktualisieren, sondern gleich zwei mal.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

G
GeneVorph Themenstarter:in
180 Beiträge seit 2015
vor 5 Jahren

Wenn es dafür eine sinnvolle Erklärung gibt, bin ich sehr gespannt darauf 😄
Ich finde das jedenfalls ziemlich unnötig, nicht nur einmal alle Bindings zu aktualisieren, sondern gleich zwei mal.

Und ich habe schon an mir gezweifelt 😄 Aber da täte mich auch die Antwort sehr interessieren!

@Th69: Ha - das ist cool! Ich hätte nicht gedacht, dass es auch für Properties funktioniert - ich hatte das immer mit Controls in Verbindung gebracht. In Zukunft werde ich diese Schreibweise wählen; von diesen Strings, die in Befehle umgesetzt werden, halte ich nicht sonderlich viel.

2.080 Beiträge seit 2012
vor 5 Jahren

nameof funktioniert für wahrscheinlich alles, was einen Namen hat, zumindest fällt mir spontan nichts ein, wo es nicht funktioniert. Ein bisschen schade finde ich, dass man im nameof keine private Member verwenden kann.

Beachte aber, es wird bei aufeinanderfolgenden Aufrufen immer der letzte Member genommen.
Bei Namespaces zum Beispiel:

namespace A.B.C.D.E
{
    public class MyClass
    {
        public const string Name = "Namespace: " + nameof(A.B.C.D.E); // E
    }
}

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

4.942 Beiträge seit 2008
vor 5 Jahren

@Palladin007: Mit Konstante meinte ich genau dein Beispiel (nur eben z.B. für nameof(TextBox.Text)), wenn man im Code öfter diesen Ausdruck verwendet (wie bei dem Beispiel von GeneMorph), um Schreibarbeit zu sparen, also z.B.:


const string Text = nameof(TextBox.Text);

textBox1.DataBindings.Add(Text, _viewModel, nameof(_viewModel.Text1), false, DataSourceUpdateMode.OnPropertyChanged);
textBox2.DataBindings.Add(Text, _viewModel, nameof(_viewModel.Text2), false, DataSourceUpdateMode.OnPropertyChanged);
textBox3.DataBindings.Add(Text, _viewModel, nameof(_viewModel.Text3), false, DataSourceUpdateMode.OnPropertyChanged);
textBox4.DataBindings.Add(Text, _viewModel, nameof(_viewModel.Text4), false, DataSourceUpdateMode.OnPropertyChanged);