Memahami C# Event: Konsep, Cara Kerja, EventHandler, dan Observer Pattern
7 Agustus 2026
•
40 menit baca
•
13 pembaca

Pelajari cara kerja Event di C# dari konsep dasar hingga implementasi nyata. Mulai dari hubungan Event dengan Delegate, mekanisme Publisher–Subscriber, EventHandler, EventArgs, hingga Observer Design Pattern lengkap dengan contoh kasus dan best practices yang digunakan dalam pengembangan aplikasi .NET.
Bagian 1: Pendahuluan
Coba perhatikan beberapa hal sederhana yang terjadi setiap hari. Saat tombol keyboard ditekan, huruf langsung muncul di layar. Ketika tombol mouse diklik, sebuah aksi langsung dijalankan, mulai dari mengubah warna tombol, menutup jendela, hingga menyimpan data. Ponsel bergetar begitu ada pesan WhatsApp yang masuk. Aplikasi e-commerce mengirim notifikasi setelah pembayaran berhasil diproses. Progress bar upload berubah menjadi tanda centang hijau ketika proses pengiriman file selesai.
Semua contoh tersebut memiliki pola yang sama. Ada sebuah kejadian, lalu ada bagian lain dari sistem yang perlu mengetahui bahwa kejadian tersebut baru saja terjadi.
Sekarang bayangkan jika aplikasi WhatsApp harus memeriksa server setiap beberapa milidetik hanya untuk memastikan apakah ada pesan baru atau tidak. Pertanyaan yang sama dikirim berulang-ulang: "Sudah ada pesan baru?"
Pendekatan seperti ini memang memungkinkan, tetapi kurang efisien. Selain menghabiskan bandwidth dan baterai, respons yang diberikan juga bergantung pada seberapa sering proses pengecekan dilakukan. Jika pesan datang tepat setelah proses pengecekan selesai, aplikasi harus menunggu hingga pengecekan berikutnya.
Teknik semacam ini dikenal dengan istilah polling, yaitu memeriksa suatu kondisi secara berkala dalam interval waktu tertentu. Polling masih digunakan pada beberapa kasus, tetapi bukan pilihan yang paling efisien ketika sebuah sistem hanya perlu merespons saat suatu kejadian benar-benar terjadi.
Pendekatan yang lebih baik adalah membalik cara berpikirnya. Alih-alih terus bertanya apakah ada perubahan, pihak yang mengetahui adanya perubahan langsung memberi tahu pihak-pihak yang berkepentingan ketika perubahan tersebut benar-benar terjadi. Tidak ada proses pengecekan berulang, tidak ada waktu yang terbuang untuk menunggu giliran memeriksa.
Dari sinilah muncul sebuah pertanyaan penting.
Bagaimana sebuah object dapat memberi tahu object lain bahwa suatu kejadian baru saja terjadi, tanpa harus terus-menerus diperiksa?
Di C#, mekanisme tersebut diwujudkan melalui Event.
Event memungkinkan sebuah object, misalnya sebuah Button, mengumumkan bahwa suatu kejadian telah terjadi, seperti saat tombol diklik. Object tersebut tidak perlu mengetahui siapa saja yang ingin menerima informasi tersebut. Selama ada object lain yang telah mendaftarkan diri sebagai pendengar, semuanya akan menerima pemberitahuan secara otomatis ketika kejadian itu benar-benar terjadi.
Menariknya, Event bukanlah fitur yang berdiri sendiri. Di balik layar, Event dibangun di atas konsep Delegate yang menjadi fondasi mekanisme pemanggilan metode secara tidak langsung. Cara komunikasi seperti ini juga merupakan implementasi dari salah satu pola desain yang sangat populer dalam pengembangan perangkat lunak, yaitu Observer Design Pattern.
Artikel ini akan membahas bagaimana Event bekerja dari dasar, alasan mengapa desainnya dibuat seperti itu, serta hubungan erat antara Event, Delegate, dan Observer Design Pattern. Setelah memahami konsep-konsep tersebut, cara kerja Event di C# akan terasa jauh lebih masuk akal dibanding sekadar menghafal sintaksnya.
Bagian 2: Apa Itu Event?
Sebelum melihat sintaks di C#, ada baiknya memahami terlebih dahulu konsep dasar di balik Event. Secara sederhana, Event adalah mekanisme yang memungkinkan sebuah object memberi tahu object lain bahwa suatu kejadian telah terjadi, tanpa harus mengetahui siapa saja yang menerima pemberitahuan tersebut. Kalimat tersebut mungkin terdengar panjang, tetapi sebenarnya terdiri dari beberapa konsep sederhana.
Event Dibangun di Atas Delegate
Hal pertama yang perlu dipahami adalah Event dibangun di atas Delegate. Jika sebelumnya sudah mengenal Delegate, Event bisa dianggap sebagai Delegate yang diberi aturan tambahan. Delegate bertugas menyimpan referensi ke satu atau lebih method, lalu memanggil method tersebut ketika diperlukan.
Event memanfaatkan kemampuan Delegate tersebut, tetapi membatasi cara penggunaannya agar lebih aman. Pembatasan inilah yang membuat Event lebih cocok digunakan sebagai mekanisme komunikasi antar object. Pembahasan mengenai aturan tambahan ini akan dijelaskan lebih rinci pada bagian berikutnya.
Event Menggunakan Pola Publish-Subscribe
Konsep penting berikutnya adalah publish-subscribe. Nama polanya memang terdengar cukup teknis, tetapi cara kerjanya sebenarnya sangat sederhana.
Di dalam mekanisme ini terdapat dua peran utama.
-
Publisher, yaitu object yang mengumumkan bahwa suatu kejadian telah terjadi.
-
Subscriber, yaitu object yang mendaftarkan diri untuk menerima pemberitahuan ketika kejadian tersebut terjadi.
Publisher tidak menghubungi setiap Subscriber satu per satu. Publisher hanya mengumumkan bahwa sebuah kejadian telah terjadi. Semua Subscriber yang sudah terdaftar akan menerima pemberitahuan tersebut secara otomatis.
Publisher Tidak Perlu Mengenal Subscriber
Inilah alasan utama mengapa Event menjadi sangat berguna.
Bayangkan terdapat sebuah object Button yang memiliki Event bernama Click. Button tidak perlu mengetahui siapa yang sedang menunggu Event tersebut. Bisa saja yang mendengarkan adalah sebuah Form, sistem pencatatan log, layanan analytics, atau bahkan komponen lain yang baru ditambahkan di kemudian hari.
Bagi Button, semuanya sama. Tugasnya hanya satu, yaitu mengumumkan bahwa tombol telah diklik. Siapa yang menerima pemberitahuan, berapa jumlahnya, dan apa yang dilakukan setelah menerima informasi tersebut sepenuhnya menjadi tanggung jawab masing-masing Subscriber.
Ilustrasinya kurang lebih seperti berikut.
Plain Text
Button│Click()│Event│───────────────│ │ │Form Log Analytics
Ketika Click() dipanggil, Event akan meneruskan pemberitahuan kepada seluruh Subscriber yang telah terdaftar. Meskipun menerima informasi yang sama pada waktu yang bersamaan, masing-masing Subscriber dapat memberikan respons yang berbeda.
-
Form mungkin menutup jendela atau mengubah tampilan.
-
Log mencatat waktu terjadinya klik.
-
Analytics mengirim data ke server untuk keperluan pelaporan.
Yang menarik, jika suatu saat muncul kebutuhan baru, misalnya menambahkan object SoundPlayer agar setiap klik menghasilkan efek suara, tidak ada perubahan yang perlu dilakukan pada class Button. Subscriber baru cukup mendaftarkan diri ke Event yang sudah tersedia.
Plain Text
Button│Click()│Event│──────────────────────│ │ │ │Form Log Analytics SoundPlayer
Button tetap menjalankan tugas yang sama seperti sebelumnya, yaitu mengumumkan bahwa sebuah klik telah terjadi. Penambahan Subscriber tidak mengubah cara kerja Button sama sekali.
Sifat inilah yang membuat Event menjadi salah satu mekanisme komunikasi yang sangat penting dalam pengembangan perangkat lunak. Publisher dan Subscriber tidak saling bergantung secara langsung sehingga masing-masing dapat berkembang tanpa memengaruhi satu sama lain.
Pada bagian berikutnya, akan terlihat mengapa pendekatan ini jauh lebih baik dibandingkan jika komunikasi antar object dilakukan secara manual tanpa menggunakan Event.
Bagian 3: Mengapa Tidak Langsung Memanggil Method?
Setelah memahami konsep Event, mungkin muncul sebuah pertanyaan.
Kalau tujuannya hanya menjalankan beberapa method, mengapa tidak memanggil method tersebut secara langsung saja?
Pertanyaan ini sangat wajar. Justru dengan membandingkan kedua pendekatan tersebut, alasan mengapa Event ada akan menjadi jauh lebih jelas.
Tanpa Event
Bayangkan terdapat sebuah sistem pemesanan sederhana. Setiap kali sebuah order berhasil dibuat, ada beberapa proses lain yang juga perlu dijalankan, misalnya:
-
mengirim email konfirmasi,
-
mengirim notifikasi ke aplikasi,
-
mencatat aktivitas ke dalam log,
-
dan mungkin di masa mendatang masih akan ada proses tambahan lainnya.
Pendekatan yang paling sederhana adalah memanggil semua method tersebut secara langsung.
C#
public class OrderService{private readonly EmailService _emailService;private readonly NotificationService _notificationService;private readonly LoggingService _loggingService;public OrderService(EmailService emailService,NotificationService notificationService,LoggingService loggingService){_emailService = emailService;_notificationService = notificationService;_loggingService = loggingService;}public void CreateOrder(string orderId){// proses pembuatan order..._emailService.SendConfirmation(orderId);_notificationService.Notify(orderId);_loggingService.Log($"Order {orderId} created");}}
Struktur hubungan antar object kurang lebih seperti berikut.
Plain Text
OrderService│├──▶ EmailService├──▶ NotificationService└──▶ LoggingService
Pada awalnya pendekatan ini terlihat sederhana dan tidak memiliki masalah. Namun, seiring bertambahnya fitur, beberapa kekurangan mulai terasa.
Coupling Menjadi Semakin Tinggi
OrderService sekarang mengenal semua service yang harus dipanggil. Class ini mengetahui bahwa ada EmailService, NotificationService, LoggingService, lengkap dengan method yang harus dijalankan pada masing-masing service. Akibatnya, perubahan pada salah satu service sering kali ikut memengaruhi OrderService. Semakin banyak dependency yang dimiliki, semakin erat pula hubungan antar class.
Sulit Dikembangkan
Bayangkan beberapa bulan kemudian muncul kebutuhan baru. Setiap order yang berhasil dibuat harus:
-
menambah poin pelanggan,
-
mengurangi stok barang,
-
mengirim data ke sistem analytics,
-
dan membuat invoice secara otomatis.
Semua proses tersebut akhirnya harus ditambahkan ke dalam OrderService. Setiap fitur baru membuat class ini semakin besar karena semua proses tambahan ditempatkan di lokasi yang sama.
Pengujian Menjadi Lebih Rumit
Hal yang sama juga terjadi saat melakukan pengujian (testing). Untuk menguji method CreateOrder, seluruh dependency perlu disiapkan atau dibuat mock, seperti EmailService, NotificationService, dan LoggingService. Padahal tanggung jawab utama OrderService sebenarnya cukup sederhana, yaitu membuat order. Proses lain hanyalah konsekuensi setelah order berhasil dibuat.
Dengan Event
Sekarang bandingkan dengan pendekatan menggunakan Event.
Plain Text
OrderService│▼OrderCreated Event│├──▶ EmailService├──▶ NotificationService├──▶ LoggingService└──▶ InventoryService
Pada pendekatan ini, OrderService tidak lagi memanggil service lain satu per satu. Setelah proses pembuatan order selesai, OrderService cukup mengumumkan bahwa sebuah order baru saja dibuat melalui Event bernama OrderCreated.
Selanjutnya, object lain yang membutuhkan informasi tersebut dapat mendaftarkan diri sebagai Subscriber. Saat Event dipicu, seluruh Subscriber akan menerima pemberitahuan secara otomatis.
Dengan cara ini, OrderService hanya memiliki satu tanggung jawab, yaitu membuat order dan mengumumkan bahwa proses tersebut telah selesai. Class ini tidak perlu mengetahui apakah ada EmailService, NotificationService, LoggingService, atau service lainnya di dalam sistem.
Jika suatu hari muncul kebutuhan baru, misalnya menambahkan LoyaltyService untuk memberikan poin kepada pelanggan, cukup buat Subscriber baru yang mendengarkan Event OrderCreated. Tidak ada perubahan yang perlu dilakukan pada OrderService.
Begitu pula saat melakukan pengujian. Karena OrderService tidak lagi bergantung secara langsung pada berbagai service lain, pengujian menjadi lebih sederhana. Fokus pengujian cukup memastikan bahwa proses pembuatan order berhasil dan Event OrderCreated benar-benar dipicu ketika seharusnya.
Inilah alasan utama mengapa Event menjadi bagian penting dalam pengembangan aplikasi. Event memisahkan kejadian yang terjadi dari siapa yang memberikan respons terhadap kejadian tersebut. Hubungan antara Publisher dan Subscriber menjadi lebih longgar (loosely coupled), sehingga masing-masing dapat berkembang secara independen tanpa saling memengaruhi.
Bagian 4: Mengenal Keyword event
Setelah memahami konsep Event, sekarang saatnya melihat bagaimana Event ditulis di C#. Namun, sebelum membahas sintaks, ada satu hal yang perlu dipahami terlebih dahulu. Event dan Delegate memiliki hubungan yang sangat erat sehingga keduanya sering dianggap sama, padahal sebenarnya memiliki peran yang berbeda.
Delegate vs Event
Pada bagian sebelumnya sudah dijelaskan bahwa Event dibangun di atas Delegate. Perhatikan dua deklarasi berikut.
C#
// Delegate biasapublic Action<string> OnOrderCreated;// Eventpublic event Action<string> OrderCreated;
Sekilas keduanya terlihat hampir sama. Keduanya sama-sama dapat menyimpan referensi ke method, menerima Subscriber, lalu memanggil seluruh method yang telah terdaftar ketika diperlukan. Perbedaannya terletak pada hak akses.
Jika Menggunakan Delegate Biasa
Apabila OnOrderCreated dideklarasikan sebagai Delegate biasa dan bersifat public, kode di luar class memiliki akses penuh terhadap Delegate tersebut. Artinya, bukan hanya bisa menambahkan atau menghapus Subscriber, tetapi juga melakukan hal-hal yang sebenarnya tidak diinginkan.
C#
var orderService = new OrderService();// Menambahkan SubscriberorderService.OnOrderCreated += SendEmail;// Menghapus seluruh SubscriberorderService.OnOrderCreated = null;// Mengganti seluruh DelegateorderService.OnOrderCreated = SomeoneElsesMethod;// Memanggil Delegate dari luar classorderService.OnOrderCreated?.Invoke("123");
Semua kode di atas diperbolehkan jika menggunakan Delegate biasa. Padahal kondisi tersebut cukup berbahaya. Misalnya, sebuah class lain tanpa sengaja menghapus seluruh Subscriber dengan memberikan nilai null, atau bahkan memanggil Invoke() secara langsung sehingga seolah-olah Event berasal dari OrderService. Keadaan seperti ini bertentangan dengan konsep yang telah dibahas sebelumnya. Publisher seharusnya menjadi satu-satunya pihak yang berhak mengumumkan bahwa suatu kejadian telah terjadi.
Peran Keyword event
Untuk mengatasi masalah tersebut, C# menyediakan keyword event. Perhatikan contoh berikut.
C#
public class OrderService{public event Action<string> OrderCreated;public void CreateOrder(string orderId){// proses pembuatan order...OrderCreated?.Invoke(orderId);}}
Sekarang lihat apa yang terjadi ketika Event digunakan dari luar class.
C#
var orderService = new OrderService();orderService.OrderCreated += SendEmail; // ✔ BolehorderService.OrderCreated -= SendEmail; // ✔ BolehorderService.OrderCreated = null; // ❌ Compile-time errororderService.OrderCreated?.Invoke("123"); // ❌ Compile-time error
Perbedaannya langsung terlihat.
Dari luar class, hanya ada dua operasi yang diperbolehkan:
-
+=untuk mendaftarkan Subscriber. -
-=untuk melepas Subscriber.
Operasi lain, seperti mengganti seluruh isi Delegate atau memanggil Invoke() secara langsung, akan ditolak oleh compiler. Hak untuk memicu Event sepenuhnya berada di tangan class yang mendeklarasikan Event tersebut. Dengan kata lain, Publisher tetap menjadi pemilik penuh atas Event yang dimilikinya. Inilah alasan utama mengapa Event lebih aman dibandingkan menggunakan Delegate biasa secara langsung.
event adalah Delegate yang aksesnya dibatasi.
Dari luar class hanya boleh melakukan subscribe (+=) dan unsubscribe (-=), sedangkan hak untuk memicu Event (Invoke) tetap dimiliki oleh class yang mendeklarasikannya.
EventHandler dan EventHandler<TEventArgs>
Pada contoh sebelumnya, tipe Event menggunakan Action<string>. Pendekatan tersebut sepenuhnya valid, tetapi dalam ekosistem .NET terdapat konvensi yang jauh lebih umum digunakan, yaitu EventHandler dan EventHandler<TEventArgs>.
C#
public event EventHandler OrderCreated;public event EventHandler<OrderCreatedEventArgs> OrderCreatedWithData;
Jika melihat definisinya, EventHandler sebenarnya hanyalah sebuah Delegate.
C#
public delegate void EventHandler(object? sender, EventArgs e);
Signature tersebut selalu memiliki dua parameter.
-
sender, yaitu object yang memicu Event. -
e, yaitu object yang membawa informasi tambahan mengenai Event.
Jika Event tidak memiliki data tambahan, parameter kedua biasanya menggunakan EventArgs.Empty.
Lalu mengapa menggunakan EventHandler jika Action<T> juga bisa? Alasan utamanya adalah konsistensi. Sebagian besar library di .NET menggunakan pola yang sama, yaitu (sender, e). Karena mengikuti konvensi yang seragam, Event akan terasa lebih mudah dipahami tanpa perlu melihat dokumentasi atau implementasinya terlebih dahulu. Saat menemukan Event baru, biasanya sudah dapat ditebak bahwa object yang memicu Event berada pada parameter sender, sedangkan informasi tambahan tersedia melalui parameter e. Pendekatan ini juga membuat seluruh codebase memiliki bentuk yang konsisten sehingga lebih mudah dipelihara dalam jangka panjang.
Bagian 5: Publisher dan Subscriber
Setelah memahami konsep dan sintaks dasar Event, sekarang saatnya melihat bagaimana semuanya bekerja dalam sebuah contoh yang utuh. Contoh berikut akan menggunakan dua class sederhana:
-
Buttonsebagai Publisher, yaitu object yang mengumumkan ketika sebuah klik terjadi. -
Formsebagai Subscriber, yaitu object yang ingin menerima pemberitahuan saat tombol diklik.
Dengan contoh ini, alur komunikasi melalui Event akan terlihat lebih jelas.
Publisher: Button
C#
public class Button{public event EventHandler? Clicked;public string Label { get; set; } = "Submit";public void Click(){Console.WriteLine($"Tombol '{Label}' ditekan...");Clicked?.Invoke(this, EventArgs.Empty);}}
Ada beberapa hal yang menarik dari class di atas. Pertama, Button memiliki sebuah Event bernama Clicked dengan tipe EventHandler. Tanda ? menunjukkan bahwa Event tersebut boleh saja belum memiliki Subscriber sama sekali. Kondisi ini sangat normal karena sebuah Event memang tidak harus selalu memiliki Subscriber. Kedua, method Click() berperan sebagai simulasi ketika tombol benar-benar ditekan. Di dalam method tersebut terdapat baris berikut.
C#
Clicked?.Invoke(this, EventArgs.Empty);
Baris inilah yang memicu Event. Ketika dipanggil, seluruh Subscriber yang telah mendaftarkan diri ke Event Clicked akan menerima pemberitahuan secara otomatis. Perhatikan bahwa Button sama sekali tidak mengetahui siapa yang akan menerima pemberitahuan tersebut. Class ini hanya mengumumkan bahwa sebuah klik telah terjadi.
Subscriber: Form
Sekarang buat sebuah class yang ingin menerima pemberitahuan setiap kali tombol diklik.
C#
public class Form{public string Title { get; set; }public Form(string title){Title = title;}public void HandleButtonClick(object? sender, EventArgs e){if (sender is Button button){Console.WriteLine($"[{Title}] Form menerima klik dari tombol '{button.Label}'");}}}
Method HandleButtonClick() memiliki signature yang sesuai dengan EventHandler, yaitu menerima dua parameter:
-
sender, yaitu object yang memicu Event. -
e, yaitu data tambahan yang dibawa oleh Event.
Pada contoh ini, sender digunakan untuk mengetahui tombol mana yang memicu Event. Karena sender berisi referensi ke object Button, Subscriber dapat mengambil informasi seperti Label tanpa Publisher perlu mengirim parameter tambahan secara khusus.
Menghubungkan Publisher dan Subscriber
Setelah Publisher dan Subscriber selesai dibuat, langkah berikutnya adalah menghubungkan keduanya.
C#
var submitButton = new Button { Label = "Submit" };var mainForm = new Form("Main Window");// SubscribesubmitButton.Clicked += mainForm.HandleButtonClick;// Simulasikan tombol ditekansubmitButton.Click();
Output yang dihasilkan:
Plain Text
Tombol 'Submit' ditekan...[Main Window] Form menerima klik dari tombol 'Submit'
Baris berikut merupakan inti dari proses subscribe.
C#
submitButton.Clicked += mainForm.HandleButtonClick;
Operator += mendaftarkan method HandleButtonClick() ke dalam Event Clicked. Ketika Click() dipanggil, Event akan di-invoke, lalu seluruh method yang telah terdaftar dipanggil secara otomatis sesuai urutan pendaftarannya. Pada contoh ini hanya ada satu Subscriber, sehingga hanya HandleButtonClick() yang dijalankan.
Menambahkan Lebih dari Satu Subscriber
Keunggulan Event mulai terasa ketika lebih dari satu object ingin merespons kejadian yang sama. Misalnya ditambahkan sebuah class untuk mencatat aktivitas klik.
C#
public class ClickLogger{public void LogClick(object? sender, EventArgs e){Console.WriteLine($"[Log] Klik tercatat pada {DateTime.Now:HH:mm:ss}");}}
Kemudian daftarkan kedua Subscriber tersebut ke Event yang sama.
C#
var logger = new ClickLogger();submitButton.Clicked += mainForm.HandleButtonClick;submitButton.Clicked += logger.LogClick;submitButton.Click();
Outputnya menjadi seperti berikut.
Plain Text
Tombol 'Submit' ditekan...[Main Window] Form menerima klik dari tombol 'Submit'[Log] Klik tercatat pada 14:22:07
Perhatikan bahwa Click() hanya dipanggil satu kali, tetapi ada dua Subscriber yang memberikan respons.
-
Formmemperbarui tampilan. -
ClickLoggermencatat aktivitas ke dalam log.
Keduanya menerima pemberitahuan yang sama tanpa saling mengetahui keberadaan satu sama lain. Yang lebih menarik lagi, Button juga tidak perlu diubah sama sekali ketika Subscriber baru ditambahkan. Jika nanti muncul kebutuhan untuk menambahkan SoundPlayer, AnalyticsService, atau NotificationService, masing-masing cukup mendaftarkan dirinya ke Event Clicked.
Publisher tetap menjalankan tugas yang sama seperti sebelumnya, yaitu mengumumkan bahwa sebuah klik telah terjadi. Seluruh Subscriber yang terdaftar akan menerima pemberitahuan secara otomatis. Inilah inti dari pola publish-subscribe. Publisher tidak perlu mengenal Subscriber, sedangkan Subscriber bebas bertambah maupun berkurang tanpa memengaruhi kode Publisher.
Bagian 6: Cara Raise Event
Pada bagian sebelumnya, Event dipicu menggunakan baris berikut.
C#
Clicked?.Invoke(this, EventArgs.Empty);
Baris tersebut terlihat sederhana, tetapi sebenarnya ada beberapa hal penting yang perlu dipahami. Terutama mengenai penggunaan operator ?. sebelum Invoke().
Mengapa Perlu Mengecek null?
Sebuah Event pada dasarnya dibangun di atas Delegate. Seperti Delegate pada umumnya, sebuah Event akan bernilai null apabila belum ada method yang didaftarkan sebagai Subscriber. Kondisi ini sangat normal. Misalnya terdapat sebuah Button yang baru dibuat, tetapi belum ada object yang melakukan subscribe ke Event Clicked.
Jika Event dipanggil secara langsung seperti berikut,
C#
public void Click(){Clicked.Invoke(this, EventArgs.Empty);}
maka kode tersebut akan berjalan dengan baik hanya jika sudah ada Subscriber yang terdaftar.
Namun, apabila Clicked masih bernilai null, pemanggilan Invoke() akan menghasilkan NullReferenceException saat aplikasi dijalankan. Masalahnya bukan karena logika Event salah, melainkan karena tidak ada Subscriber yang dapat dipanggil. Situasi seperti ini cukup sering terjadi, terutama ketika object baru selesai dibuat tetapi proses subscribe belum dilakukan.
Menggunakan Null-Conditional Operator
Untuk menghindari kondisi tersebut, C# menyediakan null-conditional operator (?.).
C#
Clicked?.Invoke(this, EventArgs.Empty);
Cara kerjanya sederhana.
-
Jika
Clickedtidak bernilainull, makaInvoke()akan dijalankan. -
Jika
Clickedbernilainull, maka baris tersebut akan dilewati begitu saja tanpa menghasilkan exception.
Secara konsep, perilakunya sama seperti kode berikut.
C#
if (Clicked != null){Clicked.Invoke(this, EventArgs.Empty);}
Hanya saja, penggunaan ?.Invoke() jauh lebih ringkas dan telah menjadi pola yang umum digunakan pada C# modern.
Sedikit Tentang Thread Safety
Ada satu hal yang sering dibahas ketika berbicara tentang Event, yaitu thread safety.
Perhatikan contoh berikut.
C#
if (Clicked != null){Clicked.Invoke(this, EventArgs.Empty);}
Sekilas tidak ada masalah. Namun, pada aplikasi yang berjalan dengan banyak thread, terdapat kemungkinan yang sangat kecil bahwa Subscriber terakhir melakukan unsubscribe tepat setelah kondisi Clicked != null diperiksa, tetapi sebelum Invoke() dipanggil.
Akibatnya, Clicked dapat berubah menjadi null di sela-sela dua operasi tersebut dan menghasilkan NullReferenceException. Penggunaan ?.Invoke() membantu mengurangi kemungkinan tersebut karena compiler akan mengevaluasi referensi Event terlebih dahulu sebelum melakukan pemanggilan.
Meskipun demikian, pembahasan mengenai thread safety pada Event memiliki cakupan yang cukup luas dan biasanya baru menjadi perhatian pada aplikasi yang memang bekerja secara intensif dengan banyak thread. Untuk sebagian besar aplikasi sehari-hari, pola berikut sudah menjadi praktik yang baik.
C#
Clicked?.Invoke(this, EventArgs.Empty);
Urutan Terjadinya Event
Agar mekanisme Event lebih mudah dipahami, berikut urutan yang biasanya terjadi.
-
Subscriber melakukan subscribe ke Event.
C#
button.Clicked += form.HandleButtonClick; -
Sebuah kejadian terjadi, misalnya pengguna menekan tombol.
-
Publisher memicu Event menggunakan
Invoke().C#
Clicked?.Invoke(this, EventArgs.Empty); -
Seluruh Subscriber yang terdaftar dipanggil sesuai urutan saat mereka melakukan subscribe.
Urutan tersebut dapat digambarkan seperti berikut.
Plain Text
Subscribe│▼Terjadi sebuah aksi│▼Publisher memanggil Invoke()│▼Seluruh Subscriber menerima notifikasi
Ada satu hal penting yang perlu diingat, Event tidak menyimpan riwayat kejadian. Artinya, hanya Subscriber yang sudah terdaftar saat Event dipicu yang akan menerima pemberitahuan. Jika sebuah Event dipicu terlebih dahulu, kemudian Subscriber baru melakukan subscribe setelahnya, Subscriber tersebut tidak akan mengetahui bahwa Event pernah terjadi sebelumnya.
Karena itu, urutan inisialisasi object sering kali menjadi hal yang penting. Subscriber sebaiknya sudah didaftarkan sebelum kemungkinan terjadinya Event, sehingga tidak ada pemberitahuan yang terlewat.
Bagian 7: EventHandler dan EventArgs
Pada contoh-contoh sebelumnya, Event menggunakan EventHandler dengan EventArgs.Empty. Pendekatan tersebut sudah cukup jika Event hanya ingin memberi tahu bahwa suatu kejadian telah terjadi. Namun, pada aplikasi nyata, sebuah Event hampir selalu membawa informasi tambahan.
Misalnya:
-
suhu berubah dari 25°C menjadi 28,5°C,
-
sebuah order selesai diproses beserta total pembayarannya,
-
atau ada karyawan baru yang berhasil ditambahkan ke dalam sistem.
Informasi seperti inilah yang dibawa oleh EventHandler<TEventArgs>.
Membuat Custom EventArgs
Cara membuatnya sangat sederhana. Buat sebuah class yang mewarisi EventArgs, lalu tambahkan property yang berisi informasi yang ingin dikirim kepada Subscriber.
C#
public class TemperatureChangedEventArgs : EventArgs{public double OldTemperature { get; }public double NewTemperature { get; }public TemperatureChangedEventArgs(double oldTemperature,double newTemperature){OldTemperature = oldTemperature;NewTemperature = newTemperature;}}
Pada contoh ini, Event akan membawa dua informasi sekaligus, yaitu suhu sebelum berubah dan suhu setelah berubah.
Menggunakannya pada Publisher
Setelah EventArgs selesai dibuat, Event dapat menggunakan class tersebut sebagai tipe generic pada EventHandler<TEventArgs>.
C#
public class Thermometer{private double _currentTemperature;public event EventHandler<TemperatureChangedEventArgs>? TemperatureChanged;public void UpdateTemperature(double newTemperature){if (newTemperature == _currentTemperature)return;var oldTemperature = _currentTemperature;_currentTemperature = newTemperature;TemperatureChanged?.Invoke(this,new TemperatureChangedEventArgs(oldTemperature,newTemperature));}}
Setiap kali suhu berubah, Publisher membuat object TemperatureChangedEventArgs, mengisinya dengan informasi yang dibutuhkan, lalu mengirimkannya bersama Event.
Subscriber Menerima Informasi Tambahan
Subscriber sekarang tidak hanya mengetahui bahwa Event terjadi, tetapi juga memperoleh seluruh data yang dibutuhkan.
C#
public class TemperatureDisplay{public void OnTemperatureChanged(object? sender,TemperatureChangedEventArgs e){Console.WriteLine($"Suhu berubah dari {e.OldTemperature}°C ke {e.NewTemperature}°C");}}
Proses penggunaannya tetap sama seperti sebelumnya.
C#
var thermometer = new Thermometer();var display = new TemperatureDisplay();thermometer.TemperatureChanged += display.OnTemperatureChanged;thermometer.UpdateTemperature(28.5);
Output yang dihasilkan:
Plain Text
Suhu berubah dari 0°C ke 28.5°C
Perbedaannya cukup jelas. Dengan EventHandler biasa, Subscriber hanya mengetahui bahwa sebuah perubahan telah terjadi. Sedangkan dengan EventHandler<TEventArgs>, Subscriber juga mengetahui informasi apa yang berubah.
Subscriber tidak perlu lagi membaca property lain atau memanggil method tambahan hanya untuk mengetahui kondisi terbaru. Seluruh informasi yang dibutuhkan sudah tersedia di dalam object EventArgs.
Contoh Lain: Order Selesai Diproses
Konsep yang sama juga dapat diterapkan pada sistem pemesanan.
C#
public class OrderCompletedEventArgs : EventArgs{public string OrderId { get; }public decimal TotalAmount { get; }public OrderCompletedEventArgs(string orderId,decimal totalAmount){OrderId = orderId;TotalAmount = totalAmount;}}
Publisher menggunakan class tersebut sebagai data Event.
C#
public class OrderService{public event EventHandler<OrderCompletedEventArgs>? OrderCompleted;public void CompleteOrder(string orderId, decimal totalAmount){// proses penyelesaian order...OrderCompleted?.Invoke(this,new OrderCompletedEventArgs(orderId, totalAmount));}}
Subscriber, seperti EmailService, dapat langsung menggunakan informasi yang tersedia.
C#
public void SendConfirmation(object? sender,OrderCompletedEventArgs e){Console.WriteLine($"Mengirim email untuk order {e.OrderId} dengan total {e.TotalAmount:C}");}
Tidak perlu lagi mencari data order melalui method lain karena seluruh informasi penting sudah dikirim bersamaan dengan Event.
Contoh Lain: Karyawan Baru Ditambahkan
Contoh berikut berasal dari sistem HR.
C#
public class EmployeeAddedEventArgs : EventArgs{public string EmployeeName { get; }public string Department { get; }public EmployeeAddedEventArgs(string employeeName,string department){EmployeeName = employeeName;Department = department;}}
Publisher:
C#
public class EmployeeService{public event EventHandler<EmployeeAddedEventArgs>? EmployeeAdded;public void AddEmployee(string name, string department){// proses penyimpanan data...EmployeeAdded?.Invoke(this,new EmployeeAddedEventArgs(name, department));}}
Subscriber dapat langsung memanfaatkan data tersebut.
Misalnya:
-
PayrollServicemembuat data penggajian. -
OnboardingServicemenyiapkan proses orientasi karyawan baru. -
NotificationServicemengirim pemberitahuan kepada tim terkait.
Seluruh Subscriber menerima informasi yang sama tanpa perlu melakukan pencarian data tambahan.
Mengapa EventArgs Menjadi Standar di .NET?
Jika diperhatikan, seluruh contoh sebelumnya memiliki bentuk yang hampir sama.
Semuanya:
-
mewarisi
EventArgs, -
memiliki property yang hanya dapat dibaca,
-
dan mengisi seluruh data melalui constructor.
Pola ini bukan sebuah kebetulan, melainkan konvensi yang digunakan hampir di seluruh ekosistem .NET. Karena menggunakan bentuk yang konsisten, Event menjadi lebih mudah dipahami. Saat menemukan sebuah EventHandler<TEventArgs>, biasanya sudah dapat ditebak bahwa seluruh informasi yang berkaitan dengan Event tersedia di dalam object EventArgs.
Pendekatan ini juga membuat kode lebih mudah dikembangkan. Jika suatu saat diperlukan informasi tambahan, cukup tambahkan property baru pada class EventArgs tanpa perlu mengubah signature Event maupun seluruh Subscriber yang sudah ada.
Inilah salah satu alasan mengapa EventHandler<TEventArgs> lebih banyak digunakan dibandingkan Delegate seperti Action<string, decimal>. Selain lebih konsisten, pola ini juga lebih mudah dipelihara dan telah menjadi standar dalam pengembangan aplikasi .NET.
Bagian 8: Contoh Studi Kasus Nyata
Setelah membahas konsep, sintaks, hingga penggunaan EventHandler<TEventArgs>, sekarang saatnya melihat bagaimana semuanya diterapkan dalam sebuah kasus yang lebih mendekati aplikasi nyata. Bayangkan sebuah sistem e-commerce.
Ketika sebuah order berhasil diselesaikan, ternyata masih ada banyak proses lain yang harus dijalankan. Misalnya:
-
mengirim email konfirmasi,
-
mengirim notifikasi ke aplikasi,
-
menambahkan poin loyalty,
-
mencatat transaksi ke sistem analytics,
-
dan menyimpan log aktivitas.
Seluruh proses tersebut terjadi karena satu kejadian yang sama, yaitu order berhasil diselesaikan.
Hubungannya dapat digambarkan seperti berikut.
Plain Text
OrderService│▼OrderCompleted Event│├──▶ EmailService├──▶ NotificationService├──▶ LoyaltyService├──▶ AnalyticsService└──▶ LoggingService
Pada contoh ini, OrderService berperan sebagai Publisher, sedangkan service lainnya menjadi Subscriber.
Publisher: OrderService
Pertama, buat sebuah EventArgs yang membawa informasi mengenai order yang telah selesai diproses.
C#
public class OrderCompletedEventArgs : EventArgs{public string OrderId { get; }public string CustomerEmail { get; }public decimal TotalAmount { get; }public OrderCompletedEventArgs(string orderId,string customerEmail,decimal totalAmount){OrderId = orderId;CustomerEmail = customerEmail;TotalAmount = totalAmount;}}public class OrderService{public event EventHandler<OrderCompletedEventArgs>? OrderCompleted;public void CompleteOrder(string orderId,string customerEmail,decimal totalAmount){Console.WriteLine($"Memproses order {orderId}...");// Validasi pembayaran// Update stok// Simpan transaksi// Dan proses bisnis lainnyaConsole.WriteLine($"Order {orderId} berhasil diselesaikan.");OrderCompleted?.Invoke(this,new OrderCompletedEventArgs(orderId,customerEmail,totalAmount));}}
Perhatikan bahwa OrderService hanya memiliki satu tanggung jawab setelah proses bisnis selesai, yaitu memicu Event OrderCompleted. Class ini tidak mengetahui siapa saja yang akan menerima pemberitahuan tersebut.
Subscriber: EmailService
Subscriber pertama bertugas mengirim email konfirmasi.
C#
public class EmailService{public void SendConfirmation(object? sender,OrderCompletedEventArgs e){Console.WriteLine($"[Email] Konfirmasi order {e.OrderId} dikirim ke {e.CustomerEmail}");}}
Subscriber: NotificationService
Subscriber berikutnya mengirim notifikasi ke aplikasi.
C#
public class NotificationService{public void SendPushNotification(object? sender,OrderCompletedEventArgs e){Console.WriteLine($"[Push] Notifikasi order {e.OrderId} berhasil terkirim");}}
Subscriber: LoyaltyService
Service ini menambahkan poin berdasarkan total transaksi.
C#
public class LoyaltyService{public void AddPoints(object? sender,OrderCompletedEventArgs e){var points = (int)(e.TotalAmount / 10_000);Console.WriteLine($"[Loyalty] +{points} poin ditambahkan untuk order {e.OrderId}");}}
Subscriber: AnalyticsService
Subscriber ini mencatat transaksi ke sistem analytics.
C#
public class AnalyticsService{public void TrackOrder(object? sender,OrderCompletedEventArgs e){Console.WriteLine($"[Analytics] Order {e.OrderId} senilai {e.TotalAmount:C} tercatat");}}
Subscriber: LoggingService
Subscriber terakhir menyimpan log aktivitas.
C#
public class LoggingService{public void LogOrder(object? sender,OrderCompletedEventArgs e){Console.WriteLine($"[Log] {DateTime.Now:HH:mm:ss} — Order {e.OrderId} completed");}}
Menghubungkan Seluruh Subscriber
Setelah seluruh class selesai dibuat, langkah berikutnya adalah mendaftarkan setiap Subscriber ke Event OrderCompleted.
C#
var orderService = new OrderService();var emailService = new EmailService();var notificationService = new NotificationService();var loyaltyService = new LoyaltyService();var analyticsService = new AnalyticsService();var loggingService = new LoggingService();orderService.OrderCompleted += emailService.SendConfirmation;orderService.OrderCompleted += notificationService.SendPushNotification;orderService.OrderCompleted += loyaltyService.AddPoints;orderService.OrderCompleted += analyticsService.TrackOrder;orderService.OrderCompleted += loggingService.LogOrder;orderService.CompleteOrder("ORD-2024-001","budi@example.com",150_000);
Hasilnya akan terlihat seperti berikut.
Plain Text
Memproses order ORD-2024-001...Order ORD-2024-001 berhasil diselesaikan.[Email] Konfirmasi order ORD-2024-001 dikirim ke budi@example.com[Push] Notifikasi order ORD-2024-001 berhasil terkirim[Loyalty] +15 poin ditambahkan untuk order ORD-2024-001[Analytics] Order ORD-2024-001 senilai Rp150.000 tercatat[Log] 14:35:02 — Order ORD-2024-001 completed
Mengapa Pendekatan Ini Sangat Fleksibel?
Hal yang paling menarik dari contoh ini bukanlah jumlah Subscriber yang menerima Event, melainkan hubungan antar class yang tetap longgar. Jika diperhatikan, OrderService tidak pernah membuat object EmailService, NotificationService, LoyaltyService, AnalyticsService, maupun LoggingService. Bahkan, OrderService tidak mengetahui bahwa class-class tersebut ada.
Seluruh Subscriber didaftarkan dari luar, biasanya pada tahap inisialisasi aplikasi atau melalui Dependency Injection. Akibatnya, Publisher dapat tetap fokus pada proses bisnis utamanya tanpa perlu mengetahui siapa saja yang akan memberikan respons terhadap Event yang dipicu.
Keuntungan lain mulai terasa ketika aplikasi terus berkembang. Misalnya muncul kebutuhan baru untuk mengirim SMS kepada pelanggan setelah order tertentu berhasil diproses. Dengan pendekatan menggunakan Event, tidak perlu mengubah OrderService.
Cukup buat SmsService, tambahkan method yang sesuai dengan EventHandler<OrderCompletedEventArgs>, lalu daftarkan sebagai Subscriber.
C#
orderService.OrderCompleted += smsService.SendPromotion;
Selesai.
Publisher tetap bekerja seperti sebelumnya, sedangkan Subscriber baru langsung ikut menerima pemberitahuan setiap kali Event OrderCompleted dipicu. Inilah salah satu alasan mengapa Event sering digunakan dalam aplikasi berskala besar. Sebuah perubahan atau penambahan fitur dapat dilakukan dengan dampak yang jauh lebih kecil terhadap kode yang sudah ada, sehingga aplikasi menjadi lebih mudah dikembangkan dan dipelihara seiring bertambahnya kebutuhan bisnis.
Bagian 9: add dan remove Accessors
Pada seluruh contoh sebelumnya, Event selalu dideklarasikan seperti ini.
C#
public event EventHandler<OrderCompletedEventArgs>? OrderCompleted;
Deklarasi tersebut terlihat sangat sederhana. Padahal, ketika kode tersebut dikompilasi, compiler sebenarnya membuat mekanisme tambahan yang tidak terlihat secara langsung. Mekanisme inilah yang menangani proses subscribe (+=) dan unsubscribe (-=).
Sebagian besar developer tidak pernah perlu memikirkannya karena compiler sudah mengerjakan semuanya secara otomatis. Namun, memahami apa yang terjadi di balik layar akan membantu menjelaskan mengapa Event memiliki keyword add dan remove.
Apa yang Dilakukan Compiler?
Ketika menulis Event seperti berikut,
C#
public event EventHandler<OrderCompletedEventArgs>? OrderCompleted;
compiler secara konsep akan menghasilkan kode yang kurang lebih setara dengan ini.
C#
private EventHandler<OrderCompletedEventArgs>? _orderCompleted;public event EventHandler<OrderCompletedEventArgs>? OrderCompleted{add{_orderCompleted += value;}remove{_orderCompleted -= value;}}
Perlu diperhatikan bahwa ini bukan kode yang benar-benar dihasilkan oleh compiler, melainkan gambaran sederhana mengenai cara kerjanya. Ada dua bagian penting di sini. Pertama, terdapat sebuah field privat bernama _orderCompleted. Field inilah yang menyimpan seluruh Subscriber yang telah terdaftar. Kedua, terdapat dua buah accessor.
-
adddipanggil setiap kali ada kode yang melakukan subscribe menggunakan+=. -
removedipanggil setiap kali ada kode yang melakukan unsubscribe menggunakan-=.
Selama tidak ada kebutuhan khusus, compiler akan membuat seluruh mekanisme tersebut secara otomatis sehingga Event cukup ditulis dalam satu baris saja.
Kapan add dan remove Ditulis Sendiri?
Meskipun implementasi bawaan compiler sudah cukup untuk sebagian besar kasus, ada beberapa situasi di mana proses subscribe dan unsubscribe perlu dikendalikan secara manual.
Mencatat Aktivitas Subscribe dan Unsubscribe
Misalnya ingin mengetahui siapa saja yang mendaftarkan atau melepas Subscriber. Hal ini cukup berguna ketika sedang melakukan debugging.
C#
public class Button{private EventHandler? _clicked;public event EventHandler? Clicked{add{Console.WriteLine($"Subscriber ditambahkan: {value?.Method.Name}");_clicked += value;}remove{Console.WriteLine($"Subscriber dihapus: {value?.Method.Name}");_clicked -= value;}}public void Click(){_clicked?.Invoke(this, EventArgs.Empty);}}
Dengan cara ini, setiap proses subscribe maupun unsubscribe akan tercatat tanpa perlu mengubah kode Subscriber.
Membatasi Jumlah Subscriber
Ada kalanya sebuah Event memang dirancang hanya boleh memiliki satu Subscriber. Aturan tersebut dapat diterapkan melalui accessor add.
C#
public class SingleSubscriberEvent{private EventHandler? _handler;public event EventHandler? Handler{add{if (_handler != null){throw new InvalidOperationException("Event ini hanya boleh memiliki satu subscriber.");}_handler = value;}remove{_handler -= value;}}}
Apabila ada Subscriber kedua yang mencoba mendaftar, aplikasi akan langsung memberikan exception sehingga aturan tersebut tetap terjaga.
Meneruskan Event ke Object Lain
Contoh lain adalah ketika sebuah class ingin membungkus object lain tanpa memperlihatkan implementasi di dalamnya.
C#
public class ButtonWrapper{private readonly Button _innerButton;public ButtonWrapper(Button innerButton){_innerButton = innerButton;}public event EventHandler? Clicked{add => _innerButton.Clicked += value;remove => _innerButton.Clicked -= value;}}
Pada contoh ini, ButtonWrapper memiliki Event Clicked. Namun sebenarnya, proses subscribe dan unsubscribe diteruskan ke Button yang berada di dalam class tersebut.
Bagi pengguna ButtonWrapper, seluruh detail implementasi tersebut tidak terlihat. Yang terlihat hanya sebuah Event biasa. Teknik seperti ini sering digunakan ketika membuat wrapper, adapter, atau class yang ingin menyembunyikan object internalnya.
Kapan Tidak Perlu Menggunakan Custom Accessor?
Meskipun add dan remove memberikan kontrol yang sangat besar, bukan berarti setiap Event harus menggunakannya. Justru sebaliknya, pada sebagian besar aplikasi, deklarasi berikut sudah lebih dari cukup.
C#
public event EventHandler<OrderCompletedEventArgs>? OrderCompleted;
Compiler akan membuat seluruh mekanisme subscribe dan unsubscribe secara otomatis. Menulis add dan remove sendiri tanpa alasan yang jelas hanya akan menambah jumlah kode yang harus dipelajari, diuji, dan dipelihara. Karena itu, custom accessor biasanya digunakan hanya ketika memang ada kebutuhan khusus, misalnya:
-
mencatat proses subscribe atau unsubscribe,
-
menerapkan aturan tertentu saat Subscriber ditambahkan,
-
atau meneruskan Event ke object lain.
Jika kebutuhan tersebut tidak ada, biarkan compiler yang menanganinya. Pendekatan ini membuat kode tetap sederhana, mudah dibaca, dan mengikuti praktik yang umum digunakan dalam pengembangan aplikasi .NET.
Bagian 10: User Actions dan UI Framework
Sejauh ini contoh yang digunakan sebagian besar berupa class sederhana seperti Button, OrderService, atau Thermometer. Namun, ada satu tempat di mana Event digunakan hampir di setiap baris kode, yaitu aplikasi dengan antarmuka pengguna (UI).
Baik WinForms, WPF, UWP, maupun .NET MAUI, semuanya menjadikan Event sebagai mekanisme utama untuk menangani interaksi pengguna. Jika pernah melihat kode seperti berikut,
C#
submitButton.Click += SubmitButton_Click;searchBox.TextChanged += SearchBox_TextChanged;dropdown.SelectionChanged += Dropdown_SelectionChanged;canvas.MouseMove += Canvas_MouseMove;
maka sebenarnya pola yang digunakan tidak berbeda dengan seluruh pembahasan sebelumnya. Pada contoh tersebut:
-
submitButton,searchBox,dropdown, dancanvasadalah Publisher. -
SubmitButton_Click,SearchBox_TextChanged, dan method lainnya adalah Subscriber.
Setiap kali terjadi sebuah aksi pada komponen UI, framework akan memicu Event yang sesuai, lalu seluruh Subscriber yang telah terdaftar akan dipanggil secara otomatis.
Mengapa UI Sangat Bergantung pada Event?
Bayangkan jika aplikasi tidak menggunakan Event, setiap komponen harus diperiksa terus-menerus. Misalnya:
-
apakah tombol sedang ditekan,
-
apakah isi
TextBoxberubah, -
apakah posisi mouse berpindah,
-
atau apakah pengguna baru saja memilih item pada
ComboBox.
Pengecekan tersebut harus dilakukan berulang kali selama aplikasi berjalan. Selain membuang sumber daya, pendekatan seperti ini juga membuat kode menjadi jauh lebih rumit. Dengan Event, pendekatannya menjadi kebalikan. Komponen UI tidak melakukan apa pun selama tidak ada interaksi dari pengguna. Begitu sebuah aksi benar-benar terjadi, misalnya tombol diklik atau teks berubah, framework akan langsung memicu Event yang sesuai. Kode aplikasi kemudian hanya bereaksi terhadap Event tersebut. Pendekatan inilah yang dikenal sebagai Event-Driven Programming, yaitu gaya pemrograman di mana alur eksekusi ditentukan oleh berbagai kejadian (event) yang muncul selama aplikasi berjalan. Karena hanya merespons ketika diperlukan, aplikasi menjadi lebih efisien dan lebih mudah dipahami.
Konsep yang Sama di Berbagai Platform
Meskipun contoh pada artikel ini menggunakan C#, konsep yang digunakan sebenarnya bersifat umum. Sebagian besar framework modern menerapkan pola yang hampir sama.
Misalnya:
-
JavaScript memiliki Event seperti
click,input, dankeydown. -
Android memiliki berbagai listener seperti
OnClickListener. -
iOS menggunakan mekanisme target-action maupun callback untuk menangani interaksi pengguna.
Nama dan sintaksnya memang berbeda, tetapi konsep dasarnya tetap sama. Ada sebuah object yang mengumumkan bahwa suatu kejadian telah terjadi, lalu object lain memberikan respons terhadap kejadian tersebut.
Sekilas Tentang UWP
Pada Universal Windows Platform (UWP), terdapat sedikit perbedaan pada implementasi internal Event. Di balik layar, proses subscribe tidak hanya menyimpan Delegate, tetapi juga menggunakan sesuatu yang disebut event registration token.
Token tersebut berfungsi sebagai identitas yang membantu framework melacak setiap proses subscribe dan unsubscribe, terutama karena UWP banyak berinteraksi dengan komponen Windows Runtime (WinRT) yang berada di luar lingkungan .NET.
Walaupun implementasinya lebih kompleks, detail tersebut sepenuhnya disembunyikan oleh compiler. Dari sudut pandang developer, sintaks yang digunakan tetap sama.
C#
myButton.Click += MyButton_Click;myButton.Click -= MyButton_Click;
Tidak ada perlakuan khusus yang perlu dilakukan. Semua proses pengelolaan token ditangani secara otomatis oleh framework.
Yang Perlu Dipahami
Pembahasan pada bagian ini bukan bertujuan untuk mempelajari WinForms, WPF, UWP, atau .NET MAUI secara mendalam. Hal yang jauh lebih penting adalah memahami bahwa seluruh framework tersebut dibangun di atas konsep Event yang sama. Komponen UI bertindak sebagai Publisher dan kode aplikasi bertindak sebagai Subscriber.
Ketika pengguna melakukan suatu aksi, Publisher akan memicu Event, kemudian seluruh Subscriber yang telah terdaftar akan menerima pemberitahuan tanpa perlu melakukan pengecekan secara terus-menerus.
Begitu konsep ini dipahami, cara kerja berbagai framework UI akan terasa jauh lebih mudah dipelajari. Meskipun nama Event dan komponen yang digunakan berbeda-beda, pola komunikasinya tetap sama: sebuah kejadian diumumkan oleh Publisher, lalu Subscriber memberikan respons sesuai kebutuhan.
Bagian 11: Observer Design Pattern
Pada bagian-bagian sebelumnya sudah dibahas bagaimana Event bekerja, mulai dari konsep Publisher dan Subscriber, cara melakukan subscribe, hingga bagaimana Event dipicu menggunakan Invoke(). Sekarang saatnya melihat gambaran yang lebih besar. Ternyata, seluruh mekanisme tersebut bukan sekadar fitur yang dimiliki C#. Event merupakan implementasi dari salah satu design pattern yang paling terkenal dalam dunia pengembangan perangkat lunak, yaitu Observer Design Pattern.
Apa Itu Observer Pattern?
Observer Pattern adalah pola desain yang mendefinisikan hubungan satu-ke-banyak (one-to-many) antar object. Ide utamanya sederhana, ketika sebuah object mengalami perubahan atau terjadi suatu kejadian, seluruh object lain yang telah mendaftarkan diri sebagai pengamat akan menerima pemberitahuan secara otomatis.
Dalam Observer Pattern terdapat dua peran utama.
-
Subject (atau sering juga disebut Observable), yaitu object yang menjadi sumber perubahan.
-
Observer, yaitu object yang ingin mengetahui ketika Subject mengalami perubahan.
Hubungan tersebut dapat digambarkan seperti berikut.
Plain Text
Observer▲│Observer▲│Subject ────────┼────────► Observer│▼Observer
Subject tidak perlu mengetahui apa yang dilakukan oleh setiap Observer. Tugasnya hanya satu, yaitu memberi tahu bahwa sebuah perubahan telah terjadi. Sebaliknya, setiap Observer bebas menentukan bagaimana cara merespons pemberitahuan tersebut.
Hubungannya dengan Event di C#
Jika penjelasan tadi terasa familiar, memang seharusnya demikian. Konsep tersebut persis dengan Publisher dan Subscriber yang telah dibahas sejak awal artikel. Perbedaannya hanya pada istilah yang digunakan.
| Observer Pattern | Event di C# |
|---|---|
| Subject | Publisher (class yang mendeklarasikan event) |
| Observer | Subscriber (method yang melakukan subscribe) |
| Attach / Subscribe | += |
| Detach / Unsubscribe | -= |
| Notify | ?.Invoke() |
Artinya, setiap kali menulis kode seperti berikut,
C#
button.Click += HandleClick;
sebenarnya sedang menerapkan Observer Pattern. Begitu pula ketika Publisher memanggil:
C#
Clicked?.Invoke(this, EventArgs.Empty);
Publisher sedang melakukan proses Notify, yaitu memberi tahu seluruh Observer bahwa sebuah kejadian telah terjadi.
Observer Pattern Tanpa Event
Jika membaca buku Design Patterns karya Gang of Four atau melihat implementasi Observer Pattern pada beberapa bahasa lain, biasanya akan ditemukan class seperti berikut.
-
ISubject -
IObserver -
Attach() -
Detach() -
Notify()
Semua mekanisme tersebut harus dibuat secara manual. Subject harus menyimpan daftar Observer, menyediakan method untuk menambahkan dan menghapus Observer, lalu melakukan iterasi terhadap seluruh Observer setiap kali terjadi perubahan. Di C#, sebagian besar pekerjaan tersebut sudah disediakan oleh bahasa itu sendiri melalui Event.
Ketika menggunakan:
-
keyword
event, -
operator
+=, -
operator
-=, -
dan
Invoke(),
sebenarnya seluruh mekanisme Observer Pattern sudah tersedia tanpa perlu membuat implementasinya dari awal. Inilah salah satu alasan mengapa penggunaan Event terasa sederhana meskipun konsep di baliknya cukup kuat.
Studi Kasus: Weather Station
Contoh klasik untuk menjelaskan Observer Pattern adalah sebuah stasiun cuaca. Bayangkan terdapat sebuah alat yang secara berkala mengukur suhu udara. Setiap kali suhu berubah, berbagai perangkat perlu memperbarui tampilannya.
Plain Text
WeatherStation│▼TemperatureChanged Event│├──▶ Phone├──▶ Website├──▶ Smart TV└──▶ Dashboard
Publisher mendeklarasikan Event.
C#
public class TemperatureChangedEventArgs : EventArgs{public double Temperature { get; }public TemperatureChangedEventArgs(double temperature){Temperature = temperature;}}public class WeatherStation{public event EventHandler<TemperatureChangedEventArgs>? TemperatureChanged;public void MeasureTemperature(double temperature){Console.WriteLine($"Sensor membaca suhu: {temperature}°C");TemperatureChanged?.Invoke(this,new TemperatureChangedEventArgs(temperature));}}
Kemudian beberapa Subscriber mendaftarkan dirinya.
C#
public class Phone{public void UpdateDisplay(object? sender,TemperatureChangedEventArgs e)=> Console.WriteLine($"[Phone] Menampilkan suhu: {e.Temperature}°C");}public class Website{public void UpdateDisplay(object? sender,TemperatureChangedEventArgs e)=> Console.WriteLine($"[Website] Widget cuaca diperbarui: {e.Temperature}°C");}public class SmartTv{public void UpdateDisplay(object? sender,TemperatureChangedEventArgs e)=> Console.WriteLine($"[Smart TV] Overlay suhu: {e.Temperature}°C");}public class Dashboard{public void UpdateDisplay(object? sender,TemperatureChangedEventArgs e)=> Console.WriteLine($"[Dashboard] Grafik suhu ditambahkan titik baru: {e.Temperature}°C");}
Seluruh object kemudian dihubungkan melalui Event.
C#
var station = new WeatherStation();var phone = new Phone();var website = new Website();var smartTv = new SmartTv();var dashboard = new Dashboard();station.TemperatureChanged += phone.UpdateDisplay;station.TemperatureChanged += website.UpdateDisplay;station.TemperatureChanged += smartTv.UpdateDisplay;station.TemperatureChanged += dashboard.UpdateDisplay;station.MeasureTemperature(31.2);
Output yang dihasilkan:
Plain Text
Sensor membaca suhu: 31.2°C[Phone] Menampilkan suhu: 31.2°C[Website] Widget cuaca diperbarui: 31.2°C[Smart TV] Overlay suhu: 31.2°C[Dashboard] Grafik suhu ditambahkan titik baru: 31.2°C
Perhatikan bahwa WeatherStation tidak pernah mengetahui siapa saja yang sedang menampilkan data suhu. Publisher hanya mengumumkan bahwa suhu telah berubah.
Setiap Subscriber kemudian memberikan respons sesuai tanggung jawabnya masing-masing. Bahkan jika suatu saat ditambahkan MobileWidget, DigitalBillboard, atau IoTDisplay, tidak ada perubahan yang perlu dilakukan pada WeatherStation. Cukup daftarkan Subscriber baru ke Event yang sudah ada.
Mengapa Observer Pattern Penting?
Memahami hubungan antara Event dan Observer Pattern memberikan sudut pandang yang berbeda terhadap keyword event. Event bukan sekadar fitur untuk menangani klik tombol atau perubahan teks pada aplikasi desktop.
Event adalah implementasi nyata dari sebuah pola desain yang telah digunakan selama puluhan tahun dalam pengembangan perangkat lunak. Pemahaman ini juga memudahkan ketika berpindah ke bahasa pemrograman atau framework lain.
Meskipun sintaksnya berbeda, konsep yang digunakan tetap sama. Selalu ada sebuah object yang mengumumkan bahwa sesuatu telah terjadi, dan selalu ada object lain yang memilih untuk mendengarkan serta memberikan respons terhadap kejadian tersebut.
Karena itu, ketika memahami Event di C#, sebenarnya bukan hanya sebuah fitur bahasa yang dipelajari, tetapi juga salah satu pola komunikasi antar object yang paling mendasar dan paling banyak digunakan dalam software engineering.
Bagian 12: Best Practices
Setelah memahami konsep, sintaks, dan cara kerja Event, ada beberapa praktik yang umum diterapkan dalam pengembangan aplikasi .NET. Sebagian besar memang bukan aturan yang wajib diikuti. Namun, mengikuti kebiasaan-kebiasaan ini akan membuat kode lebih konsisten, lebih mudah dipelihara, dan mengurangi potensi masalah di kemudian hari.
Gunakan EventHandler Jika Memungkinkan
Pada bagian sebelumnya sudah dibahas bahwa .NET menyediakan EventHandler dan EventHandler<TEventArgs> sebagai konvensi standar untuk Event. Karena itu, selama tidak ada kebutuhan khusus, sebaiknya gunakan tipe tersebut.
C#
// Disarankanpublic event EventHandler<OrderCompletedEventArgs>? OrderCompleted;
Dibandingkan membuat Delegate sendiri seperti berikut.
C#
// Gunakan jika memang ada alasan khususpublic event Action<string, decimal>? OrderCompleted;
Keuntungan terbesar bukan pada performa, melainkan pada konsistensi.
Developer yang melihat Event tersebut langsung mengetahui bahwa:
-
parameter pertama adalah object yang memicu Event (
sender), -
parameter kedua berisi informasi tambahan (
EventArgs), -
dan cara menggunakannya mengikuti pola yang sama dengan Event lain di seluruh ekosistem .NET.
Semakin konsisten sebuah codebase, semakin mudah pula kode tersebut dipahami dan dipelihara.
Selalu Unsubscribe Jika Sudah Tidak Dibutuhkan
Salah satu kesalahan yang paling sering terjadi saat menggunakan Event adalah lupa melakukan unsubscribe. Perhatikan contoh berikut.
C#
public class Form{public void AttachToButton(Button button){button.Clicked += HandleClick;}public void DetachFromButton(Button button){button.Clicked -= HandleClick;}private void HandleClick(object? sender, EventArgs e){Console.WriteLine("Button diklik.");}}
Sekilas tidak ada masalah. Namun bayangkan Form sudah tidak digunakan lagi karena jendelanya telah ditutup. Jika DetachFromButton() tidak pernah dipanggil, maka Button masih menyimpan referensi ke HandleClick. Karena HandleClick adalah method milik Form, secara tidak langsung Button juga masih menyimpan referensi ke object Form. Akibatnya, selama Button masih hidup, Garbage Collector menganggap Form masih digunakan sehingga object tersebut tidak dapat dibersihkan dari memori. Kondisi inilah yang dikenal sebagai memory leak.
Aturan sederhana yang sering digunakan adalah:
Jika melakukan subscribe, pastikan ada tempat yang jelas untuk melakukan unsubscribe.
Pada aplikasi desktop, proses unsubscribe biasanya dilakukan ketika window ditutup atau object di-dispose.
Sekilas Tentang Weak Event Pattern
Ada situasi di mana Publisher memiliki umur yang jauh lebih panjang dibanding Subscriber.
Misalnya:
-
aplikasi memiliki satu object global yang hidup selama aplikasi berjalan,
-
sementara Subscriber dibuat dan dihancurkan berkali-kali.
Pada kondisi seperti ini, lupa melakukan unsubscribe jauh lebih mudah terjadi. Untuk mengatasi masalah tersebut, beberapa framework menyediakan Weak Event Pattern.
Alih-alih menyimpan referensi biasa (strong reference), mekanisme ini menggunakan WeakReference, sehingga Subscriber tetap dapat dibersihkan oleh Garbage Collector meskipun belum melakukan unsubscribe.
Topik ini cukup luas dan biasanya baru diperlukan pada aplikasi yang lebih kompleks. Yang terpenting untuk diingat adalah bahwa Weak Event Pattern memang ada sebagai solusi ketika pengelolaan siklus hidup object menjadi lebih rumit.
Gunakan Nama Event yang Jelas
Di ekosistem .NET, Event umumnya diberi nama menggunakan past tense, yaitu menggambarkan bahwa sebuah kejadian sudah selesai terjadi.
Contohnya:
C#
public event EventHandler? Completed;public event EventHandler? Changed;public event EventHandler? Clicked;public event EventHandler? Closed;public event EventHandler? Saved;
Penamaan seperti ini membuat maksud Event lebih mudah dipahami. Misalnya, Clicked menunjukkan bahwa tombol sudah diklik ketika Subscriber menerima notifikasi. Begitu pula Completed menunjukkan bahwa suatu proses telah selesai. Ada juga Event yang memang dipicu sebelum suatu aksi dilakukan. Biasanya Event seperti ini menggunakan bentuk present participle.
Sebagai contoh:
C#
public event EventHandler? Closing;public event EventHandler? Closed;
Closing dipicu sebelum proses penutupan berlangsung, sehingga Subscriber masih memiliki kesempatan untuk membatalkan proses tersebut. Sementara Closed dipicu setelah proses benar-benar selesai. Pola penamaan seperti ini banyak digunakan pada WinForms, WPF, dan berbagai library .NET lainnya.
Satu Event untuk Satu Kejadian
Usahakan setiap Event hanya merepresentasikan satu kejadian yang spesifik.
Sebagai contoh:
C#
public event EventHandler? OrderCompleted;public event EventHandler? PaymentSucceeded;public event EventHandler? CustomerRegistered;
Masing-masing Event memiliki makna yang jelas. Sebaliknya, hindari membuat Event yang terlalu umum seperti berikut.
C#
public event EventHandler? SomethingHappened;
atau
C#
public event EventHandler<SystemEventArgs>? ProcessChanged;
yang ternyata digunakan untuk puluhan kondisi berbeda.
Event seperti ini membuat Subscriber harus memeriksa berbagai kondisi tambahan hanya untuk mengetahui apa yang sebenarnya terjadi. Jika sebuah Event mulai menangani terlalu banyak skenario, biasanya itu merupakan tanda bahwa Event tersebut sebaiknya dipecah menjadi beberapa Event yang lebih spesifik.
Jangan Jadikan Event Sebagai Tempat Seluruh Logika Bisnis
Event sangat baik digunakan untuk memberi tahu bahwa sebuah kejadian telah terjadi. Namun, Event bukan tempat yang tepat untuk menaruh seluruh alur bisnis aplikasi. Sebagai contoh, ketika sebuah order selesai diproses, sangat masuk akal jika Event OrderCompleted digunakan untuk:
-
mengirim email,
-
mencatat log,
-
memperbarui analytics,
-
atau menambahkan poin loyalitas.
Sebaliknya, proses inti seperti validasi pembayaran, pengecekan stok, atau penyimpanan order ke database sebaiknya tetap berada di dalam OrderService. Dengan kata lain, Event lebih cocok digunakan sebagai mekanisme notifikasi, bukan sebagai pengganti seluruh alur bisnis aplikasi.
Bagian 13: Kesalahan yang Sering Dilakukan
Setelah membahas berbagai praktik yang disarankan, ada baiknya melihat beberapa kesalahan yang cukup sering ditemui saat menggunakan Event. Sebagian besar kesalahan ini tidak selalu langsung menimbulkan bug. Namun, seiring bertambahnya ukuran aplikasi, dampaknya bisa menjadi cukup besar, mulai dari sulitnya melakukan pemeliharaan hingga munculnya masalah performa dan memory leak.
Lupa Melakukan Unsubscribe
Kesalahan yang paling umum adalah melakukan subscribe, tetapi tidak pernah melakukan unsubscribe.
C#
public class Form{public void Setup(Button button){button.Clicked += HandleClick;}private void HandleClick(object? sender, EventArgs e){}}
Sekilas kode tersebut terlihat baik-baik saja. Masalah baru muncul ketika object Form dibuat dan dihancurkan berkali-kali, sementara object Button tetap hidup selama aplikasi berjalan.
Karena Button masih menyimpan referensi ke HandleClick, object Form juga ikut tetap hidup di memori. Akibatnya, Garbage Collector tidak dapat membersihkannya meskipun secara logis object tersebut sudah tidak digunakan. Semakin sering kondisi ini terjadi, semakin banyak object yang menumpuk di memori. Oleh karena itu, setiap kali melakukan subscribe, pastikan juga terdapat tempat yang jelas untuk melakukan unsubscribe.
Menggunakan Delegate sebagai Pengganti Event
Kesalahan lain yang cukup sering ditemukan adalah mengekspos Delegate secara langsung.
C#
// Kurang disarankanpublic Action<string>? OnOrderCreated;
Pendekatan yang benar adalah menggunakan keyword event.
C#
public event Action<string>? OrderCreated;
Perbedaannya memang terlihat kecil, tetapi dampaknya cukup besar. Jika hanya menggunakan Delegate biasa, kode dari luar class dapat melakukan hal-hal berikut.
C#
orderService.OnOrderCreated = null;orderService.OnOrderCreated = SendEmail;orderService.OnOrderCreated?.Invoke("ORD-001");
Artinya, kode eksternal dapat:
-
menghapus seluruh Subscriber,
-
mengganti seluruh daftar Subscriber,
-
bahkan memicu Delegate tersebut secara langsung.
Semua ini membuat Publisher kehilangan kendali terhadap Event miliknya sendiri.
Keyword event dibuat untuk mencegah kondisi tersebut.
Mencoba Memicu Event dari Luar Class
Publisher seharusnya menjadi satu-satunya pihak yang boleh memicu Event. Karena itu, kode seperti berikut seharusnya tidak dapat dilakukan.
C#
public class OrderProcessor{public void Process(OrderService orderService){// Tidak dapat dilakukan jika menggunakan keyword event.// orderService.OrderCompleted?.Invoke(...);}}
Jika Event dideklarasikan dengan benar menggunakan keyword event, compiler akan langsung menolak kode tersebut. Inilah salah satu alasan mengapa event lebih aman dibanding Delegate biasa.
Tidak Melakukan Null Checking
Kesalahan berikutnya adalah memanggil Event tanpa memastikan bahwa Event tersebut memiliki Subscriber.
C#
// Berpotensi menghasilkan NullReferenceExceptionClicked.Invoke(this, EventArgs.Empty);
Apabila belum ada Subscriber yang terdaftar, Clicked masih bernilai null. Akibatnya, aplikasi akan menghasilkan NullReferenceException. Cara yang benar adalah menggunakan null-conditional operator.
C#
Clicked?.Invoke(this, EventArgs.Empty);
Jika tidak ada Subscriber, pemanggilan tersebut akan dilewati begitu saja tanpa menghasilkan exception.
Menggunakan Event untuk Masalah yang Terlalu Kompleks
Event dirancang sebagai mekanisme notifikasi satu arah. Publisher mengumumkan bahwa sesuatu telah terjadi, kemudian Subscriber memberikan respons. Namun, terkadang Event dipaksa menangani skenario yang sebenarnya kurang cocok. Misalnya ketika Publisher membutuhkan nilai balik dari Subscriber.
Sebagai contoh, Publisher ingin mengetahui apakah seluruh Subscriber menyetujui sebuah proses sebelum proses tersebut dilanjutkan. Walaupun hal ini masih bisa dilakukan dengan berbagai cara, biasanya solusi tersebut mulai terasa rumit dan sulit dipelihara. Contoh lain adalah ketika urutan eksekusi antar Subscriber menjadi bagian penting dari aturan bisnis.
Memang, pada implementasi Delegate di .NET, Subscriber dipanggil sesuai urutan saat didaftarkan. Namun, sebaiknya jangan menjadikan urutan tersebut sebagai bagian penting dari logika bisnis aplikasi. Jika urutan eksekusi memang harus dikendalikan secara ketat, pemanggilan method secara langsung atau penggunaan pipeline biasanya merupakan pilihan yang lebih tepat.
Kasus lain yang juga kurang cocok adalah ketika Publisher harus menunggu beberapa proses asynchronous selesai sebelum dapat melanjutkan pekerjaannya. EventHandler standar tidak dirancang untuk skenario seperti ini. Pendekatan berbasis Task atau pola asynchronous lainnya umumnya lebih sesuai.
Menganggap Event Sebagai Pengganti Semua Pola Komunikasi
Event adalah alat yang sangat berguna, tetapi bukan solusi untuk semua jenis komunikasi antar object.
Event bekerja sangat baik ketika:
-
hanya perlu memberi tahu bahwa sebuah kejadian telah terjadi,
-
Publisher tidak perlu mengetahui siapa Subscriber-nya,
-
dan Publisher tidak membutuhkan respons balik.
Jika komunikasi mulai membutuhkan alur yang kompleks, saling bergantung, atau membutuhkan hasil dari pihak lain, biasanya ada pola lain yang lebih tepat digunakan.
Bagian 14: Ringkasan
Seluruh pembahasan pada artikel ini sebenarnya berangkat dari satu pertanyaan sederhana.
Bagaimana sebuah object dapat memberi tahu object lain bahwa sesuatu telah terjadi, tanpa harus terus-menerus diperiksa?
Jawaban dari pertanyaan tersebut adalah Event. Meskipun sintaksnya terlihat sederhana, Event menyelesaikan masalah komunikasi antar object dengan cara yang rapi, aman, dan fleksibel.
Sebagai penutup, berikut beberapa poin penting yang dapat dijadikan rangkuman.
-
Event dibangun di atas Delegate. Event memanfaatkan kemampuan Delegate untuk menyimpan dan memanggil method, tetapi menambahkan pembatasan sehingga hanya Publisher yang berhak memicu Event.
-
Event merupakan implementasi Observer Design Pattern di C#. Publisher berperan sebagai Subject, Subscriber berperan sebagai Observer, sedangkan
+=,-=, danInvoke()menjadi implementasi dari proses subscribe, unsubscribe, dan notify. -
Publisher tidak perlu mengetahui siapa Subscriber-nya. Publisher hanya mengumumkan bahwa suatu kejadian telah terjadi. Siapa yang mendengarkan, berapa jumlahnya, dan apa yang dilakukan setelah menerima notifikasi bukan menjadi tanggung jawab Publisher.
-
Event membantu mengurangi coupling. Dibandingkan memanggil method satu per satu secara langsung, Event memisahkan antara "apa yang terjadi" dan "siapa yang memberikan respons". Hasilnya, aplikasi menjadi lebih mudah dikembangkan, diuji, dan dipelihara.
-
Event digunakan hampir di seluruh ekosistem .NET. Mulai dari aplikasi desktop seperti WinForms, WPF, UWP, dan .NET MAUI, hingga aplikasi backend seperti sistem notifikasi, logging, analytics, maupun proses bisnis lainnya, semuanya memanfaatkan konsep yang sama.
Selain konsep utamanya, ada beberapa hal praktis yang juga penting untuk diingat.
-
Gunakan
EventHandleratauEventHandler<TEventArgs>agar mengikuti konvensi .NET. -
Gunakan
?.Invoke()saat memicu Event untuk menghindariNullReferenceException. -
Lakukan unsubscribe ketika Subscriber sudah tidak digunakan agar terhindar dari memory leak.
-
Gunakan custom
adddanremovehanya jika memang memiliki kebutuhan khusus.
Namun, bagian yang paling berharga dari seluruh pembahasan ini bukanlah menghafal sintaks seperti event, +=, atau ?.Invoke(). Sintaks selalu bisa dicari kembali ketika dibutuhkan. Yang jauh lebih penting adalah memahami mengapa Event didesain seperti sekarang. Mengapa Publisher tidak boleh mengetahui Subscriber. Mengapa hanya Publisher yang boleh memicu Event. Mengapa komunikasi dibuat longgar (loosely coupled), bukan saling bergantung secara langsung.
Begitu alasan-alasan tersebut dipahami, Event tidak lagi terlihat sebagai sekadar fitur bahasa C#, melainkan sebagai salah satu pola komunikasi antar object yang banyak digunakan dalam pengembangan perangkat lunak.
Pemahaman tersebut juga tidak berhenti di C#. Saat berpindah ke bahasa atau framework lain, pola yang sama akan terus muncul dengan nama atau sintaks yang berbeda. Ada object yang mengumumkan sebuah kejadian, ada object lain yang mendengarkan, lalu masing-masing menjalankan tanggung jawabnya tanpa saling bergantung secara langsung.
Itulah alasan mengapa memahami Event bukan hanya membantu menulis kode C# yang lebih baik, tetapi juga membantu membangun cara berpikir dalam merancang sistem yang lebih fleksibel, mudah dikembangkan, dan lebih mudah dipelihara dalam jangka panjang.
Daftar isi
Baca juga
Artikel sebelumnya

6 Agustus 2026
•
34 menit baca
Mengupas Value Type dan Reference Type di C#
Mengapa struct sering dianggap lebih cepat daripada class? Apakah benar value type selalu berada di stack dan reference type selalu berada di heap? Artikel ini membahas konsep value type, reference type, stack, heap, Garbage Collector (GC), boxing, nullable, hingga benchmark performa secara bertahap dan mudah dipahami.
Artikel berikutnya

10 Agustus 2026
•
1 jam baca
Memahami C# Generics: Type Safety, Constraints, Factory Pattern, dan Penggunaan Nyata
Generic merupakan salah satu fitur penting dalam C# yang memungkinkan sebuah kode bekerja dengan berbagai tipe data tanpa kehilangan type safety. Artikel ini membahas Generic secara bertahap, mulai dari konsep dasar, berbagai bentuk Generic, multiple type parameter, constraint, hingga penerapannya pada collection, delegate, asynchronous programming, dan pola desain seperti Generic Repository dan Generic Factory. Pembahasan juga mencakup best practices dan kesalahan umum agar Generic dapat digunakan secara tepat tanpa menambah kompleksitas yang tidak diperlukan.