Do czego można użyć interfejsu INotifyPropertyChanged?
Do czego można użyć interfejsu INotifyPropertyChanged?
To interfejs używany podczas DataBindingu, czyli łączenia wizualnych kontrolek z właściwościami obiektów. Stosowany jest w technologiach WinForms oraz WPF.
Wyobraźmy sobie, że mamy kontrolkę TextBox oraz obiekt klasy Customer z właściwością CustomerName. Kontrolka jest powiązania za pomocą DataBindingu z właściwością obiektu:
<TextBox Text="{Binding Path=Customer.CustomerName}" />
Dzięki temu tekst wprowadzany do kontrolki jest przenoszony do właściwości obiektu.
Niestety, w drugą stronę to nie działa. Jeśli zmieni się wartość w obiekcie, na przykład gdzieś w logice aplikacji, to kontrolka ta nadal będzie wyświetlała poprzednią wartość, gdyż kontrolka nie wie, że zmieniła się wartość w obiekcie.
Musimy odświeżyć kontrolkę. Jednak takie ręczne odświeżanie kontrolek nie byłoby zbyt wygodne i wydajne.
Dlatego z pomocą przychodzi interfejs INotifyPropertyChanged, który posiada zdarzenie PropertyChanged. Wystarczy wywołać to zdarzenie z naszej klasy, a kontrolka automatycznie się odświeży!
Jedyne, co musimy zrobić, to zaimplementować ten interfejs. Interfejs jest bardzo prosty - zawiera tylko jedno zdarzenie PropertyChanged.
Implementację tego interfejsu najlepiej od razu umieścić w abstrakcyjnej klasie bazowej, aby można go było używać w całym projekcie.
Przykładowa implementacja:
using System.ComponentModel; namespace Sample { public abstract class Base : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; // Create the OnPropertyChanged method to raise the event protected void OnPropertyChanged(string name) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } } }
Teraz możemy go użyć:
public class Customer : Base { private string customerName; public string CustomerName { get { return name; } set { name = value; OnPropertyChanged("CustomerName"); } } }
Jak widać, przy każdej zmianie generowane jest zdarzenie z nazwą właściwości. Dzięki temu wszystkie kontrolki, które zbindowane są do właściwości o tej nazwie, odświeżą się.
To najprostsza implementacja, ale posiada jeden zasadniczy mankament - przy wywołaniu zdarzenia przekazujemy nazwę właściwości w ciągu tekstowego. Łatwo popełnić błąd w postaci literówki. Dodatkowo trzeba pamiętać, aby przy zmianie nazwy właściwości poprawić również wywołanie.
Rozwiązaniem tego problemu jest zastosowanie wyrażeń Lambda:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.ComponentModel; using System.Linq.Expressions; namespace Sulmar.AQuick.Model { public abstract class Base : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } protected string GetPropertyName<TProperty>(Expression<Func<TProperty>> property) { var lambda = (LambdaExpression)property; MemberExpression memberExpression; if (lambda.Body is UnaryExpression) { var unaryExpression = (UnaryExpression)lambda.Body; memberExpression = (MemberExpression)unaryExpression.Operand; } else memberExpression = (MemberExpression)lambda.Body; return memberExpression.Member.Name; } public void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> property) { OnPropertyChanged(GetPropertyName(property)); } } }
Teraz możemy przekazać nazwę właściwości za pomocą wyrażenia Lambda:
public class Customer : Base { private string customerName; public string CustomerName { get { return name; } set { name = value; OnPropertyChanged( () => CustomerName); } } }
Ewentualną pomyłkę w nazwie wykryje teraz kompilator.
W przypadku, gdy korzystamy z .NET 4.5, możemy to napisać jeszcze inaczej i prościej:
using System.ComponentModel; namespace Sample { public abstract class Base : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; // Create the OnPropertyChanged method to raise the event protected void OnPropertyChanged([CallerMemberName] string name="") { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } } }
Użycie wygląda następująco:
public class Customer : Base { private string customerName; public string CustomerName { get { return name; } set { name = value; OnPropertyChanged(); } } }
Jak widać, nie musimy już przekazywać nazwy właściwości. Nazwa właściwości jest pobierana za pomocą atrybutu CallerMemberName, który posiada nazwę metody/właściwości, skąd przychodzi wywołanie.
Reasumując, interfejs INotifyPropertyChanged odgrywa kluczową rolę w DataBindingu. Powinien znać go i rozumieć każdy programista WPF lub WinForms, która stosuje DataBinding. Implementuje się go tylko raz, ale używa wielokrotnie.
W C# 6.0 można to zaimplementować jeszcze w inny sposób za pomocą nowego operatora nameof. Operator nameof zwraca nazwę zmiennej.
Implementacja interfejsu z użyciem operatora nameof:
using System.ComponentModel; namespace Sample { public abstract class Base : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; // Create the OnPropertyChanged method to raise the event protected void OnPropertyChanged([CallerMemberName] string name) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(name)); } } } }
Sposób użycia:
public class Customer : Base { private string _customerName; public string CustomerName { get { return _customerName ?? ""; } set { if (_customerName != value) { _customerName = value; OnPropertyChanged(nameof(CustomerName)); }; } } }
Przypomina to bardzo pierwszy i najprostszy sposób implementacji. Jednak dzięki zastosowaniu operatora nameof nie ma niebezpieczeństwa, że popełnimy literówkę lub zmienimy nazwę właściwości. Jednak użycie tego nie jest już tak wygodnie jak sposób z CallerMemberName, w którym nie musimy przekazywać nazwy.