Implementowanie metody INotifyPropertyChanged w prosty sposób
Jeśli wykonano poprzednie lekcje, możesz pomyśleć, że implementacja powiązania danych jest zbyt duża. Dlaczego przejść przez wszystkie problemy z implementacją INotifyPropertyChanged
, wypalanie zdarzeń w lewo i w prawo, kiedy można po prostu użyć TimeTextBlock.Text = DateTime.Now.ToLongTime()
do wyświetlenia czasu? I to prawda, w tym prostym przypadku powiązanie danych wygląda jak overkill.
Jednak powiązanie danych jest w stanie o wiele więcej. Może przesyłać dane w obu kierunkach między interfejsem użytkownika a kodem, wyświetlać listy elementów i obsługiwać edytowanie danych. Wszystko to z architekturą, która oferuje czystą separację danych, na których działa logika aplikacji, oraz prezentację danych.
Ale jak możemy zmniejszyć ilość kodu, który deweloper musi napisać? Nikt nie chce wprowadzać dziesięciu wierszy kodu dla każdej właściwości, którą muszą zadeklarować. Na szczęście możemy wyodrębnić typowe funkcje i zmniejszyć zestawy właściwości do pojedynczego wiersza kodu. W tej lekcji pokazano, jak to zrobić.
Cel
Naszym celem jest przeniesienie wszystkich instalacji wodociągowych do implementacji interfejsu INotifyPropertyChanged
do oddzielnej klasy, aby uprościć tworzenie właściwości, która może powiadamiać interfejs użytkownika podczas jego zmiany. Przypominamy, że oto kod, który chcemy uprościć:
private bool _isNameNeeded = true;
public bool IsNameNeeded
{
get { return _isNameNeeded; }
set
{
if (value != _isNameNeeded)
{
_isNameNeeded = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsNameNeeded)));
}
}
}
W tym miejscu nie można używać właściwości automatycznych (takich jak public bool IsNameNeeded { get; set;}
) , ponieważ musimy wykonać coś w ustawieniu. Nie ma więc zbyt wiele do zrobienia z polem zapasowym, wierszem deklaracji właściwości. Korzystając z nowoczesnych funkcji języka C#, możemy zmienić metodę pobierania na get => _isNameNeeded;
, ale zapisuje tylko kilka naciśnięć klawiszy. Dlatego musimy skupić uwagę na ustawieniu właściwości. Czy możemy przekształcić to w jedną linię?
Klasa ObservableObject
Możemy utworzyć nową klasę bazową: ObservableObject
. Jest to nazywane obserwowalnym , ponieważ może być obserwowany przez interfejs użytkownika przy użyciu interfejsu INotifyPropertyChanged
. Dane i logika są hostowane w klasach dziedzicujących po nich, a interfejs użytkownika jest również powiązany z wystąpieniami tych dziedziczonej klasy.
1. Tworzenie ObservableObject
klasy
Utwórzmy nową klasę o nazwie ObservableObject
. Kliknij prawym przyciskiem myszy DatabindingSample
projekt w Eksplorator rozwiązań, wybierz pozycję Dodaj / Klasa, a następnie wprowadź ObservableObject
jako nazwę klasy. Wybierz pozycję Dodaj , aby utworzyć klasę.
1. Tworzenie ObservableObject
klasy
Utwórzmy nową klasę o nazwie ObservableObject
. Kliknij prawym przyciskiem myszy DatabindingSampleWPF
projekt w Eksplorator rozwiązań, wybierz pozycję Dodaj / Klasa i wprowadź ObservableObject
jako nazwę klasy. Wybierz pozycję Dodaj , aby utworzyć klasę.
2. Zaimplementuj INotifyPropertyChanged
interfejs
Następnie musimy zaimplementować interfejs i upublicznić naszą klasę INotifyPropertyChanged
. Zmień sygnaturę klasy, tak aby wyglądała następująco:
public class ObservableObject : INotifyPropertyChanged
Program Visual Studio wskazuje, że istnieje kilka problemów z programem INotifyPropertyChanged
. Znajduje się w nieprzywołynej przestrzeni nazw. Dodajmy go, jak pokazano tutaj.
using System.ComponentModel;
Następnie musimy zaimplementować interfejs. Dodaj ten wiersz wewnątrz treści klasy.
public event PropertyChangedEventHandler? PropertyChanged;
3. Metoda RaisePropertyChanged
W poprzednich lekcjach często podnosiliśmy PropertyChangedEvent
element w kodzie, nawet poza metodami ustawiania właściwości. Chociaż nowoczesne C# i operator warunkowy o wartości null lub (?.
) pozwoliły nam to zrobić w jednym wierszu, nadal możemy uprościć, tworząc funkcję wygody w następujący sposób:
protected void RaisePropertyChanged(string? propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Teraz w klasach, które dziedziczą z ObservableObject
klasy , wszystko, co musimy zrobić, aby zgłosić PropertyChanged
zdarzenie, jest następujące:
RaisePropertyChanged(nameof(MyProperty));
4. Metoda Set<T>
Ale co możemy zrobić na temat wzorca ustawiania, który sprawdza, czy wartość jest taka sama, jak to było, ustawia wartość, jeśli nie, i zgłasza PropertyChanged
zdarzenie? W idealnym przypadku chcielibyśmy przekształcić go w jedną liniową, w następujący sposób:
private bool _isNameNeeded = true;
public bool IsNameNeeded
{
get { return _isNameNeeded; }
set { Set(ref _isNameNeeded, value); } // Just one line!
}
To naprawdę nie może być prostsze niż to. Wywołujemy funkcję, przekazujemy odwołanie do pola zapasowego właściwości i ustawiamy nową wartość. Jak wygląda ta Set
metoda?
protected bool Set<T>(
ref T field,
T newValue,
[CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
{
return false;
}
field = newValue;
RaisePropertyChanged(propertyName);
return true;
}
Skopiuj poprzedni kod do treści ObservableObject
klasy. W przypadku [CallerMemberName]
programu należy również dodać następujący wiersz w górnej części pliku:
using System.Runtime.CompilerServices;
Istnieje wiele zaawansowanych języka C# i magia kompilatora dzieje się tutaj. Przyjrzyjmy się temu bliżej.
Set<T>
jest metodą ogólną, pomagając kompilatorowi upewnić się, że pole zapasowe i wartość są tego samego typu. Trzeci parametr metody , propertyName
, jest ozdobiony [CallerMemberName]
przez atrybut . Jeśli nie zdefiniujemy propertyName
obiektu podczas wywoływania metody, będzie ona mieć nazwę elementu członkowskiego wywołującego i umieścić go w nim w czasie kompilacji. Dlatego jeśli wywołamy Set
metodę z klasy setter IsNameNeeded
metody, kompilator umieszcza literał ciągu " IsNameNeeded" jako trzeci parametr. Nie trzeba kodować ciągów na stałe, a nawet używać nameof()
!
Set
Następnie metoda wywołuje metodę EqualityComparer<T>.Default.Equals
w celu porównania bieżącej i nowej wartości pola. Jeśli stare i nowe wartości są równe, Set
metoda zwraca false
wartość . Jeśli nie, pole kopii zapasowej jest ustawione na nową wartość, a PropertyChanged
zdarzenie jest zgłaszane przed zwróceniem true
wartości . Możesz użyć wartości zwracanej Set
metody, aby określić, czy wartość została zmieniona.
Po zaimplementowaniu ObservableObject
klasy zobaczmy, jak możemy jej używać w naszej aplikacji!
5. Tworzenie MainPageLogic
klasy
Wcześniej w tej lekcji przenieśliśmy wszystkie nasze dane i logikę z MainPage
klasy oraz do klasy dziedziczonej z ObservableObject
klasy .
Utwórzmy nową klasę o nazwie MainPageLogic
. Kliknij prawym przyciskiem myszy DatabindingSample
projekt w Eksplorator rozwiązań, wybierz pozycję Dodaj / Klasa, a następnie wprowadź MainPageLogic
jako nazwę klasy. Wybierz pozycję Dodaj , aby utworzyć klasę.
Zmień sygnaturę klasy, aby była publiczna i dziedziczona z klasy ObservableObject
.
public class MainPageLogic : ObservableObject
{
}
6. Przenieś funkcję zegara MainPageLogic
do klasy
Kod funkcji zegara składa się z trzech części: _timer
pola, ustawienia DispatcherTimer
w konstruktorze i CurrentTime
właściwości . Oto kod, który pozostawiliśmy w drugiej lekcji:
private DispatcherTimer _timer;
public MainPage()
{
this.InitializeComponent();
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_timer.Tick += (sender, o) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));
_timer.Start();
}
public string CurrentTime => DateTime.Now.ToLongTimeString();
Przenieśmy cały kod, który musi mieć związek z klasą _timer
MainPageLogic
do klasy . Wiersze w konstruktorze (z wyjątkiem this.InitializeComponent()
wywołania) powinny zostać przeniesione do MainPageLogic
konstruktora . Z poprzedniego kodu wszystkie elementy, które powinny pozostać w MainPage
obiekcie, to InitializeComponent
wywołanie w konstruktorze.
public MainPage()
{
this.InitializeComponent();
}
Na razie należy dotknąć tylko tej części kodu. Wkrótce wrócimy do pozostałej części MainPage
kodu klasy.
Po przeniesieniu MainPageLogic
klasa wygląda następująco:
public class MainPageLogic : ObservableObject
{
private DispatcherTimer _timer;
public MainPageLogic()
{
_timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_timer.Tick += (sender, o) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentTime)));
_timer.Start();
}
public string CurrentTime => DateTime.Now.ToLongTimeString();
}
Pamiętaj, że mamy wygodną funkcję do podnoszenia PropertyChanged
zdarzenia. Użyjmy tego w procedurze _timer.Tick
obsługi.
_timer.Tick += (sender, o) => RaisePropertyChanged(nameof(CurrentTime));
7. Zmień kod XAML tak, aby używał elementu MainPageLogic
Jeśli spróbujesz skompilować projekt teraz, wystąpi błąd informujący, że w pliku MainPage.xaml nie można odnaleźć właściwości "CurrentTime". I na pewno klasa MainPage
nie ma CurrentTime
już właściwości. Została przeniesiona do MainPageLogic
klasy. Aby rozwiązać ten problem, utworzymy właściwość o nazwie Logic
w MainPage
klasie . Będzie to typ MainPageLogic
, a wszystkie powiązania zostaną w tym celu wykonane.
Dodaj następujące elementy do MainPage
klasy:
public MainPageLogic Logic { get; } = new MainPageLogic();
Następnie w pliku MainPage.xaml znajdź TextBlock
zegar wyświetlający zegar.
<TextBlock Text="{x:Bind CurrentTime, Mode=OneWay}"
HorizontalAlignment="Right"
Margin="10"/>
I zmień powiązanie, dodając Logic.
do niego.
<TextBlock Text="{x:Bind Logic.CurrentTime, Mode=OneWay}"
HorizontalAlignment="Right"
Margin="10"/>
Teraz aplikacja zostanie skompilowana, a jeśli ją uruchomisz, zegar będzie tykał tak, jak powinien. Ładne!
8. Przenieś resztę logiki
Podniesiemy tempo. Przenieś resztę kodu w MainPage
klasie na MainPageLogic
. Wszystko, co powinno być pozostawione, to Logic
właściwość, konstruktor i PropertyChanged
zdarzenie.
9. Uproszczenie IsNameNeeded
W pliku MainPageLogic.cs zastąp IsNameNeeded
metodę ustawiającą właściwość wywołaniem nowej Set
metody.
public bool IsNameNeeded
{
get { return _isNameNeeded; }
set { Set(ref _isNameNeeded, value); }
}
10. Napraw metodę OnSubmitClicked
Na poziomie logiki nie zależy nam już na nadawcy lub argach zdarzeń kliknięcia przycisku. Dobrym rozwiązaniem jest również ponowne rozważenie nazwy metody. Nie robimy już kliknięć przycisków, przesyłamy logikę. Zmieńmy nazwę metody na OnSubmitClicked
, ustawmy ją jako Submit
publiczną i usuńmy parametry.
Wewnątrz metody istnieje nasz stary sposób podnoszenia PropertyChanged
zdarzenia. Zastąp go wywołaniem metody ObservableObject.RaisePropertyChanged
. W końcu cała metoda powinna wyglądać następująco:
public void Submit()
{
if (string.IsNullOrEmpty(UserName))
{
return;
}
IsNameNeeded = false;
RaisePropertyChanged(nameof(GetGreetingVisibility));
}
11. Zmień kod XAML tak, aby odwołył się do Logic
Następnie wróć do pliku MainPage.xaml i zmień pozostałe powiązania, aby przejść przez Logic
właściwość . Gdy wszystko będzie gotowe, Grid
powinien wyglądać następująco:
<Grid>
<TextBlock Text="{x:Bind Logic.CurrentTime, Mode=OneWay}"
HorizontalAlignment="Right"
Margin="10"/>
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Horizontal"
Visibility="{x:Bind Logic.IsNameNeeded, Mode=OneWay}">
<TextBlock Margin="10"
VerticalAlignment="Center"
Text="Enter your name: "/>
<TextBox Name="tbUserName"
Margin="10"
Width="150"
VerticalAlignment="Center"
Text="{x:Bind Logic.UserName, Mode=TwoWay}"/>
<Button Margin="10"
VerticalAlignment="Center"
Click="{x:Bind Logic.Submit}" >Submit</Button>
</StackPanel>
<TextBlock Text="{x:Bind sys:String.Format('Hello {0}!', tbUserName.Text), Mode=OneWay}"
Visibility="{x:Bind Logic.GetGreetingVisibility(), Mode=OneWay}"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="10"/>
</Grid>
Zwróć uwagę, Button.Click
że nawet zdarzenie może być powiązane z Submit
metodą w MainPageLogic
klasie .
Jeśli skompilujesz teraz projekt, nadal zostanie wyświetlone ostrzeżenie informujące o tym, że MainPage.PropertyChanged
element nigdy nie jest używany.
12. Tidy up MainPage
klasy
Ostrzeżenie występuje, ponieważ nie potrzebujemy już interfejsu INotifyPropertyChanged
MainPage
w klasie . Dlatego usuńmy ją z deklaracji klasy wraz ze zdarzeniem PropertyChanged
.
W końcu cała MainPage
klasa wygląda następująco:
public sealed partial class MainPage : Page
{
public MainPageLogic Logic { get; } = new MainPageLogic();
public MainPage()
{
this.InitializeComponent();
}
}
Jest to tak czyste, jak to się dzieje.
13. Uruchamianie aplikacji
Jeśli wszystko poszło dobrze, powinno być możliwe uruchomienie aplikacji w tym momencie i sprawdzenie, czy działa dokładnie tak, jak wcześniej. Gratulacje!
Podsumowanie
Więc co osiągnęliśmy z tą całą pracą? Chociaż aplikacja działa tak samo jak wcześniej, doszliśmy do skalowalnej, zrównoważonej i testowalnej architektury.
Klasa MainPage
jest teraz bardzo prosta. Zawiera odwołanie do logiki i po prostu odbiera i przekazuje zdarzenie kliknięcia przycisku. Cały przepływ danych między logiką a interfejsem użytkownika odbywa się za pośrednictwem powiązania danych, który jest szybki, niezawodny i sprawdzony.
Klasa MainPageLogic
jest teraz niezależna od interfejsu użytkownika. Nie ma znaczenia, czy zegar jest wyświetlany w innej kontrolce TextBlock
. Przesyłanie formularza może odbywać się na dowolną liczbę sposobów. Te sposoby obejmują kliknięcie przycisku, naciśnięcie klawisza Enter lub algorytm rozpoznawania twarzy wykrywający uśmiech. Formularz można również przesłać przy użyciu automatycznych testów jednostkowych przeznaczonych dla logiki i upewnić się, że działa zgodnie z wymaganiami projektu.
Z tych powodów, a także innych, dobrym rozwiązaniem jest posiadanie tylko funkcji związanych z interfejsem użytkownika w kodzie strony i oddzielenie logiki w innej klasie. Bardziej skomplikowane aplikacje mogą również mieć kontrolkę animacji i inne, konkretne funkcje związane z interfejsem użytkownika. Podczas pracy z bardziej skomplikowanymi aplikacjami doceniasz rozdzielenie interfejsu użytkownika i logiki utworzonej w tej lekcji.
Możesz ponownie użyć ObservableObject
klasy we własnym projekcie. Po odrobinie praktyki przekonasz się, że jest to rzeczywiście szybsze i łatwiejsze podejście do problemów w ten sposób. Możesz też skorzystać z istniejącej, dobrze ugruntowanej biblioteki, takiej jak zestaw narzędzi MVVM Toolkit, który jest zgodny z zasadami poznanymi w tym module.
5. Zmodyfikuj klasę, Clock
aby korzystać z ObservableObject
Zmień podpis Clock
elementu , aby dziedziczył z ObservableObject
elementu zamiast INotifyPropertyChanged
.
public class Clock : ObservableObject
Teraz mamy PropertyChanged
zdarzenie zdefiniowane zarówno w klasie, jak Clock
i w klasie bazowej, co powoduje ostrzeżenie kompilatora. PropertyChanged
Usuń zdarzenie z Clock
klasy .
Aby podnieść PropertyChanged
zdarzenie, utworzyliśmy funkcję wygody w ObservableObject
klasie . Aby go użyć, zastąp _timer.Tick
wiersz następującym:
_timer.Tick += (sender, o) => RaisePropertyChanged(nameof(CurrentTime));
Klasa Clock
stała się już prostsza. Zobaczmy jednak, co możemy zrobić z bardziej złożoną MainWindowDataContext
klasą.
6. Zmodyfikuj klasę, MainWindowDataContext
aby korzystać z ObservableObject
Podobnie jak w przypadku Clock
klasy, ponownie zaczynamy od zmiany deklaracji klasy tak, aby dziedziczyła z ObservableObject
klasy .
public class MainWindowDataContext : ObservableObject
Upewnij się, że usunięto to PropertyChanged
zdarzenie również tutaj.
Przyjrzyj się ustawieniu IsNameNeeded
właściwości . Wygląda to teraz następująco:
set
{
if (value != _isNameNeeded)
{
_isNameNeeded = value;
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs(nameof(IsNameNeeded)));
PropertyChanged?.Invoke(
this, new PropertyChangedEventArgs(nameof(GreetingVisibility)));
}
}
Jest to standardowy INotifyPropertyChanged
wzorzec z wywołaniem dodatkowych PropertyChanged
zdarzeń, jeśli nowa IsNameNeeded
wartość właściwości jest inna.
Jest to dokładnie sytuacja, dla której ObservableObject.Set
została utworzona funkcja. Funkcja Set
zwraca nawet wartość wskazującą bool
, czy stare i nowe wartości właściwości są różne. Powyższy zestaw właściwości można więc uprościć w następujący sposób:
if (Set(ref _isNameNeeded, value))
{
RaisePropertyChanged(nameof(GreetingVisibility));
}
Nieźle!
7. Uruchamianie aplikacji
Jeśli wszystko poszło dobrze, powinno być możliwe uruchomienie aplikacji w tym momencie i sprawdzenie, czy działa dokładnie tak, jak wcześniej. Gratulacje!
Podsumowanie
Więc co osiągnęliśmy z tą całą pracą? Chociaż aplikacja działa tak samo jak wcześniej, doszliśmy do skalowalnej, zrównoważonej i testowalnej architektury.
Klasa MainWindow
jest bardzo prosta. Zawiera odwołanie do logiki i po prostu odbiera i przekazuje zdarzenie kliknięcia przycisku. Cały przepływ danych między logiką a interfejsem użytkownika odbywa się za pośrednictwem powiązania danych, który jest szybki, niezawodny i sprawdzony.
Klasa MainWindowDataContext
jest teraz niezależna od interfejsu użytkownika. Nie ma znaczenia, czy zegar jest wyświetlany w innej kontrolce TextBlock
. Przesyłanie formularza może odbywać się na dowolną liczbę sposobów. Te sposoby obejmują kliknięcie przycisku, naciśnięcie klawisza Enter lub algorytm rozpoznawania twarzy wykrywający uśmiech. Formularz można również przesłać przy użyciu automatycznych testów jednostkowych przeznaczonych dla logiki i upewnić się, że działa zgodnie z wymaganiami projektu.
Z tych powodów, a także innych, dobrym rozwiązaniem jest posiadanie tylko funkcji związanych z interfejsem użytkownika w kodzie okna i oddzielenie logiki w innej klasie. Bardziej złożone aplikacje mogą również mieć kontrolkę animacji i inne, konkretne funkcje związane z interfejsem użytkownika. Podczas pracy z bardziej złożonymi aplikacjami doceniasz rozdzielenie interfejsu użytkownika i logiki utworzonej w tej lekcji.
Możesz ponownie użyć ObservableObject
klasy we własnym projekcie. Po odrobinie praktyki przekonasz się, że jest to rzeczywiście szybsze i łatwiejsze podejście do problemów w ten sposób. Możesz też skorzystać z istniejącej, dobrze ugruntowanej biblioteki, takiej jak zestaw narzędzi MVVM Toolkit, który jest zgodny z zasadami poznanymi w tym module.