Skip to content

A way to handle exceptions from ValueChangedEventManager #10654

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
n9 opened this issue Mar 26, 2025 · 7 comments
Open

A way to handle exceptions from ValueChangedEventManager #10654

n9 opened this issue Mar 26, 2025 · 7 comments
Labels
Investigate Requires further investigation by the WPF team.

Comments

@n9
Copy link

n9 commented Mar 26, 2025

WPF uses the ValueChangedEventManager to track .NET objects that do not implement the INotifyPropertyChanged interface. There is already an issue to add a runtime switch option to disable the ValueChangedEventManager (#10148).

This issue is not about disabling the ValueChangedEventManager completely, but about being able to handle an exception when for example Equals (used in ConcurrentDictionary`2.TryGetValue) throws an Exception, as in the following example, where the exception is caused by the state of COM objects:

System.Runtime.InteropServices.COMException (0x80040201): An event was unable to invoke any of the subscribers (0x80040201)
 at UIAutomationClient.CUIAutomation8Class.IUIAutomation6_CompareElements(IUIAutomationElement el1, IUIAutomationElement el2)
 at MyApp.AutomationElement.Equals(Object obj)
 at System.Collections.Concurrent.ConcurrentDictionary`2.TryGetValue(TKey key, TValue& value)
 at System.ComponentModel.PropertyDescriptor.RemoveValueChanged(Object component, EventHandler handler)
 at MS.Internal.Data.ValueChangedEventManager.ValueChangedRecord.StopListening()
 at MS.Internal.Data.ValueChangedEventManager.Purge(Object source, Object data, Boolean purgeAll)
 at MS.Internal.WeakEventTable.Purge(Boolean purgeAll)
 at MS.Internal.WeakEventTable.OnShutDown()
 at System.Windows.Threading.Dispatcher.ShutdownImplInSecurityContext(Object state)
 at MS.Internal.CulturePreservingExecutionContext.CallbackWrapper(Object obj)
 at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
--- End of stack trace from previous location ---
 at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
 at MS.Internal.CulturePreservingExecutionContext.Run(CulturePreservingExecutionContext executionContext, ContextCallback callback, Object state)
 at System.Windows.Threading.Dispatcher.ShutdownImpl()
 at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
 at System.Windows.Application.RunDispatcher(Object ignore)
 at System.Windows.Application.RunInternal(Window window)
 at MyApp.Main()
@n9 n9 added the Untriaged label Mar 26, 2025
@lindexi
Copy link
Member

lindexi commented Mar 28, 2025

As your stacktrace above, the main reason is the UIA (UI Automation) module in system has the exception or the UIA state in WPF be error.

As your stacktrace above, we can find this exception be raise in shutdown. Could you provide the repro demo?

@lindexi lindexi added Investigate Requires further investigation by the WPF team. and removed Untriaged labels Mar 28, 2025
@n9
Copy link
Author

n9 commented Mar 28, 2025

To reproduce:

  1. Create a class Foo that can (will) throw an exception inside Equals.
  2. Use this class in a complex binding: A.B.C. (Property B is of type Foo.)

The problem is that PropertyDescription uses ConcurrentDictionary for _valueChangedHandlers and does not use reference equality to compare components:

https://github.com/dotnet/runtime/blob/4f5c6938d09e935830492c006aa8381611b65ad8/src/libraries/System.ComponentModel.TypeConverter/src/System/ComponentModel/PropertyDescriptor.cs#L175

When ValueChangedEventManager.ValueChangedRecord.StopListening() tries to unsubscribe from value changes by calling PropertyDescriptor.RemoveValueChanged(), it will crash.

(It is not an issue of UIA. UIA may not be able to compare elements for various reasons: timeout, element no longer exists, ...)

@lindexi
Copy link
Member

lindexi commented Mar 28, 2025

@n9 Look like break in

_pd.RemoveValueChanged(_source, new EventHandler(OnValueChanged));

Can I know your MyApp.AutomationElement.Equals code?

I re-read the code, but I can not find any ways to remove it without call the Equals method.


It is not an issue of UIA. UIA may not be able to compare elements for various reasons: timeout, element no longer exists, ...

Yes, I agree with you.

@n9
Copy link
Author

n9 commented Mar 28, 2025

Can I know your MyApp.AutomationElement.Equals code?

Sure!

public override bool Equals(object? obj) => obj is AutomationElement element && Equals(element);
public bool Equals(AutomationElement? other) => other is not null && Automation.Native.CompareElements(Native, other.Native) != 0;

public override int GetHashCode() => RuntimeId?.GetHashCode() ?? 0;

Note: Automation.Native is IUIAutomation6 and RuntimeId is UIA_PropertyIds.UIA_RuntimeIdPropertyId converted to string. (If available. Unfortunately, not all UIA providers support RuntimeId.)


I re-read the code, but I can not find any ways to remove it without call the Equals method.

What about using ReferenceEqualityComparer?

@lindexi
Copy link
Member

lindexi commented Mar 31, 2025

@n9 Yeah, but I do not think you want to put the ReferenceEqualityComparer to your Equals method. And I do not think the System.ComponentModel.PropertyDescriptor.RemoveValueChanged can pass the ReferenceEqualityComparer.

@n9
Copy link
Author

n9 commented Apr 3, 2025

Yeah, but I do not think you want to put the ReferenceEqualityComparer to your Equals method.

Yes, the purpose of the ReferenceEqualityComparer is to avoid calling Equals.

And I do not think the System.ComponentModel.PropertyDescriptor.RemoveValueChanged can pass the ReferenceEqualityComparer.

The ReferenceEqualityComparer has to be passed to the constructor of ConcurrentDictionary. (The AddValueChanged is a virtual method.)

But I do not know if WPF and .NET teams would prefer to just change the WPF or to extend the PropertyDescriptor.

@lindexi
Copy link
Member

lindexi commented Apr 4, 2025

@n9

But I do not know if WPF and .NET teams would prefer to just change the WPF or to extend the PropertyDescriptor.

I do not think so. I do not think this proposal can pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Investigate Requires further investigation by the WPF team.
Projects
None yet
Development

No branches or pull requests

2 participants