-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
.NET version
10.0
Did it work in .NET Framework?
Yes
Did it work in any of the earlier releases of .NET Core or .NET 5+?
.NET 8
Issue description
On teardown, a custom control was found to be throwing:
System.InvalidCastException: <Internal>AccessibleObject cannot be cast to ControlAccessibleObject
at void Value.ThrowInvalidCast(Type from, Type to)
at bool System.Windows.Forms.PropertyStore.TryGetValue<T>(int key, out T value)
at void System.Windows.Forms.Control.OnHandleDestroyed(EventArgs e)
at void System.Windows.Forms.Control.WmDestroy(ref Message m)
at void System.Windows.Forms.Control.WndProc(ref Message m)
at void <internal.ControlBase>.WndProc(ref Message m)
at void <internal.Control>.WndProc(ref Message m)
at LRESULT System.Windows.Forms.NativeWindow.Callback(HWND hWnd, uint msg, WPARAM wparam, LPARAM lparam)
This appears to be the result of an undocumented breaking change in an interaction between System.Windows.Forms.Controls and System.Windows.Forms.PropertyStore. Specifically:
d08128b#diff-07a0a87cedab0d76c974ce8b105912a1b986c87116c7ee0ac73d6d5d65e4b48aL7643
introduced a subtle but significant change - previously, if the untyped get result from Properties.GetObject at s_accessibilityProperty returned a ControlAccessibleObject, then that accObj would have its Handle cleared. However, after the above change, the s_accessibilityProperty must be a ControlAccessibleObject, or else an invalid cast will be thrown:
| public bool TryGetValue<T>(int key, [NotNullWhen(true)] out T? value) | |
| { | |
| if (_values.TryGetValue(key, out Value foundValue)) | |
| { | |
| value = foundValue.Type == typeof(StrongBox<T>) | |
| ? foundValue.GetValue<StrongBox<T>>().Value | |
| : foundValue.GetValue<T>(); | |
| return value is not null; | |
| } | |
| value = default; | |
| return false; | |
| } |
Calls
winforms/src/System.Private.Windows.Core/src/System/Value.cs
Lines 1042 to 1050 in 6ac21ae
| public readonly T GetValue<T>() | |
| { | |
| if (!TryGetValue(out T value)) | |
| { | |
| ThrowInvalidCast(Type, typeof(T)); | |
| } | |
| return value; | |
| } |
which causes PropertyStore.TryGetValue to throw instead of returning null if the object it finds at the given key is not of type T.
Elsewhere in Control, any AccessibleObject, which may not be a ControlAccessibleObject, will be populated at that key:
winforms/src/System.Windows.Forms/System/Windows/Forms/Control.cs
Lines 526 to 530 in 6ac21ae
| if (!Properties.TryGetValue(s_accessibilityProperty, out AccessibleObject? accessibleObject)) | |
| { | |
| accessibleObject = CreateAccessibilityInstance().OrThrowIfNull(); | |
| Properties.AddValue(s_accessibilityProperty, accessibleObject); | |
| } |
meaning any override of
CreateAccessibleObject() that produces an AccessibleObject (which matches the type signature) may populate the PropertyStore with an object that will cause OnHandleDestroyed to throw.
Steps to reproduce
See above