Skip to content

InvalidCastException from Control.OnHandleDestroyed when a class overrides CreateAccessibleObject #14291

@akindle

Description

@akindle

.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
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:

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions