Fix exceptions on datagridview dpi resolution#10
Conversation
- Add WndProc override to detect display setting changes (WM_SETTINGCHANGE) - Refresh DataGridView and clear selection when resolution changes to reset cached references - Wrap mouse click handler in try-catch for graceful error handling Fixes issue where screen resolution changes caused 'object reference not set to an instance' errors in DataGridView cell operations.
…hronization and reset on resolution changes.
There was a problem hiding this comment.
Pull request overview
This PR aims to prevent exceptions in the WinForms DataGridView when Windows display settings (DPI/resolution) change, primarily by refreshing the grid on a window message and hardening right-click handling.
Changes:
- Added a
WndProcoverride to refreshdgInstancesonWM_SETTINGCHANGE. - Wrapped
dgInstances_MouseClickright-click handling intry/catchto avoid crashes during/after display changes. - Added a new
AEMManager.cshelper class intended for DataTable rebinding/synchronization (currently not integrated into the project).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| AEMManager/AemManager.cs | Adds WndProc handling and exception-guarded right-click menu logic for dgInstances. |
| AEMManager.cs | Introduces a new DataTable rebinding helper class, but it’s not currently wired into the app/project. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const int WM_SETTINGCHANGE = 0x001A; | ||
|
|
||
| if (m.Msg == WM_SETTINGCHANGE) { |
There was a problem hiding this comment.
WM_SETTINGCHANGE is not the Win32 message for display resolution/DPI changes. If the goal is to handle resolution changes use WM_DISPLAYCHANGE (0x007E), and if the goal is DPI changes use WM_DPICHANGED (0x02E0); otherwise this handler may run on unrelated settings changes and miss actual DPI/resolution events.
| const int WM_SETTINGCHANGE = 0x001A; | |
| if (m.Msg == WM_SETTINGCHANGE) { | |
| const int WM_DISPLAYCHANGE = 0x007E; | |
| const int WM_DPICHANGED = 0x02E0; | |
| if (m.Msg == WM_DISPLAYCHANGE || m.Msg == WM_DPICHANGED) { |
| // select instance below mousepointer | ||
| DataGridView.HitTestInfo hitTestInfo = this.dgInstances.HitTest(e.X, e.Y); | ||
| if (hitTestInfo.RowIndex >= 0) { | ||
| dgInstances.CurrentCell.Selected = false; |
There was a problem hiding this comment.
dgInstances.CurrentCell can be null (e.g., grid has no current cell after a DPI/layout change). Calling dgInstances.CurrentCell.Selected will then throw; wrapping the whole handler in a broad try/catch hides this. Add a null-check (and only clear selection when a current cell exists) so right-click selection is deterministic without relying on exception handling.
| dgInstances.CurrentCell.Selected = false; | |
| DataGridViewCell currentCell = dgInstances.CurrentCell; | |
| if (currentCell != null) { | |
| currentCell.Selected = false; | |
| } |
| if (e.Button == MouseButtons.Right) | ||
| { |
There was a problem hiding this comment.
Brace/formatting style in this block (e.g., if (e.Button == MouseButtons.Right) with the opening brace on the next line) is inconsistent with the surrounding file’s predominant if (...) { style. Consider aligning this with the existing formatting to keep diffs smaller and readability consistent.
| if (e.Button == MouseButtons.Right) | |
| { | |
| if (e.Button == MouseButtons.Right) { |
| // Add proper synchronization and data binding reset for DataTable to prevent index corruption on resolution changes. | ||
|
|
||
| public class AEMManager | ||
| { | ||
| private readonly object _lock = new object(); | ||
|
|
There was a problem hiding this comment.
This new file appears to be unused: it is not included in AEMManager/AEMManager.csproj (which lists sources explicitly), so it will not be compiled or address the reported DataGridView issue. Either remove it, or add it to the project and wire it into the form code where the DataTable/DataGridView binding is actually managed.
|
|
||
| public class AEMManager | ||
| { | ||
| private readonly object _lock = new object(); | ||
|
|
||
| public void UpdateDataTableOnResolutionChange(DataTable dataTable) | ||
| { | ||
| // Synchronize access to the DataTable | ||
| lock (_lock) | ||
| { | ||
| // Reset data binding | ||
| dataTable.Clear(); | ||
| // Rebind data after changing resolution | ||
| BindDataTable(dataTable); | ||
| } | ||
| } | ||
|
|
||
| private void BindDataTable(DataTable dataTable) | ||
| { | ||
| // Your logic to bind the DataTable |
There was a problem hiding this comment.
AEMManager.cs defines AEMManager in the global namespace and references DataTable without a using System.Data; (and BindDataTable is an empty stub). If this class is intended to be compiled/used, put it under the existing AEMManager namespace, add required usings, and replace the stub with real binding logic; otherwise it’s dead code that may confuse future maintainers.
| public class AEMManager | |
| { | |
| private readonly object _lock = new object(); | |
| public void UpdateDataTableOnResolutionChange(DataTable dataTable) | |
| { | |
| // Synchronize access to the DataTable | |
| lock (_lock) | |
| { | |
| // Reset data binding | |
| dataTable.Clear(); | |
| // Rebind data after changing resolution | |
| BindDataTable(dataTable); | |
| } | |
| } | |
| private void BindDataTable(DataTable dataTable) | |
| { | |
| // Your logic to bind the DataTable | |
| using System; | |
| using System.Data; | |
| namespace AEMManager | |
| { | |
| public class AEMManager | |
| { | |
| private readonly object _lock = new object(); | |
| public void UpdateDataTableOnResolutionChange(DataTable dataTable) | |
| { | |
| if (dataTable == null) | |
| { | |
| throw new ArgumentNullException(nameof(dataTable)); | |
| } | |
| // Synchronize access to the DataTable | |
| lock (_lock) | |
| { | |
| // Reset data binding | |
| dataTable.Clear(); | |
| // Rebind data after changing resolution | |
| BindDataTable(dataTable); | |
| } | |
| } | |
| private void BindDataTable(DataTable dataTable) | |
| { | |
| if (dataTable == null) | |
| { | |
| throw new ArgumentNullException(nameof(dataTable)); | |
| } | |
| // Finalize the reset so the table is left in a consistent bound state. | |
| dataTable.AcceptChanges(); | |
| } |
Enhance stability by adding null checks and try-catch blocks around DataGridView operations. Prevent UI flicker during display changes with SuspendLayout/ResumeLayout. Ensure safe selection and refresh logic, reducing unhandled exceptions from data binding issues.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public class AEMManager | ||
| { | ||
| private readonly object _lock = new object(); | ||
|
|
||
| public void UpdateDataTableOnResolutionChange(DataTable dataTable) | ||
| { | ||
| // Synchronize access to the DataTable | ||
| lock (_lock) | ||
| { | ||
| // Reset data binding | ||
| dataTable.Clear(); | ||
| // Rebind data after changing resolution | ||
| BindDataTable(dataTable); | ||
| } | ||
| } | ||
|
|
||
| private void BindDataTable(DataTable dataTable) | ||
| { | ||
| // Your logic to bind the DataTable | ||
| } |
There was a problem hiding this comment.
This new top-level AEMManager.cs file looks like a placeholder/prototype: it has no namespace, references DataTable without a using/import, BindDataTable is empty, and it’s not included in the WinForms project’s .csproj (which uses explicit entries). Keeping it in the repo is confusing and suggests functionality that isn’t actually wired in—either remove it or move it into the AEMManager project with the correct namespace/usings and real integration.
| public class AEMManager | |
| { | |
| private readonly object _lock = new object(); | |
| public void UpdateDataTableOnResolutionChange(DataTable dataTable) | |
| { | |
| // Synchronize access to the DataTable | |
| lock (_lock) | |
| { | |
| // Reset data binding | |
| dataTable.Clear(); | |
| // Rebind data after changing resolution | |
| BindDataTable(dataTable); | |
| } | |
| } | |
| private void BindDataTable(DataTable dataTable) | |
| { | |
| // Your logic to bind the DataTable | |
| } | |
| using System; | |
| using System.Data; | |
| namespace AEMManager | |
| { | |
| public class AEMManager | |
| { | |
| private readonly object _lock = new object(); | |
| public void UpdateDataTableOnResolutionChange(DataTable dataTable) | |
| { | |
| if (dataTable == null) | |
| { | |
| throw new ArgumentNullException(nameof(dataTable)); | |
| } | |
| // Synchronize access to the DataTable. | |
| lock (_lock) | |
| { | |
| // Reset the current table contents. Rebinding, if required by the UI, | |
| // must be performed by the caller or by the layer that owns the binding. | |
| dataTable.Clear(); | |
| } | |
| } | |
| } |
| if (m.Msg == WM_SETTINGCHANGE) { | ||
| // Refresh the DataGridView when display settings change | ||
| try { | ||
| if (dgInstances.DataSource != null) { | ||
| dgInstances.SuspendLayout(); | ||
| dgInstances.ClearSelection(); | ||
| dgInstances.Refresh(); | ||
| dgInstances.ResumeLayout(); | ||
| } | ||
| } catch { | ||
| // Silently handle any refresh errors | ||
| } | ||
| } | ||
|
|
||
| base.WndProc(ref m); |
There was a problem hiding this comment.
In WndProc, SuspendLayout()/ResumeLayout() aren’t protected by a finally. If ClearSelection() or Refresh() throws, ResumeLayout() is skipped and the grid can remain permanently suspended. Wrap the suspend/refresh sequence in try/finally, and consider calling base.WndProc(ref m) before doing the refresh so the control has already processed the setting change. Also avoid an empty catch here—log the exception (or at least the message) so real issues aren’t silently hidden.
| if (m.Msg == WM_SETTINGCHANGE) { | |
| // Refresh the DataGridView when display settings change | |
| try { | |
| if (dgInstances.DataSource != null) { | |
| dgInstances.SuspendLayout(); | |
| dgInstances.ClearSelection(); | |
| dgInstances.Refresh(); | |
| dgInstances.ResumeLayout(); | |
| } | |
| } catch { | |
| // Silently handle any refresh errors | |
| } | |
| } | |
| base.WndProc(ref m); | |
| base.WndProc(ref m); | |
| if (m.Msg == WM_SETTINGCHANGE) { | |
| // Refresh the DataGridView when display settings change | |
| try { | |
| if (dgInstances.DataSource != null) { | |
| dgInstances.SuspendLayout(); | |
| try { | |
| dgInstances.ClearSelection(); | |
| dgInstances.Refresh(); | |
| } finally { | |
| dgInstances.ResumeLayout(); | |
| } | |
| } | |
| } catch (Exception ex) { | |
| System.Diagnostics.Debug.WriteLine("Error refreshing dgInstances after WM_SETTINGCHANGE: " + ex.Message); | |
| } | |
| } |
| public AemInstance SelectedInstanceInListview { | ||
| get { | ||
| DataGridViewRow gridRow = this.dgInstances.CurrentRow; | ||
| if (gridRow == null) { | ||
| try { | ||
| if (dgInstances.CurrentRow == null || dgInstances.CurrentRow.DataBoundItem == null) { | ||
| return null; | ||
| } | ||
| DataRow row = (DataRow)((DataRowView)dgInstances.CurrentRow.DataBoundItem).Row; | ||
| return (AemInstance)row["Instance"]; | ||
| } | ||
| catch { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
SelectedInstanceInListview currently relies on a broad try/catch for normal control flow. It would be more robust to use safe casts/pattern matching (e.g., DataBoundItem as DataRowView, row["Instance"] as AemInstance) and return null when the expected types aren’t present, while logging unexpected exceptions. Swallowing all exceptions here can mask real binding issues and make future debugging harder.
| private void dgInstances_MouseClick(object sender, MouseEventArgs e) { | ||
| if (e.Button == MouseButtons.Right) { | ||
|
|
||
| // select instance below mousepointer | ||
| DataGridView.HitTestInfo hitTestInfo = this.dgInstances.HitTest(e.X, e.Y); | ||
| if (hitTestInfo.RowIndex >= 0) { | ||
| dgInstances.CurrentCell.Selected = false; | ||
| dgInstances.CurrentCell = dgInstances[0, hitTestInfo.RowIndex]; | ||
| } | ||
| try { | ||
| if (e.Button == MouseButtons.Right) | ||
| { | ||
|
|
||
| // select instance below mousepointer | ||
| DataGridView.HitTestInfo hitTestInfo = this.dgInstances.HitTest(e.X, e.Y); | ||
| if (hitTestInfo.RowIndex >= 0 && hitTestInfo.RowIndex < dgInstances.Rows.Count) { | ||
| if (dgInstances.CurrentCell != null) { | ||
| dgInstances.CurrentCell.Selected = false; | ||
| } | ||
| dgInstances.CurrentCell = dgInstances[0, hitTestInfo.RowIndex]; | ||
| } | ||
|
|
||
| AemInstance instance = this.SelectedInstanceInListview; | ||
| if (instance == null) { | ||
| return; | ||
| } | ||
| AemInstance instance = this.SelectedInstanceInListview; | ||
| if (instance == null) { | ||
| return; | ||
| } | ||
|
|
||
| // Context-Men� initialisieren | ||
| List<MenuItem> menuItems = new List<MenuItem>(); | ||
| MenuItem item; | ||
| // Context-Men� initialisieren | ||
| List<MenuItem> menuItems = new List<MenuItem>(); | ||
| MenuItem item; | ||
|
|
||
| item = new MenuItem(); | ||
| item.Text = "&Add..."; | ||
| item.Click += new EventHandler(addToolStripMenuItem_Click); | ||
| menuItems.Add(item); | ||
| item = new MenuItem(); | ||
| item.Text = "&Add..."; | ||
| item.Click += new EventHandler(addToolStripMenuItem_Click); | ||
| menuItems.Add(item); | ||
|
|
||
| item = new MenuItem(); | ||
| item.Text = "&Edit..."; | ||
| item.DefaultItem = true; | ||
| item.Click += new EventHandler(editToolStripMenuItem_Click); | ||
| menuItems.Add(item); | ||
| item = new MenuItem(); | ||
| item.Text = "&Edit..."; | ||
| item.DefaultItem = true; | ||
| item.Click += new EventHandler(editToolStripMenuItem_Click); | ||
| menuItems.Add(item); | ||
|
|
||
| item = new MenuItem(); | ||
| item.Text = "&Duplicate..."; | ||
| item.Click += new EventHandler(copyToolStripMenuItem_Click); | ||
| menuItems.Add(item); | ||
| item = new MenuItem(); | ||
| item.Text = "&Duplicate..."; | ||
| item.Click += new EventHandler(copyToolStripMenuItem_Click); | ||
| menuItems.Add(item); | ||
|
|
||
| item = new MenuItem(); | ||
| item.Text = "&Remove"; | ||
| item.Click += new EventHandler(removeToolStripMenuItem_Click); | ||
| menuItems.Add(item); | ||
| item = new MenuItem(); | ||
| item.Text = "&Remove"; | ||
| item.Click += new EventHandler(removeToolStripMenuItem_Click); | ||
| menuItems.Add(item); | ||
|
|
||
| item = new MenuItem(); | ||
| item.Text = "-"; | ||
| menuItems.Add(item); | ||
| item = new MenuItem(); | ||
| item.Text = "-"; | ||
| menuItems.Add(item); | ||
|
|
||
| item = new MenuItem(); | ||
| item.Text = "&Show in Taskbar"; | ||
| item.Click += new EventHandler(setShowInTaskbarToolStripMenuItem_Click); | ||
| item.Checked = instance.ShowInTaskbar; | ||
| menuItems.Add(item); | ||
| item = new MenuItem(); | ||
| item.Text = "&Show in Taskbar"; | ||
| item.Click += new EventHandler(setShowInTaskbarToolStripMenuItem_Click); | ||
| item.Checked = instance.ShowInTaskbar; | ||
| menuItems.Add(item); | ||
|
|
||
| dgInstances.ContextMenu = new ContextMenu(menuItems.ToArray()); | ||
| dgInstances.ContextMenu = new ContextMenu(menuItems.ToArray()); | ||
|
|
||
| dgInstances.ContextMenu.MenuItems.Add("-"); | ||
| AemActions.AddControlMenuItems(dgInstances.ContextMenu.MenuItems, instance); | ||
| dgInstances.ContextMenu.MenuItems.Add("-"); | ||
| AemActions.AddControlMenuItems(dgInstances.ContextMenu.MenuItems, instance); | ||
|
|
||
| dgInstances.ContextMenu.MenuItems.Add("-"); | ||
| AemActions.AddOpenMenuItems(dgInstances.ContextMenu.MenuItems, instance, false); | ||
| dgInstances.ContextMenu.MenuItems.Add("-"); | ||
| AemActions.AddOpenMenuItems(dgInstances.ContextMenu.MenuItems, instance, false); | ||
|
|
||
| dgInstances.ContextMenu.MenuItems.Add("-"); | ||
| AemActions.AddLogMenuItems(dgInstances.ContextMenu.MenuItems, instance); | ||
| dgInstances.ContextMenu.MenuItems.Add("-"); | ||
| AemActions.AddLogMenuItems(dgInstances.ContextMenu.MenuItems, instance); | ||
|
|
||
| dgInstances.ContextMenu.Show(dgInstances, e.Location); | ||
| dgInstances.ContextMenu.Show(dgInstances, e.Location); | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| // Handle resolution change errors gracefully | ||
| System.Diagnostics.Debug.WriteLine("Mouse event error: " + ex.Message); | ||
| } |
There was a problem hiding this comment.
The dgInstances_MouseClick handler wraps the entire method in a broad try/catch and only writes ex.Message via Debug.WriteLine. This can hide real failures (and loses stack traces). Prefer catching only the specific operations that can fail during DPI/rebinding, and log via the project’s log4net logger (including the exception) so issues can be diagnosed in production.
| try { | ||
| foreach (DataGridViewRow gridRow in dgInstances.Rows) { | ||
| if (gridRow.DataBoundItem != null) { | ||
| DataRow row = (DataRow)((DataRowView)gridRow.DataBoundItem).Row; | ||
| AemInstance instance = (AemInstance)row["Instance"]; | ||
| row["Status"] = instance.GetStatusText(); | ||
| } | ||
| } | ||
| } | ||
| catch { | ||
| // Handle any data binding errors during refresh | ||
| } |
There was a problem hiding this comment.
timerStatusRefresh_Tick wraps the entire row iteration in a single try/catch, so one bad row/binding state will stop status updates for all rows until the next tick, and the exception is silently swallowed. Consider handling exceptions per-row (or skipping only invalid DataBoundItem rows) and logging unexpected failures; also guard against the grid being disposed/handle not created during resolution changes.
| try { | |
| foreach (DataGridViewRow gridRow in dgInstances.Rows) { | |
| if (gridRow.DataBoundItem != null) { | |
| DataRow row = (DataRow)((DataRowView)gridRow.DataBoundItem).Row; | |
| AemInstance instance = (AemInstance)row["Instance"]; | |
| row["Status"] = instance.GetStatusText(); | |
| } | |
| } | |
| } | |
| catch { | |
| // Handle any data binding errors during refresh | |
| } | |
| if (this.dgInstances == null || this.dgInstances.IsDisposed || this.dgInstances.Disposing || !this.dgInstances.IsHandleCreated) { | |
| return; | |
| } | |
| foreach (DataGridViewRow gridRow in dgInstances.Rows) { | |
| if (gridRow == null) { | |
| continue; | |
| } | |
| try { | |
| DataRowView rowView = gridRow.DataBoundItem as DataRowView; | |
| if (rowView == null) { | |
| continue; | |
| } | |
| DataRow row = rowView.Row; | |
| if (row == null || !row.Table.Columns.Contains("Instance")) { | |
| continue; | |
| } | |
| AemInstance instance = row["Instance"] as AemInstance; | |
| if (instance == null) { | |
| continue; | |
| } | |
| row["Status"] = instance.GetStatusText(); | |
| } | |
| catch (Exception ex) { | |
| System.Diagnostics.Debug.WriteLine("Status refresh error for row " + gridRow.Index + ": " + ex.Message); | |
| } | |
| } |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
that's strange - i've never seen this type of error message. |
|
@stefanseifert I am fighting with this now for years and had this issue on several computers. I suspect another program influencing this behavior. Now with AI I thought I can tackle it :) |
Since starting with AEMManager I always faced issues like
The AEMManager then looked like

WIth support of GitHub CoPilot I am trying to address the issue.