C# WinForms中BackgroundWorker填充DataGridView跨线程错误修复求助
错误原因及修复方案
核心问题分析
- 跨线程访问UI控件:
DoWork事件在后台线程执行,直接调用的FillGrid中存在对dtpFromdt、dtpToDate(读取值)和dgvStock(绑定数据)的操作,这些控件由UI线程创建,后台线程直接操作会触发跨线程错误。 - BackgroundWorker被提前释放:
RunWorkerCompleted中调用backgroundWorker1.Dispose(),导致第二次点击搜索时控件已被释放,无法复用。 - 取消逻辑无效:
CancelAsync()仅设置取消标记,但后台线程未检查CancellationPending,无法真正终止任务。 - ProgressChanged滥用:
ProgressChanged运行在主线程,其中的Thread.Sleep(20000)会直接阻塞UI,属于错误用法。
具体修复步骤
1. 拆分数据获取与UI操作
将耗时的数据库查询放在后台线程执行,UI绑定操作必须回到主线程完成。
2. 传递参数给后台线程
启动BackgroundWorker前在主线程读取日期控件的值,作为参数传递给后台线程,避免后台线程直接访问UI控件。
3. 保留BackgroundWorker实例
移除RunWorkerCompleted中的backgroundWorker1.Dispose(),确保控件可复用。
4. 完善取消逻辑
启用WorkerSupportsCancellation,后台线程中定期检查CancellationPending标记,及时终止任务。
5. 清理无效的ProgressChanged代码
ProgressChanged仅用于更新进度(如进度条),移除其中的阻塞操作。
完整修正代码
private clsStkBAL obj = new clsStkBAL(); public frmSalesStk() { InitializeComponent(); backgroundWorker1.WorkerReportsProgress = true; backgroundWorker1.WorkerSupportsCancellation = true; // 启用取消支持 } // 仅在主线程执行的UI绑定方法 private void BindGrid(DataTable dt) { try { clsCommon.BindeDataGridView(this.dgvStock, dt, false); } catch (Exception ex) { clsCommon.ShowErrorMessage(ex.Message); } } // 后台线程执行的数据获取方法 private DataTable GetData(DateTime startDate, DateTime endDate) { try { this.obj.SP_STATUS = nameof(FillGrid); this.obj.Sih_StartDate = startDate; this.obj.Sih_EndDate = endDate; // 检查是否被取消,及时终止任务 if (backgroundWorker1.CancellationPending) return null; return this.obj.GetDataTable((clsSalesStock)this.obj, null, false) as DataTable; } catch (Exception ex) { // 后台线程不能直接弹消息,将异常传递到RunWorkerCompleted throw ex; } } private void btnSearch_Click(object sender, EventArgs e) { if (backgroundWorker1.IsBusy) { clear(); return; } // 主线程读取日期参数,避免跨线程访问 var startDate = dtpFromdt.Value.Date; var endDate = dtpToDate.Value.Date; // 传递参数给后台线程 backgroundWorker1.RunWorkerAsync(new { StartDate = startDate, EndDate = endDate }); // 禁用日期控件防止重复操作 dtpFromdt.Enabled = false; dtpToDate.Enabled = false; } private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { var args = e.Argument as dynamic; DateTime startDate = args.StartDate; DateTime endDate = args.EndDate; // 后台线程执行数据获取 DataTable dt = GetData(startDate, endDate); // 如果被取消,设置取消结果 if (backgroundWorker1.CancellationPending) { e.Cancel = true; return; } // 将查询结果传递给RunWorkerCompleted e.Result = dt; } private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { // 恢复控件可用状态 dtpFromdt.Enabled = true; dtpToDate.Enabled = true; if (e.Cancelled) { MessageBox.Show("任务已取消"); return; } if (e.Error != null) { clsCommon.ShowErrorMessage(e.Error.Message); return; } DataTable dt = e.Result as DataTable; if (dt != null) { BindGrid(dt); // 主线程绑定数据 MessageBox.Show("加载完成"); } } private void frmSalesStock_Load(object sender, EventArgs e) { dtpFromdt.Value = new DateTime(2021, 02, 01); dtpToDate.Value = new DateTime(2021, 02, 01); // 若设计器已绑定DoWork事件,无需重复绑定 // backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork); } public void clear() { if (backgroundWorker1.IsBusy) { backgroundWorker1.CancelAsync(); } dgvStock.DataSource = null; } private void btnClear1_Click(object sender, EventArgs e) { clear(); }
关键修复点说明
- 参数传递:主线程提前读取日期值并传递给后台线程,彻底避免跨线程访问UI控件。
- 操作分离:
GetData在后台线程执行耗时查询,BindGrid在主线程完成UI绑定,实现线程安全隔离。 - 取消逻辑:启用取消支持后,后台线程实时检查取消标记,及时终止任务。
- 控件复用:移除
Dispose调用,保证BackgroundWorker可多次使用。
内容的提问来源于stack exchange,提问作者gok
相关产品推荐
相关产品推荐

