Mengimplementasikan INotifyPropertyChanged dengan cara yang mudah

Selesai

Jika Anda telah mengikuti pelajaran sebelumnya, Anda mungkin berpikir bahwa pengikatan data terlalu merepotkan. Mengapa melalui semua masalah dalam menerapkan , menembakkan INotifyPropertyChangedperistiwa ke kiri dan kanan, ketika Anda cukup 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 jauh lebih banyak. Ini dapat mentransfer data ke kedua arah antara UI dan kode, menampilkan daftar item, dan mendukung pengeditan data. Semua ini dengan arsitektur yang menawarkan pemisahan data yang bersih yang bekerja dengan logika aplikasi Anda, dan presentasi data.

Tetapi bagaimana kita dapat mengurangi jumlah kode yang harus ditulis pengembang? Tidak ada yang ingin mengetik 10 baris kode hanya untuk mendeklarasikan properti sederhana. Untungnya, kita dapat mengekstrak fungsionalitas umum, dan menurunkan setter properti hanya ke satu baris. Pelajaran ini menunjukkan caranya.

Tujuan

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 dengan instans kelas yang diwariskan ini.

1. Buat ObservableObject kelas

Mari kita buat kelas baru yang disebut ObservableObject. DatabindingSample Klik kanan proyek di Průzkumník řešení, 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 proyek DatabindingSampleWPF di Průzkumník řešení, pilih Tambahkan/ Kelas dan masukkan ObservableObject sebagai nama kelas. Pilih Tambahkan untuk membuat kelas.

Cuplikan layar Visual Studio memperlihatkan dialog Tambahkan Item Baru dengan jenis kelas Visual C# dipilih.

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 mengangkat PropertyChangedEvent dalam kode kami, bahkan di luar setter properti. Meskipun C# modern dan operator kondisi null atau (?.) memungkinkan kami 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 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 canggih C# dan compiler magic terjadi di sini. Mari kita lihat lebih dekat.

Set<T> adalah metode generik, membantu pengkompilasi untuk memastikan bahwa bidang dukungan dan nilai memiliki jenis 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 hardcode string atau bahkan menggunakan nameof()!

Selanjutnya, metode ini Set 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 Průzkumník řešení, pilih Tambahkan/ Kelas, dan masukkan MainPageLogic sebagai nama kelas. Pilih Tambahkan untuk membuat kelas.

Ubah tanda tangan kelas, sehingga 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 ditinggalkan adalah MainPageInitializeComponent panggilan di konstruktor.

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

Untuk saat ini, hanya sentuh bagian kode ini. Kita akan kembali ke sisa MainPage 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 menaikkan PropertyChanged acara. 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 cukup yakin, 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 menjadi jenis MainPageLogic, dan kita akan melakukan semua pengikatan kita 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 dikompilasi, dan jika Anda menjalankannya, jam berdetak sebagaimana mestinya. Bagus!

8. Pindahkan sisa logika

Mari kita mengambil kecepatan. Pindahkan sisa kode di kelas ke MainPageMainPageLogic. Yang harus ditinggalkan Logic adalah properti, konstruktor, dan PropertyChanged peristiwa.

9. Sederhanakan IsNameNeeded

Di MainPageLogic.cs, ganti setter IsNameNeeded 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 pengirim peristiwa klik tombol atau argumen 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 . Ketika 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 peristiwa bahkan Button.Click dapat terikat dengan 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 memerlukan 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 semuanya 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 ini berfungsi sama seperti sebelumnya, kami telah tiba di arsitektur yang dapat diskalakan, berkelanjutan, dan dapat diuji.

Kelas 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 adalah 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 sebenarnya lebih cepat dan lebih mudah untuk mendekati masalah dengan cara ini. Atau manfaatkan pustaka yang sudah ada dan mapan, seperti MVVM Toolkit, yang mengikuti dan membangun prinsip-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 didefinisikan di Clock kelas dan kelas dasarnya, yang menghasilkan peringatan kompilator. PropertyChanged Hapus peristiwa dari Clock kelas .

Untuk menaikkan 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 dapat kita lakukan dengan kelas yang lebih kompleks MainWindowDataContext .

6. Memodifikasi MainWindowDataContext kelas untuk memanfaatkan ObservableObject

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

public class MainWindowDataContext : ObservableObject

Pastikan Anda juga menghapus acara di PropertyChanged sini.

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 fungsi ObservableObject.Set dibuat untuk. Fungsi ini Set bahkan mengembalikan nilai yang bool menunjukkan apakah nilai properti lama dan baru berbeda. Jadi, setter properti di atas dapat disederhanakan seperti ini:

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

Tidak buruk!

7. Jalankan aplikasi

Jika semuanya 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 ini berfungsi sama seperti sebelumnya, kami telah tiba di 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 adalah 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 sebenarnya lebih cepat dan lebih mudah untuk mendekati masalah dengan cara ini. Atau manfaatkan pustaka yang sudah ada dan mapan, seperti MVVM Toolkit, yang mengikuti dan membangun prinsip-prinsip yang Anda pelajari dalam modul ini.