WinForms/WPF 中,SqlCommand.BeginExecuteReader 回呼在**背景執行緒**執行,直接操作 UI 控制項 (dataGridView1.DataSource = dt) 會導致:
1. InvalidOperationException (跨執行緒存取,.NET 2.0+ 預設檢查)。
2. 若關閉 CheckForIllegalCrossThreadCalls = false,UI 凍結、重繪異常、進程無法正常退出。
// ❌ 錯誤:回呼直接綁定 UI
private void delDataBin(IAsyncResult ar) {
var com = (SqlCommand)ar.AsyncState;
var dr = com.EndExecuteReader(ar);
var dt = new DataTable(); dt.Load(dr);
this.dataGridView1.DataSource = dt; // 拋異常或 UI 異常
}
public partial class MainForm : Form {
private delegate void UpdateGridDelegate(DataTable dt);
private void btnLoad_Click(object sender, EventArgs e) {
var connStr = "UID=sa;Password=123;Initial Catalog=AD;Data Source=192.168.1.1;Asynchronous Processing=true";
using (var con = new SqlConnection(connStr)) {
con.Open();
using (var cmd = new SqlCommand("SELECT TOP 100 * FROM tb_user", con)) {
cmd.BeginExecuteReader(ar => {
try {
var c = (SqlCommand)ar.AsyncState;
using var dr = c.EndExecuteReader(ar);
var dt = new DataTable(); dt.Load(dr);
// 關鍵:檢查是否需跨執行緒調度
if (this.InvokeRequired) {
this.BeginInvoke(new UpdateGridDelegate(BindGrid), dt);
} else {
BindGrid(dt);
}
} catch (Exception ex) {
this.BeginInvoke(() => MessageBox.Show($"Error: {ex.Message}"));
}
}, cmd);
}
}
}
private void BindGrid(DataTable dt) {
dataGridView1.DataSource = dt;
}
}
// WPF 回呼中
Application.Current.Dispatcher.BeginInvoke(new Action(() => {
dataGrid.ItemsSource = dt.DefaultView;
}), DispatcherPriority.DataBind);
SqlCommand.ExecuteReaderAsync (ADO.NET 4.5+) 回傳 Task<SqlDataReader>,配合 await 自動捕獲同步上下文 (UI 執行緒),**無需手動 Invoke**。
private async void btnLoad_Click(object sender, EventArgs e) {
btnLoad.Enabled = false;
try {
var connStr = "UID=sa;Password=123;Initial Catalog=AD;Data Source=192.168.1.1;Asynchronous Processing=true";
using (var con = new SqlConnection(connStr)) {
await con.OpenAsync();
using (var cmd = new SqlCommand("SELECT TOP 100 * FROM tb_user", con)) {
using (var dr = await cmd.ExecuteReaderAsync()) {
var dt = new DataTable();
dt.Load(dr);
dataGridView1.DataSource = dt; // 自動在 UI 執行緒執行
}
}
}
} catch (Exception ex) {
MessageBox.Show($"Error: {ex.Message}");
} finally {
btnLoad.Enabled = true;
}
}
private CancellationTokenSource _cts;
private async void btnLoad_Click(object sender, EventArgs e) {
_cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); // 30 秒超時
try {
// ... await con.OpenAsync(_cts.Token);
// ... await cmd.ExecuteReaderAsync(_cts.Token);
} catch (OperationCanceledException) {
MessageBox.Show("查詢超時或已取消");
} catch (Exception ex) {
MessageBox.Show($"Error: {ex.Message}");
}
}
private void btnCancel_Click(object sender, EventArgs e) => _cts?.Cancel();
| 模式 | 執行緒模型 | UI 調度 | 適用版本 |
|---|---|---|---|
BeginExecuteReader + 回呼 | ThreadPool 執行緒 | 手動 Invoke/BeginInvoke | .NET 2.0+ |
ExecuteReaderAsync + await | ThreadPool,但 await 後續回 UI 執行緒 | 自動 (同步上下文) | .NET 4.5+ |
BackgroundWorker | ThreadPool | RunWorkerCompleted 事件在 UI 執行緒 | .NET 2.0+ (舊專案) |
Control.CheckForIllegalCrossThreadCalls = false — 掩蓋 Bug、導致不可預測 UI 錯誤。SqlConnection 連接字串**必須**包含 Asynchronous Processing=true (或 Async=true) 才能使用非同步方法。DataTable.Load 會完整讀取 SqlDataReader,大量資料考慮分頁或 SqlDataAdapter.Fill (同步) 配合 Task.Run。async void 僅限事件訂閱;其他場合用 async Task 以便錯誤傳播與等待。從 APM (Begin/End) 到 TAP (async/await),核心難點始終是**背景執行緒取得資料 → 安全調度回 UI 執行緒綁定**。掌握 InvokeRequired/BeginInvoke (WinForms) 或 Dispatcher (WPF) 的手動調度,並優先遷移至 async/await 享受自動同步上下文捕獲,是構建流暢、穩定資料驅動桌面應用的必修課。