Skip to content

Commit 6735caf

Browse files
authored
Fix InvalidCastException in Control.OnHandleDestroyed for non-ControlAccessibleObject (#14295)
<!-- Please read CONTRIBUTING.md before submitting a pull request --> Fixes #14291 ## Root Cause `Control.OnHandleDestroyed` used `Properties.TryGetValue<ControlAccessibleObject>` for both AccessibilityObject and NcAccessibilityObject, assuming the stored instance was always a ControlAccessibleObject. When a control overrides `CreateAccessibleObject()` to return a different AccessibleObject type, the generic `TryGetValue<T>` attempted an invalid cast and threw `InvalidCastException` during handle destruction. ## Proposed changes - Change the lookups in `Control.OnHandleDestroyed` for `s_accessibilityProperty` and `s_ncAccessibilityProperty` from `TryGetValue<ControlAccessibleObject>` to `TryGetValue<AccessibleObject>`, then use type pattern matching (`accObj is ControlAccessibleObject controlAccObj`) before resetting `controlAccObj.Handle = IntPtr.Zero`, so only true `ControlAccessibleObject` instances are manipulated and other AccessibleObject types no longer cause invalid casts. <!-- We are in TELL-MODE the following section must be completed --> ## Customer Impact - This change prevents `InvalidCastException` crashes when controls with custom accessible objects are destroyed ## Regression? - Yes (Regress by commit d08128b#diff-07a0a87cedab0d76c974ce8b105912a1b986c87116c7ee0ac73d6d5d65e4b48aL7643) ## Risk - Minimal <!-- end TELL-MODE --> ## Screenshots <!-- Remove this section if PR does not change UI --> ### Before Sample project: [WinFormsApp15.zip](https://github.com/user-attachments/files/25253150/WinFormsApp15.zip) On teardown, If a custom overrode CreateAccessibleObject() and returned a different AccessibleObject type, TryGetValue<ControlAccessibleObject> threw an InvalidCastException when the handle was destroyed, potentially crashing the app. https://github.com/user-attachments/assets/c37feba7-58f8-47db-a20e-fcc540ebc11d ### After Custom accessible objects that are not `ControlAccessibleObject` are left untouched during handle destruction, so no `InvalidCastException` is thrown and the app stays stable. https://github.com/user-attachments/assets/9d89b29c-c1b4-4b37-924e-4eaf23bec610 ## Test methodology <!-- How did you ensure quality? --> - Manually ## Test environment(s) <!-- Remove any that don't apply --> - .net 11.0.0-preview.2.26080.101 <!-- Mention language, UI scaling, or anything else that might be relevant --> ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/dotnet/winforms/pull/14295)
1 parent f75ac9a commit 6735caf

File tree

3 files changed

+20
-3
lines changed

3 files changed

+20
-3
lines changed

src/System.Windows.Forms/System/Windows/Forms/Control.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7518,14 +7518,16 @@ protected virtual void OnHandleDestroyed(EventArgs e)
75187518
((EventHandler?)Events[s_handleDestroyedEvent])?.Invoke(this, e);
75197519

75207520
// The Accessibility Object for this Control
7521-
if (Properties.TryGetValue(s_accessibilityProperty, out ControlAccessibleObject? accObj))
7521+
if (Properties.TryGetValue(s_accessibilityProperty, out AccessibleObject? accObj)
7522+
&& accObj is ControlAccessibleObject controlAccObj)
75227523
{
7523-
accObj.Handle = IntPtr.Zero;
7524+
controlAccObj.Handle = IntPtr.Zero;
75247525
}
75257526

75267527
// Private accessibility object for control, used to wrap the object that
75277528
// OLEACC.DLL creates to represent the control's non-client (NC) region.
7528-
if (Properties.TryGetValue(s_ncAccessibilityProperty, out ControlAccessibleObject? nonClientAccessibleObject))
7529+
if (Properties.TryGetValue(s_ncAccessibilityProperty, out AccessibleObject? ncAccObj)
7530+
&& ncAccObj is ControlAccessibleObject nonClientAccessibleObject)
75297531
{
75307532
nonClientAccessibleObject.Handle = IntPtr.Zero;
75317533
}

src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.Methods.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,19 @@ public void Control_CreateAccessibilityInstance_Invoke_ReturnsExpected(bool crea
412412
Assert.Equal(createHandle, control.IsHandleCreated);
413413
}
414414

415+
[WinFormsFact]
416+
public void Control_OnHandleDestroyed_CustomAccessibleObject_DoesNotThrow1()
417+
{
418+
using CustomCreateAccessibilityInstanceControl control = new()
419+
{
420+
CreateAccessibilityResult = new AccessibleObject()
421+
};
422+
423+
AccessibleObject accessibleObject = control.AccessibilityObject;
424+
Action action = () => control.InvokeOnHandleDestroyed(EventArgs.Empty);
425+
action.Should().NotThrow();
426+
}
427+
415428
[WinFormsFact]
416429
public void Control_CreateControl_Invoke_Success()
417430
{

src/test/unit/System.Windows.Forms/System/Windows/Forms/ControlTests.Properties.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ private class CustomCreateAccessibilityInstanceControl : Control
100100
public AccessibleObject CreateAccessibilityResult { get; set; }
101101

102102
protected override AccessibleObject CreateAccessibilityInstance() => CreateAccessibilityResult;
103+
104+
public void InvokeOnHandleDestroyed(EventArgs eventArgs) => OnHandleDestroyed(eventArgs);
103105
}
104106

105107
[WinFormsTheory]

0 commit comments

Comments
 (0)