WinForms C#中用BackgroundWorker刷新DataGridView时ShowDialog空引用异常
解决WinForms中ShowDialog()空引用异常及BackgroundWorker更新DataGridView的线程安全问题
首先,咱们直接定位你遇到的核心问题:调用ShowDialog()时的空引用异常,大概率和后台线程非法操作UI控件以及窗体生命周期与线程运行不同步有关——尤其是你设置了Control.CheckForIllegalCrossThreadCalls = false,这相当于关闭了WinForms的线程安全检查,会隐藏很多潜在问题,反而更容易引发空引用、控件状态异常这类难以排查的错误。
问题根源拆解
- 直接在后台线程操作UI控件:
backgroundWorker2_DoWork里直接给dataGridView1.DataSource赋值并调用Refresh(),违反了WinForms单线程模型——UI控件只能由创建它的主线程访问。虽然关闭了线程检查,但当窗体未完全初始化,或后续窗体被释放后,后台线程再访问控件就会触发空引用。 - 窗体生命周期与线程未同步:你用
using块包裹窗体实例,ShowDialog()结束后窗体立即被Dispose(),但此时backgroundWorker1和backgroundWorker2可能还在后台运行,它们尝试访问已被释放的控件(比如dataGridView1),直接导致空引用异常。 - 重复绑定DoWork事件:每次点击
buttonStart都会重新绑定DoWork事件,多次点击后同一个BackgroundWorker会触发多次事件处理逻辑,加剧了控件访问的冲突。
分步修复方案
1. 移除线程安全检查,改用BackgroundWorker的安全UI更新机制
BackgroundWorker本身提供了ProgressChanged(用于更新UI)和RunWorkerCompleted(用于线程结束后操作)事件,这两个事件都在主线程执行,完全符合线程安全要求:
// 子窗体构造函数里绑定事件(只绑定一次!) public FormFactProgress(string dwConn, string sourceConn, string etlConn) { InitializeComponent(); _dwConnectionString = dwConn; _sourceConnectionString = sourceConn; _etlConnectionString = etlConn; // 只绑定一次事件,避免重复绑定 backgroundWorker1.DoWork += backgroundWorker1_DoWork; backgroundWorker2.DoWork += backgroundWorker2_DoWork; backgroundWorker2.ProgressChanged += backgroundWorker2_ProgressChanged; backgroundWorker2.RunWorkerCompleted += backgroundWorker2_RunWorkerCompleted; // 允许backgroundWorker2报告进度 backgroundWorker2.WorkerReportsProgress = true; // 允许取消线程 backgroundWorker1.WorkerSupportsCancellation = true; backgroundWorker2.WorkerSupportsCancellation = true; } private void buttonStart_Click(object sender, EventArgs e) { if (!backgroundWorker1.IsBusy) { backgroundWorker1.RunWorkerAsync(); } if (!backgroundWorker2.IsBusy) { backgroundWorker2.RunWorkerAsync(); } buttonStart.Enabled = false; } private void backgroundWorker2_DoWork(object sender, DoWorkEventArgs e) { try { BackgroundWorker worker = (BackgroundWorker)sender; var dw = DwData(_dwConnectionString); var source = SourceData(_sourceConnectionString); // 第一次获取进度并报告给主线程 var progressData = GetProgress(dw, source); worker.ReportProgress(0, progressData); while (isLoading) { System.Threading.Thread.Sleep(1000); // 检查线程是否被取消(比如窗体关闭) if (worker.CancellationPending) { e.Cancel = true; return; } dw = DwData(_dwConnectionString); progressData = GetProgress(dw, source); // 把数据传递给主线程更新UI worker.ReportProgress(0, progressData); } } catch (Exception ex) { // 把异常传递到RunWorkerCompleted处理 e.Result = ex; return; } } // 主线程中安全更新DataGridView private void backgroundWorker2_ProgressChanged(object sender, ProgressChangedEventArgs e) { var progressData = e.UserState as YourProgressDataType; // 替换成你的实际数据类型 if (progressData != null && dataGridView1 != null) { dataGridView1.DataSource = progressData; dataGridView1.Refresh(); } } // 线程结束后的统一处理(成功/失败/取消) private void backgroundWorker2_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Error != null) { MessageBox.Show( "Progress viewing has failed. The ETL is still running though. Please monitor your data warehouse growth manually. Error: " + e.Error, "Progress Visualisation Failed", MessageBoxButtons.OK, MessageBoxIcon.Error); } else if (!e.Cancelled) { MessageBox.Show( "Your data warehouse facts are loaded!", "Facts Loaded", MessageBoxButtons.OK, MessageBoxIcon.Information); } }
2. 同步窗体关闭与后台线程终止
当用户关闭窗体时,主动取消后台线程,避免线程访问已释放的控件:
protected override void OnFormClosing(FormClosingEventArgs e) { base.OnFormClosing(e); // 取消并等待backgroundWorker1结束 if (backgroundWorker1.IsBusy) { backgroundWorker1.CancelAsync(); while (backgroundWorker1.IsBusy) { Application.DoEvents(); } } // 取消并等待backgroundWorker2结束 if (backgroundWorker2.IsBusy) { backgroundWorker2.CancelAsync(); while (backgroundWorker2.IsBusy) { Application.DoEvents(); } } isLoading = false; }
同时在backgroundWorker1_DoWork里添加取消检查:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { try { BackgroundWorker worker = (BackgroundWorker)sender; sSISDBCatalog.StartFactLoad(_etlConnectionString); while (isLoading) { System.Threading.Thread.Sleep(1000); if (worker.CancellationPending) { e.Cancel = true; return; } if (dynamicWarehouseCatalog.FactsLoaded(_dwConnectionString)) { if (dynamicWarehouseCatalog.AllFactsLoaded(_dwConnectionString)) { isLoading = false; } } } } catch(Exception ex) { e.Result = ex; return; } }
3. 修正using块的潜在问题
改用手动Dispose()确保线程完全终止后再释放窗体:
private void button1_Click(object sender, EventArgs e) { var f = new FormFactProgress(_dwConnectionString, _sourceConnectionString, _etlConnectionString); try { f.ShowDialog(); labelLoadWarning.Visible = false; groupBoxLoadFacts.Enabled = false; buttonLoadFacts.BackColor = Color.FromArgb(128, 255, 128); buttonLoadFacts.Text = "Loaded"; if (dynamicWarehouseCatalog.FactsLoaded(_dwConnectionString) && dynamicWarehouseCatalog.DimsLoaded(_dwConnectionString)) { groupBoxSync.Enabled = true; } } finally { f.Dispose(); } }
额外注意事项
- 给
isLoading添加volatile修饰:private volatile bool isLoading = true;,避免线程缓存变量值导致循环无法终止。 - 检查
DwData、SourceData、GetProgress方法是否可能返回null,赋值前做空值判断,避免空引用。
内容的提问来源于stack exchange,提问作者Evan Barke
相关产品推荐
相关产品推荐

