Bagikan melalui


Pola peristiwa .NET standar

Sebelumnya

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

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

Tanda tangan delegasi peristiwa

Tanda tangan standar untuk delegasi peristiwa .NET adalah:

void EventRaised(object sender, EventArgs args);

Jenis pengembalian dibatalkan. Peristiwa didasarkan pada delegasi dan merupakan delegasi multicast. Itu mendukung beberapa pelanggan untuk sumber peristiwa apa pun. Nilai pengembalian tunggal dari metode tidak diskalakan ke beberapa pelanggan peristiwa. Nilai pengembalian mana yang dilihat sumber peristiwa setelah menaikkan peristiwa? Kemudian di artikel ini Anda akan melihat cara membuat protokol peristiwa yang mendukung pelanggan peristiwa yang melaporkan informasi ke sumber peristiwa.

Daftar argumen berisi dua argumen: pengirim, dan argumen peristiwa. Jenis waktu kompilasi sender adalah System.Object, meskipun Anda mungkin tahu jenis yang lebih turunan yang akan selalu benar. Menurut konvensi, gunakan object.

Argumen kedua biasanya adalah jenis yang diturunkan dari System.EventArgs. (Anda akan melihat di bagian berikutnya bahwa konvensi ini tidak lagi diberlakukan.) Jika jenis peristiwa Anda tidak memerlukan argumen tambahan, Anda masih akan 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 khusus data, kecil, Anda harus mengikuti konvensi dan menjadikannya jenis referensi (class). Itu berarti objek argumen akan diteruskan dengan referensi, dan setiap pembaruan data akan dilihat oleh semua pelanggan. Versi pertama adalah objek yang tidak berubah. Anda harus lebih memilih untuk membuat properti dalam jenis argumen peristiwa Anda tidak berubah. Dengan begitu, satu pelanggan tidak dapat mengubah nilai sebelum pelanggan lain melihatnya. (Ada pengecualian untuk ini, seperti yang akan Anda lihat di bawah ini.)

Selanjutnya, kita perlu membuat deklarasi peristiwa di kelas FileSearcher. Memanfaatkan jenis EventHandler<T> berarti Anda tidak perlu membuat definisi jenis lain. Anda cukup menggunakan spesialisasi generik.

Mari kita isi kelas FileSearcher untuk mencari file yang cocok dengan pola, dan menaikkan peristiwa yang benar saat kecocokan ditemukan.

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

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

Menentukan dan menaikkan peristiwa seperti 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 ini mungkin terlihat seperti praktik yang buruk, kode yang dihasilkan oleh kompiler memang membuat pembungkus sehingga objek peristiwa hanya dapat diakses dengan cara yang aman. Satu-satunya operasi yang tersedia pada peristiwa mirip-bidang adalah add handler:

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

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

fileLister.FileFound += onFileFound;

dan remove handler:

fileLister.FileFound -= onFileFound;

Perhatikan bahwa ada variabel lokal untuk yang menangani. Jika Anda menggunakan badan lambda, penghapusan tidak akan berfungsi dengan benar. Ini akan menjadi contoh yang berbeda dari delegasi, dan diam-diam tidak melakukan apa-apa.

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

Mengembalikan nilai dari pelanggan peristiwa

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

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

Penanganan aktivitas tidak mengembalikan nilai, jadi Anda perlu mengomunikasikannya dengan cara lain. Pola peristiwa standar menggunakan EventArgs objek untuk menyertakan bidang yang dapat digunakan pelanggan peristiwa untuk berkomunikasi membatalkan.

