理解 C# 事件
C# 中的事件是許多開發者使用但可能未完全了解的基本概念,尤其是在創建自己的事件時。 Tim Corey 提供了一個全面的指南,教您如何創建和使用事件,探討在影片"C# Events - Creating and Consuming Events in Your Application"中好的做法和進階功能。
在這篇文章中,我們將探討 C# 的事件,重點在於其語法,如何定義,以及如何利用 Tim Corey 的影片範例解決常見的程式問題。 理解 C# 中的事件處理對於建構響應式應用程式至關重要,這篇文章將幫助您掌握自訂錯誤日誌、異常處理以及事件驅動程式設計的核心概念。
事件介紹
在 C# 和程式語言中,事件在事件驅動程式設計中扮演著關鍵角色,使應用程式能夠響應使用者互動或系統變更等行動。 與其他程式語言相比,C# 提供一種結構化的方法來處理事件,這使其成為開發快速和小型應用程式的理想選擇。 C# 中的事件由具體行動觸發,這些行動調用相應的事件處理程式以執行預期的任務。
Tim 開始解釋大多數開發者對於 C# 中的事件都很熟悉,但可能不知道如何創建自訂事件。 這段影片的目標是介紹事件,逐步示範創建自訂事件,討論其功能,並概述最佳實踐。
演示應用程序走過
Tim 使用了一個由 Windows Forms (WinForms) 建置的簡單銀行應用程式來演示事件。 該應用程式模擬了一位客戶的基礎銀行操作,包括查看餘額和記錄交易。
-
應用程式概況:
- 該應用程式有兩個窗體:一個用於顯示帳戶餘額和交易,另一個用於記錄新交易。
- 它包括模擬信用卡購物的按鈕,並通過從儲蓄轉移資金到支票來處理透支。
-
主窗體:
- 顯示客戶的姓名、支票和儲蓄帳戶餘額。
- 顯示兩個帳戶的交易列表。
- 包含一個按鈕以打開交易記錄窗體。
-
交易窗體:
- 允許用戶輸入交易金額。
- 模擬購物並通過從儲蓄轉移資金到支票來處理透支。
示範應用程式背後的代碼
Tim 解釋了該示範銀行應用程式的後端代碼:
-
客戶類別:
- 表示一個客戶,具有客戶姓名和兩個帳戶(支票和儲蓄)的屬性。
- 客戶類別很簡單,但它是理解如何管理多個帳戶的良好起點。
-
帳戶類別:
- 管理銀行帳戶的詳細信息,包括帳戶名稱、餘額和交易列表。
- 餘額屬性是十進制型別,用於精確的貨幣計算。
- 交易屬性是唯讀列表,以防止外部修改。
-
處理存款和付款:
- 新增存款方法:將存款新增到帳戶,更新餘額,並記錄交易。
- 付款方法:處理提款,包括檢查是否有足夠的資金以及如果需要,通過從備用帳戶轉移資金來管理透支保護。
-
透支保護:
- 該應用程式包含處理透支的邏輯。 如果支票帳戶餘額不足以進行交易,應用程式會檢查儲蓄帳戶以補足不足之處。 如果合併餘額足夠,則將所需金額轉移到支票帳戶並完成交易。
要查看代碼示例,您可以直接參考 Tim Corey 的影片,他在從 3:18 到 18:27 解釋了代碼。
事件:按鈕點擊
在這一部分,Tim Corey 深入講解了 Windows Forms 中按鈕點擊事件的工作原理,解釋了這些事件是如何接線並如何工作。
了解按鈕點擊事件
Tim 開始解釋 Windows Forms 應用程式中按鈕點擊事件的熟悉概念。 當您在設計器中雙擊按鈕時,Visual Studio 會自動為點擊事件生成一個事件處理方法。
-
事件處理方法:
- 每當按鈕被點擊,這個方法就會被調用。 在這裡,您可以放置應該在按鈕點擊時運行的代碼。
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 } -
接線事件:
- 事件處理方法是在窗體的設計器文件(FormName.Designer.cs)中接線到按鈕點擊事件的。 這是通過使用 += 符號將事件處理程式新增到按鈕的點擊事件中完成的。
this.recordTransactionButton.Click += new System.EventHandler(this.recordTransactionButton_Click);this.recordTransactionButton.Click += new System.EventHandler(this.recordTransactionButton_Click);
創建和調用自訂事件
Tim 解釋如何在 C# 中創建自訂事件,從事件將被觸發的帳戶類別開始。
-
定義事件:
- 使用 event 關鍵字定義一個事件。 它看起來類似於公用變數,但專門用於事件。
public event EventHandler<string> RaiseTransactionApprovedEvent;public event EventHandler<string> RaiseTransactionApprovedEvent; -
觸發事件:
- 使用 Invoke 方法觸發事件,通常在定義事件的類別中。 Invoke 方法接受兩個參數:發送者(通常是
this)和事件數據(在此情況下為字串)。
private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); }private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); } - 使用 Invoke 方法觸發事件,通常在定義事件的類別中。 Invoke 方法接受兩個參數:發送者(通常是
-
接線和處理事件:
- 使用 += 符號訂閱事件,並定義一個在事件引發時處理它的方法。
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 }
使用事件更新 UI
Tim 解釋如何使用事件自動更新瀏覽器 UI 當某些動作發生時,例如交易核准時。
-
引發事件:
- 在交易核准後觸發自訂事件。
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); } -
在 UI 中訂閱事件:
- 在主窗體中訂閱事件,以在新交易被核准時更新交易列表。
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() 解釋
Tim Corey 解釋了在 C# 中引發事件時使用 ?.Invoke() 語法的用法。 這種現代方法簡化了代碼並可確保線程安全。
問號運算符
問號(?.)在 Invoke 之前是空條件運算符。 它在調用前檢查事件處理方法是否為 null,以防止潛在的異常。
-
傳統方法:
- 之前,開發者使用多行代碼來檢查事件處理方法是否為 null,然後調用它。
if (RaiseTransactionApprovedEvent != null) { RaiseTransactionApprovedEvent(this, depositName); }if (RaiseTransactionApprovedEvent != null) { RaiseTransactionApprovedEvent(this, depositName); } -
現代方法與 ?.Invoke():
- 空條件運算符簡化了這個過程,在一行之內完成空檢查並調用事件。
RaiseTransactionApprovedEvent?.Invoke(this, depositName);RaiseTransactionApprovedEvent?.Invoke(this, depositName);- 如果
RaiseTransactionApprovedEvent是 null,則不繼續調用,從而避免任何異常。
-
優點:
- 簡化代碼:減少了安全調用事件所需的代碼量。
- 線程安全:通過在一個步驟內檢查空值並調用事件來消除競態情況。
傾聽並編寫事件代碼
Tim 解釋如何在 Windows Forms 應用程式中監聽自訂事件並相應地更新 UI。
-
訂閱事件:
- 使用 += 符號訂閱自訂事件。
customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent;customer.CheckingAccount.TransactionApprovedEvent += CheckingAccount_TransactionApprovedEvent; -
事件處理方法:
- 定義一個方法來處理事件。 此方法基於事件數據更新 UI。
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"); }
創建自訂事件:事件實踐與回顧
Tim 展示了創建、引發和處理自訂事件的整個過程。
-
觸發事件:
- 在交易核准或餘額變化後,引發事件。
private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); }private void OnTransactionApproved(string transactionName) { RaiseTransactionApprovedEvent?.Invoke(this, transactionName); } -
處理多個事件:
- 確保應用程式監聽多個事件,例如交易和餘額變化,以保持 UI 實時更新。
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"); } -
運行應用程式:
- Tim 代碼演示了如何基於由交易引發的事件自動更新 UI。
事件參數信息:偵錯
Tim 展示了如何偵錯事件並檢查隨事件傳遞的信息。
-
設置斷點:
- 在事件處理方法中設置斷點以檢查事件數據。
private void CheckingAccount_TransactionApprovedEvent(object sender, string e) { // Breakpoint here }private void CheckingAccount_TransactionApprovedEvent(object sender, string e) { // Breakpoint here } -
偵錯:
- 運行應用程式並執行同意來觸發事件。 在調試器中檢查事件參數。
- sender 參數提供了引發事件的實體,e 參數包含了事件數據。
創建另一個自訂事件(透支事件)
Tim Corey 通過創建處理透支情況的另一個自訂事件來擴展示範。 這個事件將在透支發生時觸發,並通知用戶透支金額。
定義透支事件
Tim 開始在帳戶類別中為透支場景定義新事件:
-
事件宣告:
- 使用 event 關鍵字定義事件,並使用十進制型別傳遞透支金額。
public event EventHandler<decimal> OverdraftEvent;public event EventHandler<decimal> OverdraftEvent; -
觸發事件:
- 事件在透支成功發生的代碼部分中觸發。
if (overdraftSuccessful) { OverdraftEvent?.Invoke(this, overdraftAmount); }if (overdraftSuccessful) { OverdraftEvent?.Invoke(this, overdraftAmount); }
在儀表板側訂閱透支事件
-
接線事件:
- 在儀表板窗體中訂閱透支事件。
customer.CheckingAccount.OverdraftEvent += CheckingAccount_OverdraftEvent;customer.CheckingAccount.OverdraftEvent += CheckingAccount_OverdraftEvent; -
處理事件:
- 定義一個事件處理方法,以在透支發生時顯示訊息。
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; } -
觸發並顯示事件:
- 當透支發生時,事件觸發並更新 UI 以通知用戶。
You had an overdraft protection transfer of $20.44
在多個地點監聽事件
Tim 解釋如何在多種語言和窗體中監聽同一事件,展示事件的多功能性。
-
在另一窗體上新增標籤:
- 在次級窗體上新增標籤以顯示透支訊息。
<asp:Label ID="errorMessage" runat="server" Visible="false" /><asp:Label ID="errorMessage" runat="server" Visible="false" />HTML -
訂閱事件:
- 在次級窗體中訂閱透支事件。
customer.CheckingAccount.OverdraftEvent += SecondaryForm_OverdraftEvent;customer.CheckingAccount.OverdraftEvent += SecondaryForm_OverdraftEvent; -
處理事件:
- 定義一個事件處理方法,在次級窗體上顯示透支訊息。
private void SecondaryForm_OverdraftEvent(object sender, decimal e) { errorMessage.Visible = true; }private void SecondaryForm_OverdraftEvent(object sender, decimal e) { errorMessage.Visible = true; } -
同時事件處理:
- Tim 展示事件可以在多個窗體中同時處理,確保應用程式的所有相關部分適當地回應事件。
Both the main form and the secondary form display the overdraft message when the event is triggered.
從記憶體中移除事件監聽器
Tim 強調清理事件監聽器以防止記憶洩漏並確保應用程式性能的重要性。
-
取消訂閱事件:
- 在銷毀類別實例或關閉窗體之前,必須取消訂閱事件。
customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;為什麼這很重要:
- 未能取消訂閱事件可能會導致記憶洩漏,因為正在監聽事件的對象可能無法被正確地垃圾收集。
-
使用命名方法:
- 避免使用匿名函數作為事件處理方法,因為這使得從事件中取消訂閱變得困難。
// Good practice: using named methods for event handlers// Good practice: using named methods for event handlers
泛型 EventHandler:為 T 傳遞類別
Tim Corey 解釋了通過事件傳遞數據的最佳實踐,特別是使用類別而非簡單數據類型(如字串或十進制)的好處。
為什麼要為事件數據使用類別
Tim 開始討論為什麼使用簡單數據類型較不常見,以及為什麼通常最好傳遞一個類別:
-
靈活性和可擴展性:
- 使用類別允許您在事件中傳遞多個相關聯的數據。 如果您需要稍後新增更多數據,只需擴展類別而無需更改事件的簽章。
-
EventArgs 繼承:
- 雖然以前任何通過事件傳遞的對象必須從 EventArgs 繼承,但這已不再是必需的。 不過,從 EventArgs 繼承仍然有助於一致性和清晰性。
創建透支 EventArgs 類別
Tim 示範如何為透支事件創建自訂 EventArgs 類別。
-
定義類別:
- 創建一個從 EventArgs 繼承的新類別,並包含您想要透過事件傳遞的數據屬性。
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; } } -
在事件中使用類別:
- 更新事件宣言以使用自訂的 EventArgs 類別。
public event EventHandler<OverdraftEventArgs> OverdraftEvent;public event EventHandler<OverdraftEventArgs> OverdraftEvent; -
觸發事件:
- 在調用事件時傳遞自訂 EventArgs 類別的實例。
OverdraftEvent?.Invoke(this, new OverdraftEventArgs(amountNeeded, "Additional info"));OverdraftEvent?.Invoke(this, new OverdraftEventArgs(amountNeeded, "Additional info"));
唯讀屬性的重要性
Tim 強調在 EventArgs 類別中使用唯讀屬性以防止事件數據的意外修改的重要性。
-
避免修改:
- 如果 EventArgs 類別中的屬性具有公用 setter,則任何事件處理方法都可以修改數據,這可能導致意外行為。
- 使用私有 setter 並透過構造函數初始化屬性確保事件數據保持一致性。
-
問題範例:
- Tim 示範了在一個事件處理方法中修改事件數據如何影響其他處理方法,若屬性不是唯讀的。
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 } -
解決方案:
- 使用私有 setter 並透過構造函數傳遞數據以使屬性唯讀。
public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; }public decimal AmountOverdrafted { get; private set; } public string MoreInfo { get; private set; }
何時使用 Public Set (59:29) 的例外情況
Tim Corey 突出了一個重要的例外情況,使用私有 setter 的規則對於事件數據屬性。 這一例外情況是當您需要允許事件監聽器修改事件數據時,例如在可能根據某些條件取消交易的情況下。
範例:取消交易
Tim 提供了一個範例,其中事件處理方法可能需要取消交易。 這是透過在自訂 EventArgs 類別中新增 CancelTransaction 屬性來實現的。
-
定義屬性:
- 在 EventArgs 類別中新增一個同時具有 getter 和 setter 的公用屬性。
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; } } -
在事件處理方法中設置屬性:
- 在儀表板中,事件處理方法可以設置此屬性以取消交易。
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; } -
在源方法中檢查屬性:
- 在引發事件的方法中,檢查 CancelTransaction 屬性是否為 true 再繼續進行。
if (args.CancelTransaction) { return false; // Transaction is canceled }if (args.CancelTransaction) { return false; // Transaction is canceled }
讓應用程式更具互動性
Tim 進一步優化應用程式,使其更具互動性和用戶友好性。
-
新增透支控制的複選框:
- 在窗體中新增一個複選框,允許用戶啟用或停用透支保護。
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); } -
處理複選框狀態:
- 在複選框的事件處理方法中,在決定取消交易時更新邏輯以考慮到複選框狀態。
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 } } -
更新事件處理方法:
- 確保事件處理方法考慮複選框狀態以允許或拒絕透支。
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; }
總結
Tim Corey 通過總結工作 C# 事件的關鍵點和最佳實踐來結束教學。
-
移除事件監聽器:
- 始終在銷毀對象之前移除事件監聽器,以防止記憶洩漏。
- 使用 -= 符號取消訂閱事件。
customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent;customer.CheckingAccount.OverdraftEvent -= CheckingAccount_OverdraftEvent; -
使用 EventArgs 繼承:
- 雖非必需,從 EventArgs 繼承有助於一致性,並使用內建功能如 EventArgs.Empty。
-
唯讀屬性的私有 Setter:
- 使用私有 setter 防止對事件數據的意外修改。 僅在必要時允許公用 setter,例如對於可取消的交易。
-
事件處理方法語法:
- 使用 EventHandler 委託來定義事件,提供一個明確一致的模式來傳遞事件數據。
-
空條件運算符:
- 使用空條件運算符(?.Invoke())來安全地調用事件,而不會冒 null 引用例外的風險。
結論
Tim Corey 的C# 事件綜合教程提供了寶貴的見解和實用的範例,幫助您有效地創建、處理和管理事件。 通過遵循這些最佳實踐,開發者可以創建更具互動性和響應性的應用程式。
