Mengimplementasikan INotifyPropertyChanged dengan cara yang mudah

Selesai

Jika Anda telah mengikuti pelajaran sebelumnya, Anda mungkin berpikir bahwa menerapkan pengikatan data adalah upaya yang terlalu besar. Mengapa melalui semua masalah penerapan INotifyPropertyChanged, menembakkan peristiwa ke kiri dan kanan, ketika Anda hanya dapat menggunakan TimeTextBlock.Text = DateTime.Now.ToLongTime() untuk menampilkan waktu? Dan itu benar, dalam kasus sederhana ini, pengikatan data memang terlihat seperti overkill.

Namun, pengikatan data mampu lebih banyak lagi. Ini dapat mentransfer data di kedua arah antara UI dan kode, menampilkan daftar item, dan mendukung pengeditan data. Semua ini dengan arsitektur yang menawarkan pemisahan data yang bersih dari logika aplikasi Anda, dan presentasi data.

Tetapi bagaimana kita dapat mengurangi jumlah kode yang harus ditulis pengembang? Tidak ada yang ingin memasukkan sepuluh baris kode untuk setiap properti yang perlu mereka nyatakan. Untungnya, kita dapat mengekstrak fungsionalitas umum dan mengurangi setter properti menjadi satu baris kode. Pelajaran ini menunjukkan caranya.

Tujuannya

Tujuan kami adalah memindahkan semua pipa untuk mengimplementasikan INotifyPropertyChanged antarmuka ke kelas terpisah, untuk menyederhanakan pembuatan properti yang dapat memberi tahu UI ketika berubah. Sebagai pengingat, berikut adalah kode yang ingin kami sederhanakan:

private bool _isNameNeeded = true;

public bool IsNameNeeded
{
    get { return _isNameNeeded; }
    set
    {
        if (value != _isNameNeeded)
        {
            _isNameNeeded = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsNameNeeded)));
        }
    }
}

Properti otomatis (seperti public bool IsNameNeeded { get; set;}) tidak dapat digunakan di sini, karena kita perlu melakukan sesuatu di setter. Jadi, tidak banyak yang harus dilakukan tentang bidang backing, baris deklarasi properti. Dengan menggunakan fitur C# modern, kita dapat mengubah getter menjadi get => _isNameNeeded;, tetapi itu hanya menghemat beberapa penekanan tombol. Jadi, kita perlu memfokuskan perhatian kita pada setter properti. Bisakah kita mengubahnya menjadi satu baris?

Kelas ObservableObject

Kita dapat membuat kelas dasar baru: ObservableObject. Ini disebut dapat diamati karena dapat diamati oleh UI, dengan menggunakan INotifyPropertyChanged antarmuka. Data dan logika dihosting di kelas yang mewarisinya, dan UI juga terikat pada instans kelas yang diwariskan ini.

1. Buat ObservableObject kelas

Mari kita buat kelas baru yang disebut ObservableObject. DatabindingSample Klik kanan proyek di Penjelajah Solusi, pilih Tambahkan / Kelas, dan masukkan ObservableObject sebagai nama kelas. Pilih Tambahkan untuk membuat kelas.

1. Buat ObservableObject kelas

Mari kita buat kelas baru yang disebut ObservableObject. Klik kanan pada DatabindingSampleWPF proyek di Penjelajah Solusi, pilih Tambahkan / Kelas dan masukkan ObservableObject sebagai nama kelas. Pilih Tambahkan untuk membuat kelas.

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

2. Menerapkan INotifyPropertyChanged antarmuka

Selanjutnya, kita harus mengimplementasikan INotifyPropertyChanged antarmuka, dan membuat kelas kita publik. Ubah tanda tangan kelas sehingga terlihat seperti ini:

public class ObservableObject : INotifyPropertyChanged

Visual Studio menunjukkan bahwa ada beberapa masalah dengan INotifyPropertyChanged. Ini berada di namespace layanan yang tidak direferensikan. Mari kita tambahkan seperti yang ditunjukkan di sini.

