Ubuntu环境下Windows Forms双DataGridView主从表交互时System.IndexOutOfRangeException异常排查与解决
异常原因
这个问题的核心是Mono的WinForms实现与Windows上的.NET Framework存在细节差异:当使用BindingSource实现父子DataGridView的自动关联时,Mono的CurrencyManager在处理父行切换时的子数据源重置逻辑存在bug。点击父DataGridView的行时,子BindingSource会尝试同步更新当前位置,但如果子数据源的状态没有被正确处理(比如空数据、Position同步时机错误),就会触发IndexOutOfRangeException。
另外,你用try-catch包裹Application.Run无法捕获异常,是因为这个异常是在WinForms的UI线程消息循环内部抛出的,不属于你代码直接调用的栈帧,所以需要注册全局异常处理才能捕获,但这只是临时排查手段,核心还是要修复绑定逻辑。
代码中的潜在问题
你的代码逻辑在Windows上是可行的,但在Mono环境下,自动关联的BindingSource父子关系没有被正确处理:
- 依赖
BindingSource的自动关联逻辑,而Mono对这种嵌套BindingSource的支持不够健壮 - 没有在父行切换时处理子数据源的状态重置,导致
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

