A way to handle exceptions from ValueChangedEventManager
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 (https://github.com/dotnet/wpf/issues/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()
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?
To reproduce:
- Create a class
Foothat can (will) throw an exception insideEquals. - Use this class in a complex binding:
A.B.C. (PropertyBis of typeFoo.)
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, ...)
@n9 Look like break in https://github.com/dotnet/wpf/blob/2a8258a72936092b24082c3e19f88d4b3e7e1e02/src/Microsoft.DotNet.Wpf/src/PresentationFramework/MS/Internal/Data/ValueChangedEventManager.cs#L492
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.
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?
@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.
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.
@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.