Skip to content

UIElement.Measure not threadsafe in .NET 7+ #8447

@sra-michaelgold

Description

@sra-michaelgold

Description

It seems a change was introduced as part of the .NET 7 release that makes UIElement.Measure no longer thread-safe.

Our application loads a number of XPS reports (our custom objects that extend ContentControl) in separate dispatcher threads that we manage. Prior to upgrading to .NET 7 we can call Measure on the reports in parallel with no issues. Once upgrading to .NET 7 (same also happens in .NET 8) we start to get the exception below.

I think the problem may have been introduced in this commit:
dotnet/runtime@826896f
where hashtables were changed to dictionaries. Though the hashtable is not necessarily threadsafe, it seems to effectively be acting "more threadsafe" than the dictionary. That said, it's hard to follow this code, so it could be a red-herring.

I can get around the issue by synchronizing the calls to .Measure(), but ideally the behavior in .NET7+ would work the same as .NET 6 (and prior).

 ---> System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.ComponentModel.ReflectPropertyDescriptor.AddValueChanged(Object component, EventHandler handler)
   at MS.Internal.Data.ValueChangedEventManager.AddListener(Object source, PropertyDescriptor pd, IWeakEventListener listener, EventHandler`1 handler)
   at MS.Internal.Data.ValueChangedEventManager.AddHandler(Object source, EventHandler`1 handler, PropertyDescriptor pd)
   at MS.Internal.Data.PropertyPathWorker.ReplaceItem(Int32 k, Object newO, Object parent)
   at MS.Internal.Data.PropertyPathWorker.UpdateSourceValueState(Int32 k, ICollectionView collectionView, Object newValue, Boolean isASubPropertyChange)
   at System.Windows.Data.BindingExpression.Activate(Object item)
   at System.Windows.Data.BindingExpression.AttachToContext(AttachAttempt attempt)
   at System.Windows.Data.BindingExpression.AttachOverride(DependencyObject target, DependencyProperty dp)
   at System.Windows.StyleHelper.GetInstanceValue(UncommonField`1 dataField, DependencyObject container, FrameworkElement feChild, FrameworkContentElement fceChild, Int32 childIndex, DependencyProperty dp, Int32 i, EffectiveValueEntry& entry)
   at System.Windows.StyleHelper.GetChildValueHelper(UncommonField`1 dataField, ItemStructList`1& valueLookupList, DependencyProperty dp, DependencyObject container, FrameworkObject child, Int32 childIndex, Boolean styleLookup, EffectiveValueEntry& entry, ValueLookupType& sourceType, FrameworkElementFactory templateRoot)
   at System.Windows.StyleHelper.GetChildValue(UncommonField`1 dataField, DependencyObject container, Int32 childIndex, FrameworkObject child, DependencyProperty dp, FrugalStructList`1& childRecordFromChildIndex, EffectiveValueEntry& entry, ValueLookupType& sourceType, FrameworkElementFactory templateRoot)
   at System.Windows.StyleHelper.GetValueFromTemplatedParent(DependencyObject container, Int32 childIndex, FrameworkObject child, DependencyProperty dp, FrugalStructList`1& childRecordFromChildIndex, FrameworkElementFactory templateRoot, EffectiveValueEntry& entry)
   at System.Windows.StyleHelper.InvalidatePropertiesOnTemplateNode(DependencyObject container, FrameworkObject child, Int32 childIndex, FrugalStructList`1& childRecordFromChildIndex, Boolean isDetach, FrameworkElementFactory templateRoot)
   at System.Xaml.XamlObjectWriter.Logic_CreateAndAssignToParentStart(ObjectWriterContext ctx)
   at System.Xaml.XamlObjectWriter.WriteStartMember(XamlMember property)
   at System.Windows.FrameworkTemplate.LoadTemplateXaml(XamlReader templateReader, XamlObjectWriter currentWriter)

Reproduction Steps

Create a number of STA dispatcher threads and load the same XAML based control in all of the threads and call Measure() on the controls

Expected behavior

No exceptions

Actual behavior

 System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at System.Collections.Generic.Dictionary`2.TryInsert(TKey key, TValue value, InsertionBehavior behavior)
   at System.ComponentModel.ReflectPropertyDescriptor.AddValueChanged(Object component, EventHandler handler)
   at MS.Internal.Data.ValueChangedEventManager.AddListener(Object source, PropertyDescriptor pd, IWeakEventListener listener, EventHandler`1 handler)
   at MS.Internal.Data.ValueChangedEventManager.AddHandler(Object source, EventHandler`1 handler, PropertyDescriptor pd)
   at MS.Internal.Data.PropertyPathWorker.ReplaceItem(Int32 k, Object newO, Object parent)
   at MS.Internal.Data.PropertyPathWorker.UpdateSourceValueState(Int32 k, ICollectionView collectionView, Object newValue, Boolean isASubPropertyChange)
   at System.Windows.Data.BindingExpression.Activate(Object item)
   at System.Windows.Data.BindingExpression.AttachToContext(AttachAttempt attempt)
   at System.Windows.Data.BindingExpression.AttachOverride(DependencyObject target, DependencyProperty dp)
   at System.Windows.StyleHelper.GetInstanceValue(UncommonField`1 dataField, DependencyObject container, FrameworkElement feChild, FrameworkContentElement fceChild, Int32 childIndex, DependencyProperty dp, Int32 i, EffectiveValueEntry& entry)
   at System.Windows.StyleHelper.GetChildValueHelper(UncommonField`1 dataField, ItemStructList`1& valueLookupList, DependencyProperty dp, DependencyObject container, FrameworkObject child, Int32 childIndex, Boolean styleLookup, EffectiveValueEntry& entry, ValueLookupType& sourceType, FrameworkElementFactory templateRoot)
   at System.Windows.StyleHelper.GetChildValue(UncommonField`1 dataField, DependencyObject container, Int32 childIndex, FrameworkObject child, DependencyProperty dp, FrugalStructList`1& childRecordFromChildIndex, EffectiveValueEntry& entry, ValueLookupType& sourceType, FrameworkElementFactory templateRoot)
   at System.Windows.StyleHelper.GetValueFromTemplatedParent(DependencyObject container, Int32 childIndex, FrameworkObject child, DependencyProperty dp, FrugalStructList`1& childRecordFromChildIndex, FrameworkElementFactory templateRoot, EffectiveValueEntry& entry)
   at System.Windows.StyleHelper.InvalidatePropertiesOnTemplateNode(DependencyObject container, FrameworkObject child, Int32 childIndex, FrugalStructList`1& childRecordFromChildIndex, Boolean isDetach, FrameworkElementFactory templateRoot)
   at System.Xaml.XamlObjectWriter.Logic_CreateAndAssignToParentStart(ObjectWriterContext ctx)
   at System.Xaml.XamlObjectWriter.WriteStartMember(XamlMember property)
   at System.Windows.FrameworkTemplate.LoadTemplateXaml(XamlReader templateReader, XamlObjectWriter currentWriter)

Regression?

Yes, this worked fine in .NET6 and broke in .NET7+

Known Workarounds

Synchronize access to Measure()

Impact

Medium - there is a work-around, but it may have some performance impacts

Configuration

.NET7, .NET8
Windows 10 x64
I don't know if it is specific to this config.

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs more informationNot enough information has been provided. Please share more detail as requested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions