Implementowanie metody INotifyPropertyChanged w prosty sposób

Ukończone

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ę.

Screenshot of Visual Studio showing the Add New Item dialog with a Visual C# class type selected.

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 ObservableObjectklasy , 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 falsewartość . Jeśli nie, pole kopii zapasowej jest ustawione na nową wartość, a PropertyChanged zdarzenie jest zgłaszane przed zwróceniem truewartoś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 ObservableObjectklasy .

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ą _timerMainPageLogic do klasy . Wiersze w konstruktorze (z wyjątkiem this.InitializeComponent() wywołania) powinny zostać przeniesione do MainPageLogickonstruktora . 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 Submitpubliczną 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 INotifyPropertyChangedMainPage 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 Clockelementu , 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 ObservableObjectklasy .

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.