Dua pola yang berbeda dapat digunakan, berdasarkan semantik kontrak Batal. Dalam kedua kasus, Anda akan 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 semua pelanggan melihat peristiwa dinaikkan, 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 operasi harus dibatalkan, dan setiap pelanggan dapat mengubahnya untuk menunjukkan operasi harus dilanjutkan. Setelah semua pelanggan melihat peristiwa dinaikkan, komponen FileSearcher memeriksa boolean dan mengambil tindakan. Ada satu langkah ekstra dalam pola ini: komponen perlu tahu apakah ada pelanggan yang melihat 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, nilai default untuk Boolean bidang, sehingga Anda tidak membatalkan secara tidak sengaja. Satu-satunya perubahan lain pada komponen ini adalah memeriksa bendera setelah menaikkan peristiwa untuk melihat apakah ada pelanggan yang meminta pembatalan:

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

private FileFoundArgs RaiseFileFound(string file)
{
    var args = new FileFoundArgs(file);
    FileFound?.Invoke(this, args);
    return args;
}

Salah satu keuntungan dari pola ini adalah bahwa itu bukan perubahan yang melanggar. Tidak ada pelanggan yang meminta pembatalan sebelumnya, dan mereka masih belum. Tak satu pun dari kode pelanggan perlu diperbarui kecuali ingin mendukung protokol pembatalan baru. Ini penggabungan yang sangat longgar.

Mari kita perbarui pelanggan sehingga meminta pembatalan setelah 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.

Ini bisa menjadi operasi yang panjang dalam direktori dengan banyak sub-direktori. Mari tambahkan peristiwa yang akan dinaikkan saat setiap pencarian direktori baru dimulai. Hal ini memungkinkan pelanggan untuk melacak kemajuan, dan memperbarui pengguna untuk maju. Semua sampel yang Anda buat sejauh ini bersifat publik. Mari kita jadikan yang satu ini sebagai peristiwa internal. Itu berarti Anda juga dapat membuat jenis yang digunakan untuk argumen internal juga.

Anda akan 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 akan menggunakan sintaks yang berbeda. Selain menggunakan sintaks bidang, Anda dapat secara eksplisit membuat properti, dengan menambahkan dan menghapus penanganan. Dalam sampel ini, Anda tidak memerlukan kode tambahan di penanganan 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 kompiler untuk definisi peristiwa bidang yang telah Anda lihat sebelumnya. Anda membuat peristiwa menggunakan sintaks yang sangat mirip dengan yang digunakan untuk properti. Perhatikan bahwa penanganan memiliki nama yang berbeda: add dan remove. Ini dipanggil untuk berlangganan peristiwa, atau berhenti berlangganan dari peristiwa tersebut. Perhatikan bahwa Anda juga harus mendeklarasikan bidang dukungan pribadi untuk menyimpan variabel peristiwa. Ini diinisialisasi ke null.

Selanjutnya, mari kita tambahkan kelebihan beban metode Search yang melintasi subdirektori dan meningkatkan kedua peristiwa. Cara termudah untuk mencapai hal ini 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)
        {
            RaiseSearchDirectoryChanged(dir, totalDirs, completedDirs++);
            // Search 'dir' and its subdirectories for files that match the search pattern:
            SearchDirectory(dir, searchPattern);
        }
        // Include the Current Directory:
        RaiseSearchDirectoryChanged(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))
    {
        FileFoundArgs args = RaiseFileFound(file);
        if (args.CancelRequested)
        {
            break;
        }
    }
}

private void RaiseSearchDirectoryChanged(
    string directory, int totalDirs, int completedDirs) =>
    _directoryChanged?.Invoke(
        this,
            new SearchDirectoryArgs(directory, totalDirs, completedDirs));

private FileFoundArgs RaiseFileFound(string file)
{
    var args = new FileFoundArgs(file);
    FileFound?.Invoke(this, args);
    return args;
}

Pada titik ini, Anda dapat menjalankan aplikasi yang memanggil kelebihan beban untuk mencari semua sub-direktori. Tidak ada pelanggan pada peristiwa baru DirectoryChanged, tetapi menggunakan idiom ?.Invoke() memastikan bahwa ini berfungsi dengan benar.

Mari tambahkan penanganan untuk menulis baris yang menunjukkan kemajuan di jendela konsol.

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

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

Lihat juga

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