using System.ComponentModel;

Selanjutnya, kita harus mengimplementasikan antarmuka. Tambahkan baris ini di dalam isi kelas.

public event PropertyChangedEventHandler? PropertyChanged;

3. Metode RaisePropertyChanged

Dalam pelajaran sebelumnya, kami sering menaikkan PropertyChangedEvent dalam kode kami, bahkan di luar setter properti. Meskipun C# modern dan operator null-conditional atau (?.) memungkinkan kami untuk melakukan ini dalam satu baris, kami masih dapat menyederhanakan dengan membuat fungsi kenyamanan seperti ini:

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

Jadi sekarang, di kelas yang mewarisi dari ObservableObject, yang harus kita lakukan untuk meningkatkan PropertyChanged peristiwa adalah sebagai berikut:

RaisePropertyChanged(nameof(MyProperty));

4. Metode Set<T>

Tetapi apa yang dapat kita lakukan tentang pola setter yang memeriksa apakah nilainya sama seperti itu, menetapkan nilai jika tidak, dan menaikkan PropertyChanged peristiwa? Idealnya, kami ingin mengubahnya menjadi satu baris, seperti ini:

private bool _isNameNeeded = true;

public bool IsNameNeeded
{
    get { return _isNameNeeded; }
    set { Set(ref _isNameNeeded, value); }  // Just one line!
}

Ini tidak bisa benar-benar menjadi lebih sederhana dari itu. Kami memanggil fungsi, meneruskan referensi ke bidang dukungan properti, dan mengatur nilai baru. Jadi, seperti apa metode ini Set ?

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;
}

Salin kode sebelumnya ke dalam isi ObservableObject kelas. Untuk [CallerMemberName], Anda juga perlu menambahkan baris berikut ke bagian atas file:

using System.Runtime.CompilerServices;

Ada banyak C# canggih dan sihir kompilator terjadi di sini. Mari kita lihat lebih dekat.

Set<T> adalah metode generik, membantu pengkompilasi untuk memastikan bahwa bidang backing dan nilainya berjenis yang sama. Parameter ketiga metode, propertyName, dihiasi oleh [CallerMemberName] atribut . Jika kita tidak menentukan propertyName kapan memanggil metode , itu akan mengambil nama anggota panggilan, dan menempatkannya di sana selama waktu kompilasi. Jadi, jika kita memanggil Set dari setter IsNameNeeded metode , pengkompilasi menempatkan string literal, "IsNameNeeded", sebagai parameter ketiga. Tidak perlu melakukan hardcode string atau bahkan menggunakan nameof()!

Selanjutnya, Set metode memanggil EqualityComparer<T>.Default.Equals untuk membandingkan nilai bidang saat ini dan baru. Jika nilai lama dan baru sama, Set metode mengembalikan false. Jika tidak, bidang backing diatur ke nilai baru, dan PropertyChanged peristiwa dinaikkan sebelum mengembalikan true. Anda dapat menggunakan nilai Set pengembalian metode untuk menentukan apakah nilai telah berubah.

Dengan kelas yang ObservableObject diimplementasikan, mari kita lihat bagaimana kita dapat menggunakannya di aplikasi kita!

5. Buat MainPageLogic kelas

Sebelumnya dalam pelajaran ini, kami memindahkan semua data dan logika kami keluar dari MainPage kelas, dan ke kelas yang mewarisi dari ObservableObject.

Mari kita buat kelas baru, yang disebut MainPageLogic. DatabindingSample Klik kanan proyek di Penjelajah Solusi, pilih Tambahkan / Kelas, dan masukkan MainPageLogic sebagai nama kelas. Pilih Tambahkan untuk membuat kelas.

Ubah tanda tangan kelas, sehingga bernilai publik dan mewarisi dari ObservableObject.

public class MainPageLogic : ObservableObject
{
}

6. Pindahkan fitur jam ke MainPageLogic kelas

