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

Ubuntu环境下Windows Forms双DataGridView主从表交互时System.IndexOutOfRangeException异常排查与解决

DataGridView 1:n关系在Mono/Ubuntu下点击崩溃的问题分析与解决

异常原因

这个问题的核心是Mono的WinForms实现与Windows上的.NET Framework存在细节差异:当使用BindingSource实现父子DataGridView的自动关联时,Mono的CurrencyManager在处理父行切换时的子数据源重置逻辑存在bug。点击父DataGridView的行时,子BindingSource会尝试同步更新当前位置,但如果子数据源的状态没有被正确处理(比如空数据、Position同步时机错误),就会触发IndexOutOfRangeException。

另外,你用try-catch包裹Application.Run无法捕获异常,是因为这个异常是在WinForms的UI线程消息循环内部抛出的,不属于你代码直接调用的栈帧,所以需要注册全局异常处理才能捕获,但这只是临时排查手段,核心还是要修复绑定逻辑。

代码中的潜在问题

你的代码逻辑在Windows上是可行的,但在Mono环境下,自动关联的BindingSource父子关系没有被正确处理:

  1. 依赖BindingSource的自动关联逻辑,而Mono对这种嵌套BindingSource的支持不够健壮
  2. 没有在父行切换时处理子数据源的状态重置,导致CurrencyManager在更新Position时找不到有效项

解决方法

方法1:手动加载子表数据(最可靠的绕过方案)

放弃BindingSource的自动父子关联,改为在父DataGridView的选中事件中手动查询并绑定子表数据,完全绕开Mono的bug:

修改你的button1_Click方法,调整绑定逻辑:

private void button1_Click(object sender, EventArgs e)
{
    var connectionString = Program.getConnectionString();
    if (connectionString == null)
    {
        Program.LogError("[error][Form1.button1_Click()] connectionString is null after reading it from file. ");
        return;
    }
    Console.WriteLine("[log][Form1.button1_Click()] connectionString: " + connectionString);

    if ((sqlConnection = new SqlConnection(connectionString)) == null)
    {
        Program.LogError("[error][Form1.button1_Click()] creating a SQL Connection failed");
        return;
    }
    Console.WriteLine("[log][Form1.button1_Click()] created SQL Connection");

    try
    {
        sqlConnection.Open();
        Console.WriteLine("[log][Form1.button1_Click()] Opened the SQL Connection");
        Console.WriteLine("[log][Form1.button1_Click()] The state of the connection: " + sqlConnection.State);

        using (SqlCommand sqlCommand = new SqlCommand("USE [CoffeeShopDB]", sqlConnection))
        {
            sqlCommand.ExecuteNonQuery();
            Console.WriteLine("[log][Form1.button1_Click()] Switched to [CoffeeShopDB]");
        }

        dataAdapterPosition = new SqlDataAdapter("select * from Position", sqlConnection);
        dataAdapterSalary = new SqlDataAdapter("select * from Salary", sqlConnection);
        sqlCommandBuilderSalary = new SqlCommandBuilder(dataAdapterSalary);

        dataSet = new DataSet();
        dataAdapterPosition.Fill(dataSet, "Position");
        dataAdapterSalary.Fill(dataSet, "Salary");

        // 保留DataRelation但不用于自动绑定,仅用于数据关联
        dataSet.Relations.Add(new DataRelation("FK_pid_Position_to_Salary", dataSet.Tables["Position"].Columns["pid"], dataSet.Tables["Salary"].Columns["pidfk"]));

        // 初始化绑定源,子绑定源暂时不关联父源
        bindingSourcePosition = new BindingSource { DataSource = dataSet, DataMember = "Position" };
        bindingSourceSalary = new BindingSource();
        
        dataGridView1.DataSource = bindingSourcePosition;
        dataGridView2.DataSource = bindingSourceSalary;

        // 注册父DataGridView的选中变化事件
        dataGridView1.SelectionChanged += DataGridView1_SelectionChanged;
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception);
    }
}

// 手动处理父行选中,加载对应子数据
private void DataGridView1_SelectionChanged(object sender, EventArgs e)
{
    if (dataGridView1.CurrentRow != null && dataGridView1.CurrentRow.DataBoundItem is DataRowView rowView)
    {
        // 获取选中父行的pid
        int pid = rowView.Row.Field<int>("pid");
        // 通过DataRelation获取关联的子行
        DataRow[] childRows = rowView.Row.GetChildRows("FK_pid_Position_to_Salary");
        // 创建临时表存放子数据
        DataTable childTable = dataSet.Tables["Salary"].Clone();
        foreach (DataRow row in childRows)
        {
            childTable.ImportRow(row);
        }
        // 绑定子表数据
        bindingSourceSalary.DataSource = childTable;
    }
    else
    {
        // 没有选中行时清空子表
        bindingSourceSalary.DataSource = null;
    }
}

方法2:调整BindingSource绑定顺序(尝试修复自动关联)

如果你想保留自动关联的逻辑,可以尝试调整绑定顺序,确保父BindingSource完全初始化后再绑定子源,并在父行变化时重置子源的Position:

private void button1_Click(object sender, EventArgs e)
{
    // ... 前面的代码保持不变 ...

    dataSet.Relations.Add(new DataRelation("FK_pid_Position_to_Salary", dataSet.Tables["Position"].Columns["pid"], dataSet.Tables["Salary"].Columns["pidfk"]));

    // 先绑定父源并强制完成布局
    bindingSourcePosition = new BindingSource { DataSource = dataSet, DataMember = "Position" };
    dataGridView1.DataSource = bindingSourcePosition;
    // 强制父控件完成数据绑定布局
    dataGridView1.PerformLayout();
    dataGridView1.Update();

    // 再初始化子绑定源
    bindingSourceSalary = new BindingSource { DataSource = bindingSourcePosition, DataMember = "FK_pid_Position_to_Salary" };
    dataGridView2.DataSource = bindingSourceSalary;

    // 父源当前项变化时,重置子源的Position,避免CurrencyManager出错
    bindingSourcePosition.CurrentChanged += (s, args) =>
    {
        if (bindingSourceSalary != null)
        {
            bindingSourceSalary.Position = -1;
        }
    };
}

方法3:迁移到.NET 6+跨平台WinForms

Mono的WinForms已经停止维护,而微软官方的.NET 6及以上版本提供了完善的跨平台WinForms支持,其实现比Mono更稳定,大概率已经修复了这类兼容性bug。你可以将项目迁移到.NET 6+,直接在Ubuntu上运行,不需要依赖Mono。

额外提示:全局异常捕获

如果需要临时排查这类UI线程异常,可以在Program.cs中添加全局异常处理:

static class Program
{
    [STAThread]
    static void Main()
    {
        // 注册全局未处理异常捕获
        AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
        Application.ThreadException += Application_ThreadException;

        Application.SetHighDpiMode(HighDpiMode.SystemAware);
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form1());
    }

    private static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e)
    {
        Program.LogError($"UI线程异常: {e.Exception}");
    }

    private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
    {
        var ex = e.ExceptionObject as Exception;
        if (ex != null)
        {
            Program.LogError($"未处理异常: {ex}");
        }
    }
}

内容的提问来源于stack exchange,提问作者sandrino

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 17:47:46