Entwickler-Ecke

WinForms - UI mit INotifiyPropertyChanged Interface aktualisieren


lapadula - Mo 04.02.19 14:51
Titel: UI mit INotifiyPropertyChanged Interface aktualisieren
Hallo, ich experimentiere gerade etwas mit dem PropertyChanged Interface und habe dazu eine Frage.

Ich möchte mein Klassen-Objekt aktuell halten. Wenn sich eine Property des Objektes ändert, dann möchte ich das
das UI aktualisieren.

Nur als Beispiel: Ich erstelle eine Rechnung. Das Klassen-Objekt Rechnung hat die Property Betrag und Umsatzsteuer.
Wenn der Benutzer einen Bruttobetrag eingibt, dann soll er die berechnete Umsatzsteuer unmittelbar sehen.

Ich habe da was zusammengebastelt, das Event wird aber nach unnötigerweise (in diesem Beispiel) doppelt abgefeuert.


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
43:
44:
45:
46:
47:
48:
49:
50:
51:
52:
53:
54:
55:
56:
57:
58:
59:
60:
61:
62:
63:
64:
65:
66:
67:
68:
69:
70:
71:
72:
73:
74:
75:
76:
77:
78:
79:
80:
81:
82:
public class ClassRechnung : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private readonly Dictionary<stringobject> propertyValues;


        public ClassRechnung()
        {
            propertyValues = new Dictionary<stringobject>();
        }
        
        public int Betrag
        {
            get { return Get(() => Betrag); }
            set
            {
                if (value != Betrag)
                {
                    Set(() => Betrag, value);
                }
            }
        }

        public decimal MwST
        {
            get { return Get(() => MwST); }
            set
            {
                if (value != MwST)
                {
                    Set(() => MwST, value);
                }
            }
        }

        protected void Set<T>(Expression<Func<T>> expression, T value)
        {
            string propertyName = GetPropertyNameFromExpression(expression);
            Set(propertyName, value);
        }
        public static string GetPropertyNameFromExpression<T>(Expression<Func<T>> expression)
        {
            MemberExpression memberExpression = (MemberExpression)expression.Body;
            return memberExpression.Member.Name;
        }

        protected void Set<T>(string name, T value)
        {
            if (propertyValues.ContainsKey(name))
            {
                propertyValues[name] = value;
                OnPropertyChanged(name);
            }
            else
            {
                propertyValues.Add(name, value);
                OnPropertyChanged(name);
            }
        }
        protected T Get<T>(string name)
        {
            if (propertyValues.ContainsKey(name))
            {
                return (T)propertyValues[name];
            }
            return default(T);
        }
        protected T Get<T>(Expression<Func<T>> expression)
        {
            string propertyName = GetPropertyNameFromExpression(expression);
            return Get<T>(propertyName);
        }

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(thisnew PropertyChangedEventArgs(propertyName));
            }
        }
    }



C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
public partial class Form1 : Form
    {
        ClassRechnung _Rechnung = null;
        public Form1()
        {
            InitializeComponent();
            _Rechnung = new ClassRechnung();
            _Rechnung.PropertyChanged += ClassRechnung_PropertyChanged;
        }
        private void ClassRechnung_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            UpdateUI();
        }

        private void textBoxBetrag_TextChanged(object sender, System.EventArgs e)
        {
            int Betrag = 0;
            if (int.TryParse(textBoxBetrag.Text, out Betrag))
            {
                _Rechnung.Betrag = Betrag;
                _Rechnung.MwST = (Betrag * 0.19M);
            }
        }

        private void UpdateUI()
        {
            labelBetrag.Text = _Rechnung.Betrag.ToString();
            labelMwst.Text = _Rechnung.MwST.ToString();
        }


    }


Ist vllt ein schlechtes Beispiel, weil die MwSt normalerweise nicht vom Benutzer verändert wird und ich somit das Event bei
dieser Property nicht abfeuern muss. Wenn es aber verschiedene Rechnungspositionen gibt, wo der Benutzer die Beträge ändern kann, dann möchte ich mein Rechnungsobjekt aktuell halten,
ohne das das Event mehrfach abgefeuert wird.

Ich könnte pfuschen und den handler, nach dem der Betrag gesetzt wurde rausnehmen und dann wieder zuweisen, so:


C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
_Rechnung .Betrag = Betrag;

_Rechnung .PropertyChanged -= ClassRechnung _PropertyChanged;

_Rechnung .MwST = (Betrag * 0.19M);
_Rechnung .xxx = xxx;
_Rechnung .xxx = xxx;
_Rechnung .xxx = xxx;
_Rechnung .xxx = xxx;

_Rechnung .PropertyChanged += ClassRechnung _PropertyChanged;


Wie kann ich es besser lösen?


Ralf Jansen - Mo 04.02.19 15:50

Die Klasse selbst kann nicht wissen wann du von außen fertig bist mit dem setzen von Properties. Aus sich selbst heraus(aus der Klasse selbst) sehe ich nicht wie man das machen sollte da muss man schon explizit nachhelfen.

Das man mehr als eine Property setzen muss ist aber, denke ich,

a.) eher ein Initialisierungsproblem, und kann dann dadurch gelöst werden das man den Changed Handler später verdrahtet.
b.) und es sollte beim ändern auch für jede Property ein Changed Event geworfen werden. Weil ja bei verschiedenen Properties unterschiedliche Receiver des Events reagieren sollen und in den EventArgs die betroffene Property explizit genannt wird.

Stell dir vor du würdest die beiden Labels nicht über die UpdateUI Methode aktualisieren sondern über DataBinding (was jetzt eigentlich super funktionieren sollte sobald INotifyPropertyChanged implementiert ist) du hast jetzt 2 Receiver für den Event das eine Label reagiert wenn in den EventArgs Betrag genannt wird und das andere wenn Mwst genannt wird.
Du hast aber das auf Property Ebene bezogene INotifyPropertyChanged Konzept auf Klassenänderung degeneriert und eine einzelne Änderung wieder auf eine komplette UI Aktualisierung zurückgemappt.

Wenn dich DataBinding nicht interessiert un du das nicht benutzen willst und dein Ziel Klassenänderungen zu verfolgen ist und nicht Propertyänderungen solltest du ein anderes eigenes Interface nehmen das so ähnlich funktioniert wie INotifyPropertyChanged. INotifyPropertyChanged nehmen aber es eigentlich anders benutzen wollen verwirrt andere Entwickler nur. Möglicherweise bleibt es auch nicht dabei nur andere Entwickler verwirrt werden ;)


Delete - Mo 04.02.19 22:59

- Nachträglich durch die Entwickler-Ecke gelöscht -


jfheins - Mi 06.02.19 21:59

Ich kann dir leider bei deinem Problem nicht direkt helfen. Nur: Wenn du zwei Properties änderst, dann sind zwei Events voll OK. Das soll so und ist in der Regel auch nicht zu langsam ;-)
(Oder meintest du, dass pro Property zweimal gefeuert wird?)

Was mir aber aufgefallen ist: Du machst das viel zu kompliziert mit dem INotifyPropertyChanged.
Schreib dir eine Basisklasse:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
  public abstract class BindableBase : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler PropertyChanged;

    protected void SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
    {
      if (!Equals(storage, value))
      {
        storage = value;
        PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(propertyName));
      }
    }

    protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
      PropertyChanged?.Invoke(thisnew PropertyChangedEventArgs(propertyName));
    }
  }


Und leite deine Models davon ab. In der Modellklasse hast du dann nur noch:

C#-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
  public class ClassRechnung : BindableBase
  {
    private int _betrag;
    public int Betrag
    {
      get { return _betrag; }
      set { SetProperty(ref _betrag, value); }
    }
  }