Bagikan melalui


Pola peristiwa .NET standar

Sebelumnya

Peristiwa .NET umumnya mengikuti beberapa pola yang diketahui. Standardisasi pada pola-pola ini berarti bahwa pengembang dapat menerapkan pengetahuan tentang pola standar tersebut, yang dapat diterapkan ke program peristiwa .NET apa pun.

Mari kita melalui pola standar ini sehingga Anda memiliki semua pengetahuan yang Anda butuhkan untuk membuat sumber peristiwa standar, dan berlangganan dan memproses peristiwa standar dalam kode Anda.

Tanda tangan delegasi acara

Tanda tangan standar untuk delegasi peristiwa .NET adalah:

void EventRaised(object sender, EventArgs args);

Tanda tangan standar ini memberikan wawasan tentang kapan peristiwa digunakan:

  • Jenis pengembalian tidak berlaku. Peristiwa dapat memiliki nol untuk banyak pendengar. Menaikkan acara akan memberi tahu semua pendengar. Secara umum, pendengar tidak memberikan nilai sebagai respons terhadap peristiwa.
  • Peristiwa menunjukkan pengirim: Tanda tangan peristiwa menyertakan objek yang menaikkan peristiwa. Itu menyediakan mekanisme kepada pendengar untuk berkomunikasi dengan pengirim. Jenis waktu kompilasi sender adalah System.Object, meskipun Anda mungkin tahu jenis yang lebih turunan yang akan selalu benar. Menurut konvensi, gunakan object.
  • Events mengumpulkan informasi tambahan dalam satu struktur: Parameter args adalah jenis yang berasal dari System.EventArgs yang mencakup informasi tambahan yang diperlukan. (Anda akan melihat di bagian berikutnya bahwa konvensi ini tidak lagi diberlakukan.) Jika jenis peristiwa Anda tidak memerlukan argumen lagi, Anda masih harus memberikan kedua argumen. Ada nilai khusus, EventArgs.Empty yang harus Anda gunakan untuk menunjukkan bahwa peristiwa Anda tidak berisi informasi tambahan apa pun.

Mari kita buat kelas yang mencantumkan file dalam direktori, atau subdirektorinya yang mengikuti pola. Komponen ini memunculkan peristiwa untuk setiap file yang ditemukan yang cocok dengan pola.

Menggunakan model peristiwa memberikan beberapa keunggulan desain. Anda dapat membuat beberapa pendengar peristiwa yang melakukan tindakan berbeda saat file yang dicari ditemukan. Menggabungkan pendengar yang berbeda dapat membuat algoritma yang lebih kuat.

Berikut adalah deklarasi argumen peristiwa awal untuk menemukan file yang dicari:

public class FileFoundArgs : EventArgs
{
    public string FoundFile { get; }

    public FileFoundArgs(string fileName) => FoundFile = fileName;
}

Meskipun jenis ini terlihat seperti jenis data yang sederhana dan kecil, Anda harus mengikuti konvensi dan mempertahankannya sebagai jenis referensi (class). Itu berarti objek argumen diteruskan oleh referensi, dan pembaruan apa pun pada data dilihat oleh semua pelanggan. Versi pertama adalah objek yang tidak berubah. Anda sebaiknya membuat properti dalam jenis argumen peristiwa Anda tak berubah. Dengan begitu, satu pelanggan tidak dapat mengubah nilai sebelum pelanggan lain melihatnya. (Ada pengecualian untuk praktik ini, seperti yang Anda lihat nanti.)

Selanjutnya, kita perlu membuat deklarasi peristiwa di kelas FileSearcher. Menggunakan jenis System.EventHandler<TEventArgs> berarti Anda tidak perlu membuat definisi jenis lain. Anda hanya menggunakan spesialisasi generik.

Mari kita isi kelas FileSearcher untuk mencari file yang cocok dengan pola, dan memicu peristiwa yang tepat ketika ditemukan kecocokan.

public class FileSearcher
{
    public event EventHandler<FileFoundArgs>? FileFound;

    public void Search(string directory, string searchPattern)
    {
        foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
        {
            FileFound?.Invoke(this, new FileFoundArgs(file));
        }
    }
}

Menentukan dan menaikkan acara yang mirip dengan bidang

Cara termudah untuk menambahkan peristiwa ke kelas Anda adalah dengan mendeklarasikan peristiwa itu sebagai bidang publik, seperti pada contoh sebelumnya:

public event EventHandler<FileFoundArgs>? FileFound;

Ini terlihat seperti mendeklarasikan bidang publik, yang tampaknya merupakan praktik berorientasi objek yang buruk. Anda ingin melindungi akses data melalui properti, atau metode. Meskipun kode ini mungkin terlihat seperti praktik yang buruk, kode yang dihasilkan oleh pengkompilasi memang membuat pembungkus sehingga objek peristiwa hanya dapat diakses dengan cara yang aman. Satu-satunya operasi yang tersedia pada peristiwa seperti bidang adalah menambahkan dan menghapus handler:

var fileLister = new FileSearcher();
int filesFound = 0;

EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
    Console.WriteLine(eventArgs.FoundFile);
    filesFound++;
};

fileLister.FileFound += onFileFound;
fileLister.FileFound -= onFileFound;

Ada variabel lokal untuk handler. Jika Anda menggunakan isi dari fungsi lambda, remove handler tidak akan berfungsi secara benar. Ini akan menjadi contoh yang berbeda dari delegasi, dan diam-diam tidak melakukan apa-apa.

Kode di luar kelas tidak dapat menaikkan peristiwa, juga tidak dapat melakukan operasi lain.

Dimulai dengan C# 14, peristiwa dapat dinyatakan sebagai anggota parsial. Deklarasi acara parsial harus mencakup deklarasi yang mendefinisikan dan deklarasi pelaksana. Deklarasi yang mendefinisikan harus menggunakan sintaks peristiwa seperti bidang. Deklarasi pelaksana harus mendeklarasikan penanganan add dan remove.

Mengembalikan nilai dari subscriber acara

Versi sederhana Anda berfungsi dengan baik. Mari tambahkan fitur lain: Pembatalan.

Ketika Anda menaikkan peristiwa Found, pendengar harus dapat menghentikan pemrosesan lebih lanjut, jika file ini adalah yang terakhir dicari.

Pengelola acara tidak mengembalikan nilai, jadi Anda perlu menyampaikannya dengan cara lain. Pola peristiwa standar menggunakan objek EventArgs untuk menyertakan bidang yang dapat digunakan untuk mengomunikasikan pembatalan kepada pelanggan acara.

Dua pola berbeda dapat digunakan, berdasarkan semantik kontrak Pembatalan. Dalam kedua kasus, Anda menambahkan bidang boolean ke EventArguments untuk peristiwa file yang ditemukan.

Satu pola akan mengizinkan satu pelanggan membatalkan operasi. Untuk pola ini, bidang baru diinisialisasi ke false. Setiap pelanggan dapat mengubahnya menjadi true. Setelah menaikkan peristiwa untuk semua pelanggan, komponen FileSearcher memeriksa nilai boolean dan mengambil tindakan.

Pola kedua hanya akan membatalkan operasi jika semua pelanggan ingin operasi dibatalkan. Dalam pola ini, bidang baru diinisialisasi untuk menunjukkan bahwa operasi harus dibatalkan, dan setiap pelanggan bisa mengubahnya untuk menunjukkan bahwa operasi harus dilanjutkan. Setelah semua pelanggan memproses peristiwa yang dimunculkan, komponen FileSearcher memeriksa boolean dan mengambil tindakan. Ada satu langkah tambahan dalam pola ini: komponen perlu mengetahui apakah ada pelanggan yang merespons peristiwa tersebut. Jika tidak ada pelanggan, bidang akan menunjukkan pembatalan yang salah.

Mari kita terapkan versi pertama untuk sampel ini. Anda perlu menambahkan bidang boolean bernama CancelRequested ke jenis FileFoundArgs:

public class FileFoundArgs : EventArgs
{
    public string FoundFile { get; }
    public bool CancelRequested { get; set; }

    public FileFoundArgs(string fileName) => FoundFile = fileName;
}

Bidang baru ini secara otomatis diinisialisasi ke false sehingga Anda tidak membatalkan secara tidak sengaja. Satu-satunya perubahan lain pada komponen adalah memeriksa bendera setelah menaikkan acara untuk melihat apakah salah satu pelanggan meminta pembatalan:

private void SearchDirectory(string directory, string searchPattern)
{
    foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
    {
        var args = new FileFoundArgs(file);
        FileFound?.Invoke(this, args);
        if (args.CancelRequested)
            break;
    }
}

Salah satu keuntungan dari pola ini adalah bahwa ini bukan perubahan yang melanggar. Tidak ada pelanggan yang meminta pembatalan sebelumnya, dan masih belum. Tidak ada kode pelanggan yang memerlukan pembaruan kecuali mereka ingin mendukung protokol pembatalan baru.

Mari kita perbarui langganan agar bisa meminta pembatalan begitu menemukan yang dapat dieksekusi pertama:

EventHandler<FileFoundArgs> onFileFound = (sender, eventArgs) =>
{
    Console.WriteLine(eventArgs.FoundFile);
    eventArgs.CancelRequested = true;
};

Menambahkan deklarasi acara lain

Mari kita tambahkan satu fitur lagi, dan tunjukkan idiom bahasa lain untuk peristiwa. Mari kita tambahkan kelebihan beban metode Search yang melintasi semua subdirektori untuk mencari file.