Kode untuk fitur jam terdiri dari tiga bagian: _timer bidang , menyiapkan DispatcherTimer di konstruktor, dan CurrentTime properti . Berikut adalah kode seperti yang telah kami tinggalkan dalam pelajaran kedua:

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();

Mari kita pindahkan semua kode yang ada hubungannya dengan ke _timerMainPageLogic kelas . Baris dalam konstruktor (kecuali untuk this.InitializeComponent() panggilan) harus dipindahkan ke MainPageLogickonstruktor . Dari kode sebelumnya, semua yang harus dibiarkan adalah MainPageInitializeComponent panggilan di konstruktor.

public MainPage()
{
    this.InitializeComponent();
}

Untuk saat ini, hanya sentuh bagian kode ini. Kita akan kembali ke MainPage sisa kode kelas' segera.

Setelah pemindahan MainPageLogic , kelas terlihat seperti ini:

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();
}

Ingat, kita memiliki fungsi kenyamanan untuk meningkatkan PropertyChanged peristiwa. Mari kita gunakan itu di handler _timer.Tick .

_timer.Tick += (sender, o) => RaisePropertyChanged(nameof(CurrentTime));

7. Ubah XAML untuk menggunakan MainPageLogic

Jika Anda mencoba mengkompilasi proyek sekarang, Anda akan mendapatkan kesalahan yang mengatakan bahwa "Properti 'CurrentTime' tidak dapat ditemukan pada jenis 'MainPage'" di MainPage.xaml. Dan tentu saja, MainPage kelas tidak lagi memiliki CurrentTime properti. Sudah dipindahkan ke MainPageLogic kelas. Untuk memperbaikinya, kita akan membuat properti yang disebut Logic di MainPage kelas . Ini akan berjenis MainPageLogic, dan kita akan melakukan semua pengikatan melalui ini.

Tambahkan yang berikut ini ke MainPage kelas :

public MainPageLogic Logic { get; } = new MainPageLogic();

Selanjutnya, di MainPage.xaml, temukan TextBlock yang menampilkan jam.

<TextBlock Text="{x:Bind CurrentTime, Mode=OneWay}"
           HorizontalAlignment="Right"
           Margin="10"/>

Dan ubah pengikatan dengan menambahkannya Logic. .

<TextBlock Text="{x:Bind Logic.CurrentTime, Mode=OneWay}"
           HorizontalAlignment="Right"
           Margin="10"/>

Sekarang aplikasi mengkompilasi, dan jika Anda menjalankannya, jam berdetak sebagaimana mestinya. Bagus!

8. Pindahkan sisa logika

Mari kita mengambil kecepatan. Pindahkan kode lainnya di kelas ke MainPageMainPageLogic. Semua yang harus dibiarkan adalah Logic properti, konstruktor, dan PropertyChanged peristiwa.

9. Sederhanakan IsNameNeeded

Di MainPageLogic.cs, ganti IsNameNeeded setter properti dengan panggilan ke metode baru Set kami.

public bool IsNameNeeded
{
    get { return _isNameNeeded; }
    set { Set(ref _isNameNeeded, value); }
}

10. Perbaiki OnSubmitClicked metode

Pada tingkat logika, kami tidak lagi peduli tentang tombol klik pengirim peristiwa atau arg peristiwa. Ini juga merupakan praktik yang baik untuk mempertimbangkan kembali nama metode. Kami tidak lagi melakukan klik tombol, kami mengirimkan logika. Jadi, mari kita ganti nama OnSubmitClicked metode menjadi Submit, jadikan publik, dan hapus parameter.

Di dalam metode , ada cara lama kita untuk meningkatkan PropertyChanged peristiwa . Ganti dengan panggilan ke ObservableObject.RaisePropertyChanged. Pada akhirnya, seluruh metode akan terlihat seperti ini:

public void Submit()
{
    if (string.IsNullOrEmpty(UserName))
    {
        return;
    }

    IsNameNeeded = false;
    RaisePropertyChanged(nameof(GetGreetingVisibility));
}

