You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WinForms C#中用BackgroundWorker刷新DataGridView时ShowDialog空引用异常

解决WinForms中ShowDialog()空引用异常及BackgroundWorker更新DataGridView的线程安全问题

首先,咱们直接定位你遇到的核心问题:调用ShowDialog()时的空引用异常,大概率和后台线程非法操作UI控件以及窗体生命周期与线程运行不同步有关——尤其是你设置了Control.CheckForIllegalCrossThreadCalls = false,这相当于关闭了WinForms的线程安全检查,会隐藏很多潜在问题,反而更容易引发空引用、控件状态异常这类难以排查的错误。

问题根源拆解

  1. 直接在后台线程操作UI控件:backgroundWorker2_DoWork里直接给dataGridView1.DataSource赋值并调用Refresh(),违反了WinForms单线程模型——UI控件只能由创建它的主线程访问。虽然关闭了线程检查,但当窗体未完全初始化,或后续窗体被释放后,后台线程再访问控件就会触发空引用。
  2. 窗体生命周期与线程未同步:你用using块包裹窗体实例,ShowDialog()结束后窗体立即被Dispose(),但此时backgroundWorker1和backgroundWorker2可能还在后台运行,它们尝试访问已被释放的控件(比如dataGridView1),直接导致空引用异常。
  3. 重复绑定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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 06:36:56