Understanding C# Event
Events in C# are a fundamental concept that many developers use but may not fully understand, particularly when it comes to creating their own events. Tim Corey provides a comprehensive guide on how to create and use events, exploring best practices and advanced features in his video "C# Events - Creating and Consuming Events in Your Application".
In this article, we are going to explore C# events, focusing on their syntax, how they are defined, and how they help solve common programming problems using Tim Corey's video examples. Understanding event handling in C# is essential for building responsive applications, and this article will help you grasp the core concepts of custom error logging, exception handling, and event-driven programming.
Introduction to Events
In C# and programming languages, events play a critical role in event-driven programming, enabling applications to respond to actions such as user interactions or system changes. Compared to other programming languages, C# offers a structured approach to handling events, making it ideal for both fast and small applications. Events in C# are triggered by specific actions, and these actions invoke corresponding event handlers to perform the desired tasks.
Tim begins by explaining that most developers are familiar with events in C# but may not know how to create custom events. The goal of the video is to introduce events, walk through creating custom events, discuss their features, and outline best practices.
Demo Application Walk-through
Tim uses a simple banking application built with Windows Forms (WinForms) to demonstrate events. The application simulates basic banking operations for one customer, including viewing balances and recording transactions.
Application Overview:
The application has two forms: one to display account balances and transactions, and another to record new transactions.
- It includes buttons to simulate credit card purchases and handle overdrafts by transferring funds from savings to checking.
Main Form:
Displays the customer's name, checking and savings account balances.
Shows lists of transactions for both accounts.
- Contains a button to open the transaction recording form.
Transaction Form:
Allows the user to enter transaction amounts.
- Simulates making purchases and handles overdrafts by transferring funds from savings to checking.
Code Behind the Demo App
Tim explains the backend code of the demo banking application:
Customer Class:
Represents a customer with properties for the customer’s name and two accounts (checking and savings).
- The Customer class is simple but serves as a good starting point for understanding how to manage multiple accounts.
Account Class:
Manages the details of a bank account, including the account name, balance, and transaction list.
The Balance property is of type decimal for precise monetary calculations.
- The Transactions property is a read-only list to prevent external modification.
Handling Deposits and Payments:
AddDeposit Method: Adds a deposit to the account, updates the balance, and logs the transaction.
- MakePayment Method: Handles withdrawals, including checking for sufficient funds and managing overdraft protection by transferring funds from a backup account if necessary.
Overdraft Protection:
- The application includes logic to handle overdrafts. If the checking account balance is insufficient for a transaction, the application checks the savings account to cover the shortfall. If the combined balance is sufficient, it transfers the required amount to the checking account and completes the transaction.
For code samples, you can refer to Tim Corey’s video directly where he explains the code from 3:18 to 18:27.
Event: Button Click
In this section, Tim Corey dived into the workings of button click events in Windows Forms, explaining how these events are wired up and how they function.
Understanding Button Click Events
Tim begins by explaining the familiar concept of button click events in Windows Forms applications. When you double-click a button in the designer, Visual Studio automatically generates an event handler method for the click event.
Event Handler Method:
- This method is called whenever the button is clicked. It is where you place the code that should run in response to the button click.
private void recordTransactionButton_Click(object sender, EventArgs e) { // Code to handle the button click event }
private void recordTransactionButton_Click(object sender, EventArgs e) { // Code to handle the button click event }
Wiring Up the Event:
- The event handler method is wired up to the button click event in the form's designer file (FormName.Designer.cs). This is done using the += operator to add the event handler to the button's click event.
this.recordTransactionButton.Click += new System.EventHandler(this.recordTransactionButton_Click);
this.recordTransactionButton.Click += new System.EventHandler(this.recordTransactionButton_Click);
Creating and Invoking Custom Events
Tim explains how to create custom events in C#, starting with the Account class where events will be triggered.
Defining an Event:
- An event is defined using the event keyword. It looks similar to a public variable but is specifically for events.
public event EventHandler<string> RaiseTransactionApprovedEvent;
public event EventHandler<string> RaiseTransactionApprovedEvent;
Triggering the Event:
- Events are triggered using the Invoke method, typically within the class that defines the event. The Invoke method takes two parameters: the sender (usually this) and event data (in this case, a string).
private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); }
private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); }
Wiring Up and Handling Events:
- Subscribe to the event using the += operator and define a method to handle the event when it is raised.
account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent; private void Account_RaiseTransactionApprovedEvent(object sender, string e) { // Code to handle the event }
account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent; private void Account_RaiseTransactionApprovedEvent(object sender, string e) { // Code to handle the event }
Using Events to Update the UI
Tim explains how to use events to automatically update the browser UI when certain actions occur, such as when a transaction is approved.
Raising Events:
- Trigger the custom event after a transaction is approved.
public void AddDeposit(decimal amount, string depositName) { // Code to add deposit RaiseTransactionApprovedEvent?.Invoke(this, depositName); }
public void AddDeposit(decimal amount, string depositName) { // Code to add deposit RaiseTransactionApprovedEvent?.Invoke(this, depositName); }
Subscribing to Events in the UI:
- Subscribe to the event in the main form to update the transaction list whenever a new transaction is approved.
public MainForm() { InitializeComponent(); account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent; } private void Account_RaiseTransactionApprovedEvent(object sender, string e) { // Update the UI with the new transaction }
public MainForm() { InitializeComponent(); account.RaiseTransactionApprovedEvent += Account_RaiseTransactionApprovedEvent; } private void Account_RaiseTransactionApprovedEvent(object sender, string e) { // Update the UI with the new transaction }
Event?.Invoke() Explained
Tim Corey explains the use of the ?.Invoke() syntax when raising events in C#. This modern approach simplifies the code and ensures thread safety.
The Question Mark Operator
The question mark (?.) before Invoke is a null-conditional operator. It checks if the event handler is null before invoking it, preventing potential exceptions.
Traditional Approach:
- Previously, developers used multiple lines of code to check if the event handler was null and then invoke it.
if (RaiseTransactionApprovedEvent != null) { RaiseTransactionApprovedEvent(this, depositName); }
if (RaiseTransactionApprovedEvent != null) { RaiseTransactionApprovedEvent(this, depositName); }
Modern Approach with ?.Invoke():
- The null-conditional operator streamlines this process, performing the null check and invoking the event in one line.
RaiseTransactionApprovedEvent?.Invoke(this, depositName);
RaiseTransactionApprovedEvent?.Invoke(this, depositName);
- If RaiseTransactionApprovedEvent is null, the invocation does not proceed, effectively avoiding any exceptions.
Benefits:
Simplifies Code: Reduces the amount of code needed to safely invoke events.
- Thread Safety: Eliminates race conditions by checking for null and invoking the event in a single step.
Listening to and Writing Code for the Event
Tim explains how to listen to custom events in a Windows Forms application and update the UI accordingly.
Subscribing to Events:
- Subscribe to the custom event using the += operator.
customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent;
customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent;
Event Handler Method:
- Define a method to handle the event. This method updates the UI based on the event data.
private void CheckingAccount_TransactionApprovedEvent(object sender, string e) { // Update the UI with the new transaction checkingTransactionsDataSource.DataSource = null; checkingTransactionsDataSource.DataSource = customer.CheckingAccount.Transactions; checkingBalanceLabel.Text = customer.CheckingAccount.Balance.ToString("C2"); }
private void CheckingAccount_TransactionApprovedEvent(object sender, string e) { // Update the UI with the new transaction checkingTransactionsDataSource.DataSource = null; checkingTransactionsDataSource.DataSource = customer.CheckingAccount.Transactions; checkingBalanceLabel.Text = customer.CheckingAccount.Balance.ToString("C2"); }
Creating the Custom Event: Event in Action and Recap
Tim demonstrates the entire process of creating, raising, and handling custom events in action.
Triggering the Event:
- Raise the event after a transaction is approved or when the balance changes.
private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); }
private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); }
Handling Multiple Events:
- Ensure the application listens to multiple events, such as transactions and balance changes, to keep the UI updated in real-time.
public MainForm() { InitializeComponent(); customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent; customer.SavingsAccount.TransactionApprovedEvent += SavingsAccount_TransactionApprovedEvent; } private void SavingsAccount_TransactionApprovedEvent(object sender, string e) { // Update the UI with the new savings transaction savingsTransactionsDataSource.DataSource = null; savingsTransactionsDataSource.DataSource = customer.SavingsAccount.Transactions; savingsBalanceLabel.Text = customer.SavingsAccount.Balance.ToString("C2"); }
public MainForm() { InitializeComponent(); customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent; customer.SavingsAccount.TransactionApprovedEvent += SavingsAccount_TransactionApprovedEvent; } private void SavingsAccount_TransactionApprovedEvent(object sender, string e) { // Update the UI with the new savings transaction savingsTransactionsDataSource.DataSource = null; savingsTransactionsDataSource.DataSource = customer.SavingsAccount.Transactions; savingsBalanceLabel.Text = customer.SavingsAccount.Balance.ToString("C2"); }
Running the Application:
Tim runs the application to demonstrate how the UI updates automatically based on events triggered by transactions.
Event Argument Information: Debugging
Tim shows how to debug events and inspect the information sent with them.
Setting a Breakpoint:
- Set a breakpoint in the event handler method to inspect the event data.
private void CheckingAccount_TransactionApprovedEvent(object sender, string e) { // Breakpoint here }
private void CheckingAccount_TransactionApprovedEvent(object sender, string e) { // Breakpoint here }
Debugging:
Run the application and perform a transaction to trigger the event. Inspect the event arguments in the debugger.
- The sender parameter provides the instance that raised the event, and the e parameter contains the event data.
Creating Another Custom Event (Overdraft Event)
Tim Corey expands on the demo by creating another custom event to handle overdraft situations. This event will be triggered when an overdraft occurs and will notify the user of the overdraft amount.
Defining the Overdraft Event
Tim starts by defining a new event in the Account class for overdraft scenarios:
Event Declaration:
- Define the event using the event keyword with a decimal type to pass the overdraft amount.
public event EventHandler<decimal> OverdraftEvent;
public event EventHandler<decimal> OverdraftEvent;
Triggering the Event:
- The event is triggered in the part of the code where an overdraft successfully occurs.
if (overdraftSuccessful) { OverdraftEvent?.Invoke(this, overdraftAmount); }
if (overdraftSuccessful) { OverdraftEvent?.Invoke(this, overdraftAmount); }
Subscribing to the Overdraft Event on the Dashboard Side
Wiring Up the Event:
- Subscribe to the overdraft event in the Dashboard form.
customer.CheckingAccount.OverdraftEvent += CheckingAccount_OverdraftEvent;
customer.CheckingAccount.OverdraftEvent += CheckingAccount_OverdraftEvent;
Handling the Event:
- Define an event handler to display a message when an overdraft occurs.
private void CheckingAccount_OverdraftEvent(object sender, decimal e) { errorMessage.Text = $"You had an overdraft protection transfer of {e:C2}"; errorMessage.Visible = true; }
private void CheckingAccount_OverdraftEvent(object sender, decimal e) { errorMessage.Text = $"You had an overdraft protection transfer of {e:C2}"; errorMessage.Visible = true; }
Triggering and Displaying the Event:
- When an overdraft occurs, the event triggers and updates the UI to notify the user.
You had an overdraft protection transfer of $20.44
You had an overdraft protection transfer of $20.44
Listening for the Event in Multiple Places
Tim explains how to listen to the same event in multiple languages and forms, demonstrating the versatility of events.
Adding a Label to Another Form:
- Add a label to a secondary form to display overdraft messages.
errorMessage.Visible = false;
errorMessage.Visible = false;
Subscribing to the Event:
- Subscribe to the overdraft event in the secondary form.
customer.CheckingAccount.OverdraftEvent += SecondaryForm_OverdraftEvent;
customer.CheckingAccount.OverdraftEvent += SecondaryForm_OverdraftEvent;
Handling the Event:
- Define an event handler to display the overdraft message on the secondary form.
private void SecondaryForm_OverdraftEvent(object sender, decimal e) { errorMessage.Visible = true; }
private void SecondaryForm_OverdraftEvent(object sender, decimal e) { errorMessage.Visible = true; }
Simultaneous Event Handling:
- Tim demonstrates that the event can be handled in multiple forms simultaneously, ensuring that all relevant parts of the application respond to the event appropriately.
Both the main form and the secondary form display the overdraft message when the event is triggered.
Both the main form and the secondary form display the overdraft message when the event is triggered.
Removing Event Listeners from Memory
Tim highlights the importance of cleaning up event listeners to prevent memory leaks and ensure proper application performance.
Unsubscribing from Events:
- It is crucial to unsubscribe from events before destroying a class instance or closing a form.
customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;
customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;
Why It's Important:
- Failing to unsubscribe from events can cause memory leaks, as the objects listening for events may not be properly garbage collected.
Using Named Methods:
- Avoid using anonymous functions for event handlers, as it makes it difficult to unsubscribe from the events.
// Good practice: using named methods for event handlers
// Good practice: using named methods for event handlers
Generic EventHandler: Passing a Class for T
Tim Corey explains the best practices for passing data through events, particularly the benefits of using a class rather than simple data types like string or decimal.
Why Use a Class for Event Data
Tim starts by discussing why using simple data types for events is less common and why it's typically better to pass a class:
Flexibility and Scalability:
- Using a class allows you to pass multiple pieces of related data through an event. If you need to add more data later, you can simply extend the class without changing the event's signature.
EventArgs Inheritance:
- While it used to be required that any object passed through an event had to inherit from EventArgs, this is no longer the case. However, inheriting from EventArgs can still be beneficial for consistency and clarity.
Creating the Overdraft EventArgs Class
Tim demonstrates how to create a custom EventArgs class for the overdraft event.
Define the Class:
- Create a new class that inherits from EventArgs and includes properties for the data you want to pass through the event.
public class OverdraftEventArgs : EventArgs { public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; } public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo) { AmountOverdrafted = amountOverdrafted; MoreInfo = moreInfo; } }
public class OverdraftEventArgs : EventArgs { public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; } public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo) { AmountOverdrafted = amountOverdrafted; MoreInfo = moreInfo; } }
Using the Class in the Event:
- Update the event declaration to use the custom EventArgs class.
public event EventHandler<OverdraftEventArgs> OverdraftEvent;
public event EventHandler<OverdraftEventArgs> OverdraftEvent;
Triggering the Event:
- Pass an instance of the custom EventArgs class when invoking the event.
OverdraftEvent?.Invoke(this, new OverdraftEventArgs(amountNeeded, "Additional info"));
OverdraftEvent?.Invoke(this, new OverdraftEventArgs(amountNeeded, "Additional info"));
Importance of Read-Only Properties
Tim emphasizes the importance of using read-only properties in the EventArgs class to prevent accidental modification of event data.
Avoiding Modifications:
If properties in the EventArgs class have public setters, any event handler can modify the data, which can lead to unexpected behavior.
- Using private setters and initializing properties through the constructor ensures that event data remains consistent.
Example of the Problem:
- Tim demonstrates how modifying event data in one event handler can affect other handlers if properties are not read-only.
private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { e.AmountOverdrafted = 1000; // This modification affects all handlers }
private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { e.AmountOverdrafted = 1000; // This modification affects all handlers }
Solution:
- Use private setters and pass data through the constructor to make properties read-only.
public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; }
public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; }
Exception When to Use Public Set (59:29)
Tim Corey highlights an important exception to the rule of using private setters for event data properties. This exception is when you need to allow event listeners to modify the event data, such as in cases where a transaction might be canceled based on certain conditions.
Example: Canceling a Transaction
Tim provides an example where an event handler might need to cancel a transaction. This is achieved by adding a CancelTransaction property to the custom EventArgs class.
Defining the Property:
- Add a public property with both a getter and setter to the EventArgs class.
public class OverdraftEventArgs : EventArgs { public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; } public bool CancelTransaction { get; set; } = false; public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo) { AmountOverdrafted = amountOverdrafted; MoreInfo = moreInfo; } }
public class OverdraftEventArgs : EventArgs { public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; } public bool CancelTransaction { get; set; } = false; public OverdraftEventArgs(decimal amountOverdrafted, string moreInfo) { AmountOverdrafted = amountOverdrafted; MoreInfo = moreInfo; } }
Setting the Property in the Event Handler:
- In the dashboard, the event handler can set this property to cancel the transaction.
private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { if (denyOverdraft.Checked) { e.CancelTransaction = true; } errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}"; errorMessage.Visible = true; }
private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { if (denyOverdraft.Checked) { e.CancelTransaction = true; } errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}"; errorMessage.Visible = true; }
Checking the Property in the Source Method:
- In the method that triggers the event, check if the CancelTransaction property is set to true before proceeding.
if (args.CancelTransaction) { return false; // Transaction is canceled }
if (args.CancelTransaction) { return false; // Transaction is canceled }
Making the Application More Interactive
Tim further refines the application to make it more interactive and user-friendly.
Adding a Checkbox for Overdraft Control:
- Add a checkbox to the form that allows the user to enable or disable overdraft protection.
private void InitializeComponent() { this.denyOverdraft = new System.Windows.Forms.CheckBox(); // Initialize other controls this.denyOverdraft.Text = "Stop Overdrafts"; this.denyOverdraft.CheckedChanged += new System.EventHandler(this.denyOverdraft_CheckedChanged); }
private void InitializeComponent() { this.denyOverdraft = new System.Windows.Forms.CheckBox(); // Initialize other controls this.denyOverdraft.Text = "Stop Overdrafts"; this.denyOverdraft.CheckedChanged += new System.EventHandler(this.denyOverdraft_CheckedChanged); }
Handling the Checkbox State:
- In the event handler for the checkbox, update the logic to consider the checkbox state when deciding to cancel a transaction.
private void denyOverdraft_CheckedChanged(object sender, EventArgs e) { if (denyOverdraft.Checked) { // Logic to stop overdraft transactions } }
private void denyOverdraft_CheckedChanged(object sender, EventArgs e) { if (denyOverdraft.Checked) { // Logic to stop overdraft transactions } }
Updating the Event Handler:
- Ensure the event handler respects the checkbox state to allow or deny overdrafts.
private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { if (denyOverdraft.Checked) { e.CancelTransaction = true; } errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}"; errorMessage.Visible = true; }
private void CheckingAccount_OverdraftEvent(object sender, OverdraftEventArgs e) { if (denyOverdraft.Checked) { e.CancelTransaction = true; } errorMessage.Text = $"You had an overdraft protection transfer of {e.AmountOverdrafted:C2}"; errorMessage.Visible = true; }
Summary
Tim Corey wraps up the tutorial by summarizing key points and best practices for working with events in C#.
Remove Event Listeners:
Always remove event listeners before destroying objects to prevent memory leaks.
- Use the -= operator to unsubscribe from events.
customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;
customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;
Use EventArgs Inheritance:
- While not mandatory, inheriting from EventArgs can be beneficial for consistency and using built-in features like EventArgs.Empty.
Private Setters for Read-Only Properties:
- Use private setters to prevent unintended modifications to event data. Only allow public setters when necessary, such as for cancelable transactions.
Event Handler Syntax:
- Use the EventHandler
delegate to define events, providing a clear and consistent pattern for passing event data.
- Use the EventHandler
Null-Conditional Operator:
- Use the null-conditional operator (?.Invoke()) to safely invoke events without risking null reference exceptions.
Conclusion
Tim Corey’s comprehensive tutorial on C# events provides valuable insights and practical examples for creating, handling, and managing events effectively. By following these best practices, developers can create more interactive and responsive applications.