上一篇連上 TiDB Cloud之後,你的程式裡多半有一行這樣的呼叫:
MySqlDbHelper.InitDb(); // 建好資料表問題來了:這一行跑完,你怎麼知道它成功還是失敗?
雲端資料庫比本機 SQLite 多了一堆會出錯的地方——SSL 沒設好、Port 打成 3306、密碼複製時漏字、region 太遠連逾時。這些狀況下 InitDb() 會失敗,但如果它只是一個回傳 void 的方法,呼叫它的人根本收不到任何消息,程式就這樣帶著一個沒建好的資料庫繼續往下跑,直到後面某個查詢爆炸,你才回頭找是哪裡出事。
這篇要解決的就是這件事:讓 InitDb() 在做完之後主動告訴呼叫端「我成功了」或「我失敗了,原因是這個」。C# 用來做這件事的機制叫事件(event)。
先看問題:void 方法是個黑洞
這是 SmartInventory 專案裡 SQLite 版的建表方法,很典型:
public static void InitDb()
{
using (var conn = new SqliteConnection(connStr))
{
conn.Open();
string sql = "CREATE TABLE IF NOT EXISTS Products ( ... )";
using (var cmd = new SqliteCommand(sql, conn))
{
cmd.ExecuteNonQuery();
}
}
}呼叫端是這樣用的:
DbHelper.InitDb();
all = DbHelper.GetAllProducts();
RefreshView();看得出盲點嗎?InitDb() 回傳 void,所以呼叫端只能假設它成功了,直接往下 GetAllProducts()。本機 SQLite 幾乎不會失敗,這樣寫沒事。但換成雲端的 MySqlDbHelper.InitDb(),連線隨時可能失敗,這種「做完就沒下文」的寫法就變成一個黑洞——出錯了也沒人知道。
你可能會想到幾種土法,但每種都有問題:
- 改成回傳 `bool`:能知道成功與否,但不知道為什麼失敗。使用者看到「初始化失敗」卻沒有原因,等於沒講。
- 在 `InitDb` 裡直接 `MessageBox.Show`:訊息是出來了,但你把「資料庫工具」跟「WinForms 畫面」綁死了。這個 Helper 以後就沒辦法用在主控台程式、也沒辦法寫測試——MVP 那篇講過,負責資料的類別不該碰畫面。
- 丟 `Exception`:語意對,但呼叫端每次呼叫都得包一層
try/catch,而且例外應該留給「真正意外」的狀況,不該拿來當一般的結果回報。
事件是這個問題的標準解法:工具類負責「廣播」發生了什麼事,誰想知道就自己「訂閱」。工具不需要知道誰在聽,聽的人也不必守在那裡等。
C# 事件:廣播、訂閱,三個動作
先建立直覺,再看語法。
事件就像廣播電台。MySqlDbHelper 是電台,它做完建表後對外喊一聲「初始化結束了,結果是這樣」。它不在乎有沒有人在聽、有幾個人在聽——它只負責喊。MainForm 是聽眾,它事先「訂閱」了這個電台,電台一喊它就收到,然後決定要做什麼(例如跳訊息框)。
這個安排的好處是兩邊互不認識:MySqlDbHelper 完全不知道有 WinForms 的存在,它可以被主控台程式訂閱、被測試訂閱、被三個不同的畫面同時訂閱;而 MainForm 也不必去改 MySqlDbHelper 的內部——它只是個聽眾。
程式碼只有三個部分:宣告事件、發送事件、訂閱事件。以下都是 SmartInventory 專案裡的真實寫法。
1. 宣告事件(在工具類裡)
public static event EventHandler<(bool IsSuccess, string Message)>? OnDbInitResult;拆開來看:
event:關鍵字,宣告這是一個事件。EventHandler<T>:C# 內建的事件型別,<T>是「這個事件會夾帶什麼資料」。- :夾帶的資料——一個 ,同時裝「成功了嗎」和「訊息是什麼」兩個值。這正好補上了前面 方案缺的那一半:不只知道成敗,還帶著原因。
跑起來看看
用一個主控台程式把整條路走一遍(這裡用 SQLite,不需要雲端帳號密碼就能重現)。工具類跟前面 MySqlDbHelper 的事件寫法一模一樣:
using Microsoft.Data.Sqlite;
public static class DbHelper
{
public static event EventHandler<(bool IsSuccess, string Message)>? OnDbInitResult;
public static void InitDb(string connStr)
{
try
{
using (var conn = new SqliteConnection(connStr))
{
conn.Open();
string sql = """
CREATE TABLE IF NOT EXISTS Products (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Name TEXT,
Category TEXT,
Quantity INTEGER,
Price REAL
);
""";
using (var cmd = new SqliteCommand(sql, conn))
{
cmd.ExecuteNonQuery();
OnDbInitResult?.Invoke(null, (true, "資料庫開啟成功!"));
}
}
}
catch (Exception ex)
{
OnDbInitResult?.Invoke(null, (false, $"資料庫初始化失敗:{ex.Message}"));
}
}
}真正用在 WinForms
回到 SmartInventory 的 MainForm。它的建構式現在長這樣(節錄):
public MainForm()
{
InitializeComponent();
// ...其他初始化...
// 先訂閱,再呼叫 InitDb
MySqlDbHelper.OnDbInitResult += MySqlDbHelper_OnDbInitResult;
MySqlDbHelper.InitDb();
DbHelper.InitDb();
all = DbHelper.GetAllProducts();
RefreshView();
}
private void MySqlDbHelper_OnDbInitResult(object? sender, (bool IsSuccess, string Message) e)
{
MessageBox.Show(e.Message);
}停下來檢查:你的連線字串有沒有藏著真密碼
這件事跟事件無關,但它是這個範例最容易出的意外,一定要單獨提醒一次。
MySqlDbHelper 裡的連線字串長這樣:
private static string connStr =
"Server=gateway01.xxxx.prod.aws.tidbcloud.com;" +
"Database=MyInventory;" +
"Uid=xxxxxxxx.root;" +
"Pwd=你的密碼;" + // ← 就是這一行
"Port=4000;" +
"SslMode=Preferred;";跟著上一篇做下來,你的 Pwd= 後面填的是 TiDB Cloud 給你的真實密碼。只要這個檔案被 git commit、再 git push 到 GitHub,這組密碼就攤在網路上了——就算你事後刪掉那一行,它仍然留在 Git 的歷史紀錄裡,等於已經外流。雲端資料庫的密碼外流,任何人都能連上刪光你的資料。
自己動手前先做兩件事:
1. 把連線字串搬進 `appsettings.json`,程式從設定檔讀,別寫死在 .cs 裡。
2. ,含真密碼的檔案永遠不進版控。
小結:這跟 MVP 那篇的 AddClicked 是同一件事
如果你看過 MVP 架構那篇,這裡的機制應該很眼熟。那篇的 IProductView 有一個 AddClicked 事件,View 按下按鈕時引發它、Presenter 訂閱它:
// MVP 篇:View 引發、Presenter 訂閱
public event EventHandler AddClicked;
// ...
_view.AddClicked += OnAddClicked;跟這篇 OnDbInitResult 是同一個機制,只是方向和角色不同。共通的精神是:把「做事的人」和「對結果有反應的人」拆開。
- MVP 篇:View 做事(收到點擊),Presenter 反應(處理邏輯)。View 不知道 Presenter 存在。
- 這篇:Helper 做事(建資料庫),畫面反應(顯示訊息)。Helper 不知道畫面存在。
一旦你抓到這個「廣播/訂閱」的形狀,之後在 C# 看到 事件名 += 方法名,你就知道那是「有人在等某件事發生」。按鈕的 Click、計時器的 Tick、背景工作的 Completed——全都是同一套。
回顧一下,把 void InitDb() 升級成會發事件的版本只花三步:宣告 public static event EventHandler<(bool, string)>? OnDbInitResult; → 成功與失敗處各一句 (記得 )→ 呼叫端 (要在觸發之前先訂閱)。換來的是:呼叫端不必猜、不必包 、拿得到具體原因,而且工具類完全不碰畫面,可以重用、可以測試。