11. Ubah XAML untuk merujuk ke Logic

Selanjutnya, kembali ke MainPage.xaml, dan ubah pengikatan yang tersisa untuk melalui Logic properti . Setelah semua selesai, Grid akan terlihat seperti ini:

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

Perhatikan bagaimana bahkan Button.Click peristiwa dapat terikat ke Submit metode di MainPageLogic kelas .

Jika Anda mengkompilasi proyek sekarang, Anda masih mendapatkan peringatan yang mengatakan bahwa MainPage.PropertyChanged tidak pernah digunakan.

12. Rapikan MainPage kelas

Peringatan terjadi karena kita tidak lagi membutuhkan INotifyPropertyChanged antarmuka pada MainPage kelas . Jadi, mari kita hapus dari deklarasi kelas, bersama dengan acara.PropertyChanged

Pada akhirnya, seluruh MainPage kelas terlihat seperti ini:

public sealed partial class MainPage : Page
{
    public MainPageLogic Logic { get; } = new MainPageLogic();

    public MainPage()
    {
        this.InitializeComponent();
    }

}

Ini sebersih yang didapatkan.

13. Jalankan aplikasi

Jika semua berjalan dengan baik, Anda harus dapat menjalankan aplikasi pada saat ini, dan memverifikasi bahwa aplikasi berfungsi persis seperti sebelumnya. Selamat!

Ringkasan

Jadi, apa yang kita capai dengan semua pekerjaan ini? Meskipun aplikasi berfungsi sama seperti sebelumnya, kami telah tiba pada arsitektur yang dapat diskalakan, berkelanjutan, dan dapat diuji.

Kelasnya MainPage sekarang sangat sederhana. Ini berisi referensi ke logika, dan hanya menerima dan meneruskan peristiwa klik tombol. Semua aliran data antara logika dan UI terjadi melalui pengikatan data, yang cepat, kuat, dan terbukti.

Kelas MainPageLogic sekarang UI-agnostic. Tidak masalah apakah jam ditampilkan dalam TextBlock atau beberapa kontrol lainnya. Pengiriman formulir dapat terjadi dengan sejumlah cara. Cara-cara ini termasuk klik tombol, tekan tombol Enter, atau algoritma pengenalan wajah yang mendeteksi senyum. Formulir juga dapat dikirimkan dengan menggunakan pengujian unit otomatis yang menargetkan logika dan memastikannya berfungsi sesuai dengan persyaratan proyek.

Untuk alasan ini, serta yang lain, adalah praktik yang baik untuk hanya memiliki fitur terkait UI di codebehind halaman, dan memisahkan logika di kelas yang berbeda. Aplikasi yang lebih rumit mungkin juga memiliki kontrol animasi dan fitur terkait UI konkret lainnya. Saat bekerja dengan aplikasi yang lebih rumit, Anda akan menghargai pemisahan UI dan logika yang telah kami buat dalam pelajaran ini.

Anda dapat menggunakan ObservableObject kembali kelas dalam proyek Anda sendiri. Setelah sedikit latihan, Anda akan menemukan bahwa itu sebenarnya lebih cepat dan lebih mudah untuk mendekati masalah dengan cara ini. Atau manfaatkan pustaka yang ada dan mapan, seperti MVVM Toolkit, yang mengikuti dan membangun berdasarkan prinsip yang Anda pelajari dalam modul ini.

5. Memodifikasi Clock kelas untuk memanfaatkan ObservableObject

Ubah tanda tangan Clock, sehingga mewarisi dari ObservableObject bukan INotifyPropertyChanged.

public class Clock : ObservableObject

Sekarang kita memiliki peristiwa yang PropertyChanged ditentukan di Clock kelas dan kelas dasarnya, yang menghasilkan peringatan kompilator. PropertyChanged Hapus peristiwa dari Clock kelas .

Untuk meningkatkan PropertyChanged acara, kami telah membuat fungsi kenyamanan di ObservableObject kelas . Untuk menggunakannya, ganti _timer.Tick baris dengan ini:

_timer.Tick += (sender, o) => RaisePropertyChanged(nameof(CurrentTime));

Kelas sudah Clock menjadi lebih sederhana. Tapi mari kita lihat apa yang bisa kita lakukan dengan kelas yang lebih kompleks MainWindowDataContext .

6. Memodifikasi MainWindowDataContext kelas untuk memanfaatkan ObservableObject

Seperti halnya Clock kelas, kita kembali memulai dengan mengubah deklarasi kelas sehingga mewarisi dari ObservableObject.

public class MainWindowDataContext : ObservableObject

Pastikan Anda menghapus peristiwa di PropertyChanged sini juga.

Lihatlah setter IsNameNeeded properti. Inilah yang terlihat sekarang:

set
{
    if (value != _isNameNeeded)
    {
        _isNameNeeded = value;
        PropertyChanged?.Invoke(
            this, new PropertyChangedEventArgs(nameof(IsNameNeeded)));
        PropertyChanged?.Invoke(
            this, new PropertyChangedEventArgs(nameof(GreetingVisibility)));
    }
}

Ini adalah pola standar INotifyPropertyChanged , dengan pemanggilan peristiwa tambahan PropertyChanged jika nilai properti baru IsNameNeeded berbeda.

Ini persis situasi ObservableObject.Set fungsi dibuat untuk. Fungsi ini Set bahkan mengembalikan nilai yang bool menunjukkan apakah nilai lama dan baru properti berbeda. Jadi, setter properti di atas dapat disederhanakan seperti ini:

if (Set(ref _isNameNeeded, value))
{
    RaisePropertyChanged(nameof(GreetingVisibility));
}

Tidak buruk!

7. Jalankan aplikasi

Jika semua berjalan dengan baik, Anda harus dapat menjalankan aplikasi pada saat ini, dan memverifikasi bahwa aplikasi berfungsi persis seperti sebelumnya. Selamat!

Ringkasan

Jadi, apa yang kita capai dengan semua pekerjaan ini? Meskipun aplikasi berfungsi sama seperti sebelumnya, kami telah tiba pada arsitektur yang dapat diskalakan, berkelanjutan, dan dapat diuji.

Kelas MainWindow ini sangat sederhana. Ini berisi referensi ke logika, dan hanya menerima dan meneruskan peristiwa klik tombol. Semua aliran data antara logika dan UI terjadi melalui pengikatan data, yang cepat, kuat, dan terbukti.

Kelas MainWindowDataContext sekarang UI-agnostic. Tidak masalah apakah jam ditampilkan dalam TextBlock atau beberapa kontrol lainnya. Pengiriman formulir dapat terjadi dengan sejumlah cara. Cara-cara ini termasuk klik tombol, tekan tombol Enter , atau algoritma pengenalan wajah yang mendeteksi senyum. Formulir juga dapat dikirimkan dengan menggunakan pengujian unit otomatis yang menargetkan logika dan memastikannya berfungsi sesuai dengan persyaratan proyek.

Untuk alasan ini, serta yang lain, adalah praktik yang baik untuk hanya memiliki fitur terkait UI di kode jendela di belakang, dan memisahkan logika di kelas yang berbeda. Aplikasi yang lebih kompleks mungkin juga memiliki kontrol animasi dan fitur terkait UI konkret lainnya. Saat bekerja dengan aplikasi yang lebih kompleks, Anda akan menghargai pemisahan UI dan logika yang telah kami buat dalam pelajaran ini.

Anda dapat menggunakan ObservableObject kembali kelas dalam proyek Anda sendiri. Setelah sedikit latihan, Anda akan menemukan bahwa itu sebenarnya lebih cepat dan lebih mudah untuk mendekati masalah dengan cara ini. Atau manfaatkan pustaka yang ada dan mapan, seperti MVVM Toolkit, yang mengikuti dan membangun berdasarkan prinsip yang Anda pelajari dalam modul ini.