Metode ini bisa menjadi operasi yang panjang dalam direktori dengan banyak subdirektori. Tambahkan event yang dipicu saat setiap pencarian direktori baru dimulai. Acara ini memungkinkan pelanggan untuk melacak kemajuan, dan memperbarui pengguna tentang kemajuan tersebut. Semua sampel yang Anda buat sejauh ini bersifat publik. Mari kita jadikan peristiwa ini sebagai peristiwa internal. Itu berarti Anda juga dapat membuat jenis argumen internal juga.

Anda mulai dengan membuat kelas turunan EventArgs baru untuk melaporkan direktori dan kemajuan baru.

internal class SearchDirectoryArgs : EventArgs
{
    internal string CurrentSearchDirectory { get; }
    internal int TotalDirs { get; }
    internal int CompletedDirs { get; }

    internal SearchDirectoryArgs(string dir, int totalDirs, int completedDirs)
    {
        CurrentSearchDirectory = dir;
        TotalDirs = totalDirs;
        CompletedDirs = completedDirs;
    }
}

Sekali lagi, Anda dapat mengikuti rekomendasi untuk membuat jenis referensi yang tidak dapat diubah untuk argumen peristiwa.

Selanjutnya, tentukan peristiwa. Kali ini, Anda menggunakan sintaks yang berbeda. Selain menggunakan sintaks bidang, Anda dapat secara eksplisit membuat properti peristiwa dengan menambahkan dan menghapus handler. Dalam sampel ini, Anda tidak memerlukan kode tambahan di handler tersebut, tetapi ini menunjukkan bagaimana Anda akan membuatnya.

internal event EventHandler<SearchDirectoryArgs> DirectoryChanged
{
    add { _directoryChanged += value; }
    remove { _directoryChanged -= value; }
}
private EventHandler<SearchDirectoryArgs>? _directoryChanged;

Dalam banyak hal, kode yang Anda tulis di sini mencerminkan kode yang dihasilkan pengkompilasi untuk definisi peristiwa bidang yang Anda lihat sebelumnya. Anda membuat peristiwa menggunakan sintaks yang mirip dengan properti . Perhatikan bahwa pengendali memiliki nama yang berbeda: add dan remove. Aksesor ini dipanggil untuk berlangganan acara, atau berhenti berlangganan dari acara. Perhatikan bahwa Anda juga harus mendeklarasikan bidang dukungan pribadi untuk menyimpan variabel peristiwa. Variabel ini diinisialisasi ke null.

Selanjutnya, mari kita tambahkan kelebihan beban metode Search yang melintasi subdirektori dan meningkatkan kedua peristiwa. Cara termampu adalah dengan menggunakan argumen default untuk menentukan bahwa Anda ingin mencari semua direktori:

public void Search(string directory, string searchPattern, bool searchSubDirs = false)
{
    if (searchSubDirs)
    {
        var allDirectories = Directory.GetDirectories(directory, "*.*", SearchOption.AllDirectories);
        var completedDirs = 0;
        var totalDirs = allDirectories.Length + 1;
        foreach (var dir in allDirectories)
        {
            _directoryChanged?.Invoke(this, new (dir, totalDirs, completedDirs++));
            // Search 'dir' and its subdirectories for files that match the search pattern:
            SearchDirectory(dir, searchPattern);
        }
        // Include the Current Directory:
        _directoryChanged?.Invoke(this, new (directory, totalDirs, completedDirs++));
        SearchDirectory(directory, searchPattern);
    }
    else
    {
        SearchDirectory(directory, searchPattern);
    }
}

private void SearchDirectory(string directory, string searchPattern)
{
    foreach (var file in Directory.EnumerateFiles(directory, searchPattern))
    {
        var args = new FileFoundArgs(file);
        FileFound?.Invoke(this, args);
        if (args.CancelRequested)
            break;
    }
}

Pada titik ini, Anda dapat menjalankan aplikasi yang memanggil kelebihan beban untuk mencari semua subdirektori. Tidak ada pengikut pada peristiwa baru DirectoryChanged, tetapi menggunakan idiom ?.Invoke() memastikan fungsinya dengan benar.

Mari kita tambahkan handler untuk menulis baris kemajuan di jendela konsol.

fileLister.DirectoryChanged += (sender, eventArgs) =>
{
    Console.Write($"Entering '{eventArgs.CurrentSearchDirectory}'.");
    Console.WriteLine($" {eventArgs.CompletedDirs} of {eventArgs.TotalDirs} completed...");
};

Anda melihat pola yang diikuti di seluruh ekosistem .NET. Dengan mempelajari pola dan konvensi ini, Anda menulis C# idiomatik dan .NET dengan cepat.

Lihat juga

Selanjutnya, Anda melihat beberapa perubahan dalam pola ini dalam rilis terbaru .NET.