From fff570ea87764eba671d0971751c7f9831c9043e Mon Sep 17 00:00:00 2001 From: Mikhail Poliudov Date: Fri, 10 Oct 2025 16:43:25 +0700 Subject: [PATCH 1/4] Implementad Safe Enumerable List Changed Visual/Logical Children properties to use CopyOnWrite --- src/Avalonia.Base/StyledElement.cs | 23 +-- .../Utilities/SafeEnumerableList.cs | 179 ++++++++++++++++++ src/Avalonia.Base/Visual.cs | 60 +++--- .../Primitives/TemplatedControl.cs | 7 +- 4 files changed, 218 insertions(+), 51 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/SafeEnumerableList.cs diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index b7f6262d9f..df775b7211 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -14,6 +14,7 @@ using Avalonia.Logging; using Avalonia.LogicalTree; using Avalonia.PropertyStore; using Avalonia.Styling; +using Avalonia.Utilities; namespace Avalonia { @@ -85,7 +86,7 @@ namespace Avalonia private string? _name; private Classes? _classes; private ILogicalRoot? _logicalRoot; - private AvaloniaList? _logicalChildren; + private SafeEnumerableList? _logicalChildren; private IResourceDictionary? _resources; private Styles? _styles; private bool _stylesApplied; @@ -278,7 +279,7 @@ namespace Avalonia { if (_logicalChildren == null) { - var list = new AvaloniaList + var list = new SafeEnumerableList { ResetBehavior = ResetBehavior.Remove, Validator = this @@ -742,12 +743,10 @@ namespace Avalonia element._dataContextUpdating = true; element.OnDataContextBeginUpdate(); - var logicalChildren = element.LogicalChildren; - var logicalChildrenCount = logicalChildren.Count; - for (var i = 0; i < logicalChildrenCount; i++) + foreach (var child in element.LogicalChildren) { - if (element.LogicalChildren[i] is StyledElement s && + if (child is StyledElement s && s.InheritanceParent == element && !s.IsSet(DataContextProperty)) { @@ -901,12 +900,10 @@ namespace Avalonia AttachedToLogicalTree?.Invoke(this, e); } - var logicalChildren = LogicalChildren; - var logicalChildrenCount = logicalChildren.Count; - for (var i = 0; i < logicalChildrenCount; i++) + foreach (var s in LogicalChildren) { - if (logicalChildren[i] is StyledElement child && child._logicalRoot != e.Root) // child may already have been attached within an event handler + if (s is StyledElement child && child._logicalRoot != e.Root) // child may already have been attached within an event handler { child.OnAttachedToLogicalTreeCore(e); } @@ -922,12 +919,10 @@ namespace Avalonia OnDetachedFromLogicalTree(e); DetachedFromLogicalTree?.Invoke(this, e); - var logicalChildren = LogicalChildren; - var logicalChildrenCount = logicalChildren.Count; - for (var i = 0; i < logicalChildrenCount; i++) + foreach (var s in LogicalChildren) { - if (logicalChildren[i] is StyledElement child) + if (s is StyledElement child) { child.OnDetachedFromLogicalTreeCore(e); } diff --git a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs b/src/Avalonia.Base/Utilities/SafeEnumerableList.cs new file mode 100644 index 0000000000..1789053ad7 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SafeEnumerableList.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Collections; + +namespace Avalonia.Utilities +{ + internal class SafeEnumerableList : IAvaloniaList, INotifyCollectionChanged + { + private AvaloniaList _list = new(); + private int _generation; + private int _enumCount = 0; + + public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event PropertyChangedEventHandler? PropertyChanged; + + public SafeEnumerableList() + { + _list.CollectionChanged += List_CollectionChanged; + _list.PropertyChanged += List_PropertyChanged; + } + + private void List_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + + private void List_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + CollectionChanged?.Invoke(this, e); + } + + public SafeListEnumerator GetEnumerator() => new SafeListEnumerator(this, _list); + + public int IndexOf(T item) + { + return _list.IndexOf(item); + } + + public void Insert(int index, T item) + { + GetList().Insert(index, item); + } + + public void RemoveAt(int index) + { + GetList().RemoveAt(index); + } + + public void Add(T item) + { + GetList().Add(item); + } + + public void Clear() + { + GetList().Clear(); + } + + public bool Contains(T item) + { + return _list.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _list.CopyTo(array, arrayIndex); + } + + public bool Remove(T item) + { + return GetList().Remove(item); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + private AvaloniaList GetList() + { + if (_enumCount > 0) + { + _list.CollectionChanged -= List_CollectionChanged; + _list = new(_list) + { + Validator = Validator, + ResetBehavior = ResetBehavior, + }; + _list.CollectionChanged += List_CollectionChanged; + ++_generation; + _enumCount = 0; + } + return _list; + } + + public void AddRange(IEnumerable items) + { + ((IAvaloniaList)GetList()).AddRange(items); + } + + public void InsertRange(int index, IEnumerable items) + { + ((IAvaloniaList)GetList()).InsertRange(index, items); + } + + public void Move(int oldIndex, int newIndex) + { + ((IAvaloniaList)GetList()).Move(oldIndex, newIndex); + } + + public void MoveRange(int oldIndex, int count, int newIndex) + { + ((IAvaloniaList)GetList()).MoveRange(oldIndex, count, newIndex); + } + + public void RemoveAll(IEnumerable items) + { + ((IAvaloniaList)GetList()).RemoveAll(items); + } + + public void RemoveRange(int index, int count) + { + ((IAvaloniaList)GetList()).RemoveRange(index, count); + } + + public int Count => _list.Count; + + public bool IsReadOnly => ((ICollection)_list).IsReadOnly; + + public ResetBehavior ResetBehavior { get => _list.ResetBehavior; set => _list.ResetBehavior = value; } + public IAvaloniaListItemValidator? Validator { get => _list.Validator; set => _list.Validator = value; } + + public T this[int index] { get => _list[index]; set => GetList()[index] = value; } + + public struct SafeListEnumerator : IEnumerator + { + private readonly SafeEnumerableList _owner; + private readonly int _generation; + private readonly AvaloniaList.Enumerator _enumerator; + + public SafeListEnumerator(SafeEnumerableList owner, AvaloniaList list) + { + _owner = owner; + _generation = owner._generation; + ++owner._enumCount; + _enumerator = list.GetEnumerator(); + } + + public bool MoveNext() + => _enumerator.MoveNext(); + + public T Current => _enumerator.Current; + + object IEnumerator.Current => ((IEnumerator)_enumerator).Current; + + public void Reset() => throw new InvalidOperationException(); + + public void Dispose() + { + if (_generation == _owner._generation) + { + --_owner._enumCount; + } + } + } + } +} diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 6b10fa228c..40ba64562d 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -4,6 +4,7 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; using Avalonia.Data; @@ -39,7 +40,7 @@ namespace Avalonia /// public static readonly DirectProperty BoundsProperty = AvaloniaProperty.RegisterDirect(nameof(Bounds), o => o.Bounds); - + /// /// Defines the property. /// @@ -51,7 +52,7 @@ namespace Avalonia /// public static readonly StyledProperty ClipProperty = AvaloniaProperty.Register(nameof(Clip)); - + /// /// Defines the property. /// @@ -69,7 +70,7 @@ namespace Avalonia /// public static readonly StyledProperty OpacityMaskProperty = AvaloniaProperty.Register(nameof(OpacityMask)); - + /// /// Defines the property. /// @@ -113,7 +114,7 @@ namespace Avalonia /// public static readonly StyledProperty ZIndexProperty = AvaloniaProperty.Register(nameof(ZIndex)); - + private static readonly WeakEvent InvalidatedWeakEvent = WeakEvent.Register( (s, h) => s.Invalidated += h, @@ -201,7 +202,7 @@ namespace Avalonia /// Gets a value indicating whether this control and all its parents are visible. /// public bool IsEffectivelyVisible { get; private set; } = true; - + /// /// Updates the property based on the parent's /// . @@ -218,7 +219,7 @@ namespace Avalonia // PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ // will cause extra allocations and overhead. - + var children = VisualChildren; // ReSharper disable once ForCanBeConvertedToForeach @@ -255,7 +256,7 @@ namespace Avalonia get { return GetValue(OpacityMaskProperty); } set { SetValue(OpacityMaskProperty, value); } } - + /// /// Gets or sets the effect of the control. /// @@ -269,8 +270,8 @@ namespace Avalonia /// /// Gets or sets a value indicating whether to apply mirror transform on this control. /// - public bool HasMirrorTransform - { + public bool HasMirrorTransform + { get { return _hasMirrorTransform; } protected set { SetAndRaise(HasMirrorTransformProperty, ref _hasMirrorTransform, value); } } @@ -413,8 +414,8 @@ namespace Avalonia sender.InvalidateVisual(); } }); - - + + var invalidateAndSubscribeObserver = new AnonymousObserver( static e => { @@ -466,7 +467,7 @@ namespace Avalonia if (change.Property == IsVisibleProperty) { UpdateIsEffectivelyVisible(VisualParent?.IsEffectivelyVisible ?? true); - } + } else if (change.Property == FlowDirectionProperty) { InvalidateMirrorTransform(); @@ -477,7 +478,7 @@ namespace Avalonia } } } - + protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { base.LogicalChildrenCollectionChanged(sender, e); @@ -515,18 +516,16 @@ namespace Avalonia OnAttachedToVisualTree(e); AttachedToVisualTree?.Invoke(this, e); InvalidateVisual(); - + _visualRoot.Renderer.RecalculateChildren(_visualParent); - + if (ZIndex != 0) _visualParent.HasNonUniformZIndexChildren = true; - - var visualChildren = VisualChildren; - var visualChildrenCount = visualChildren.Count; - for (var i = 0; i < visualChildrenCount; i++) + + foreach (var child in VisualChildren) { - if (visualChildren[i] is { } child && child._visualRoot != e.Root) // child may already have been attached within an event handler + if (child is { } && child._visualRoot != e.Root) // child may already have been attached within an event handler { child.OnAttachedToVisualTreeCore(e); } @@ -558,12 +557,10 @@ namespace Avalonia DetachedFromVisualTree?.Invoke(this, e); e.Root.Renderer.AddDirty(this); - var visualChildren = VisualChildren; - var visualChildrenCount = visualChildren.Count; - for (var i = 0; i < visualChildrenCount; i++) + foreach (var child in VisualChildren) { - if (visualChildren[i] is { } child) + if (child is { }) { child.OnDetachedFromVisualTreeCore(e); } @@ -617,7 +614,7 @@ namespace Avalonia { newTransform.Changed += sender.RenderTransformChanged; } - + sender.InvalidateVisual(); } } @@ -651,7 +648,7 @@ namespace Avalonia var parent = sender?.VisualParent; if (sender?.ZIndex != 0 && parent is Visual parentVisual) parentVisual.HasNonUniformZIndexChildren = true; - + sender?.InvalidateVisual(); parent?.VisualRoot?.Renderer.RecalculateChildren(parent); } @@ -721,15 +718,15 @@ namespace Avalonia break; } } - + private static void SetVisualParent(IList children, Visual? parent) { var count = children.Count; for (var i = 0; i < count; i++) { - var visual = (Visual) children[i]!; - + var visual = (Visual)children[i]!; + visual.SetVisualParent(parent); } } @@ -738,12 +735,11 @@ namespace Avalonia { base.OnTemplatedParentControlThemeChanged(); - var count = VisualChildren.Count; var templatedParent = TemplatedParent; - for (var i = 0; i < count; ++i) + foreach (var child in VisualChildren) { - if (VisualChildren[i] is StyledElement child && + if (child is not null && child.TemplatedParent == templatedParent) { child.OnTemplatedParentControlThemeChanged(); diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index bd4d08ecc2..63668671e8 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -411,12 +411,9 @@ namespace Avalonia.Controls.Primitives { control.TemplatedParent = templatedParent; - var children = control.LogicalChildren; - var count = children.Count; - - for (var i = 0; i < count; i++) + foreach (var s in control.LogicalChildren) { - if (children[i] is StyledElement child && child.TemplatedParent is null) + if (s is StyledElement child && child.TemplatedParent is null) { ApplyTemplatedParent(child, templatedParent); } From 01245657d45f382dcd067b80dee70914e34a0c27 Mon Sep 17 00:00:00 2001 From: Mikhail Poliudov Date: Mon, 10 Nov 2025 19:02:19 +0700 Subject: [PATCH 2/4] Test added --- .../Utilities/SafeEnumerableList.cs | 4 +- .../SafeEnumerateCollectionTests.cs | 198 ++++++++++++++++++ 2 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/Collections/SafeEnumerateCollectionTests.cs diff --git a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs b/src/Avalonia.Base/Utilities/SafeEnumerableList.cs index 1789053ad7..b0601e7639 100644 --- a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs +++ b/src/Avalonia.Base/Utilities/SafeEnumerableList.cs @@ -148,7 +148,7 @@ namespace Avalonia.Utilities { private readonly SafeEnumerableList _owner; private readonly int _generation; - private readonly AvaloniaList.Enumerator _enumerator; + private readonly IEnumerator _enumerator; public SafeListEnumerator(SafeEnumerableList owner, AvaloniaList list) { @@ -163,7 +163,7 @@ namespace Avalonia.Utilities public T Current => _enumerator.Current; - object IEnumerator.Current => ((IEnumerator)_enumerator).Current; + object IEnumerator.Current => _enumerator.Current!; public void Reset() => throw new InvalidOperationException(); diff --git a/tests/Avalonia.Base.UnitTests/Collections/SafeEnumerateCollectionTests.cs b/tests/Avalonia.Base.UnitTests/Collections/SafeEnumerateCollectionTests.cs new file mode 100644 index 0000000000..e31a20d4c8 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Collections/SafeEnumerateCollectionTests.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Base.UnitTests.Collections +{ + public class SafeEnumerateCollectionTests + { + private class ViewModel1 + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + + private class ViewModel2 + { + public string CarProducer { get; set; } + public string CarModel { get; set; } + } + + [Fact] + public void NoExceptionWhenDetachingTabControlWithTemplate() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + TabControl tabControl; + IDataTemplate contentTemplate = new FuncDataTemplate( + (i, ns) => + { + return + new Border() + { + BorderBrush = Brushes.Red, + BorderThickness = new Thickness(4), + Padding = new Thickness(3), + Child = new ContentControl + { + Content = i, + } + }; + }); + /*IDataTemplate windowTemplate = new FuncDataTemplate((i, ns) => + { + + });*/ + var window = new Window + { + /*DataTemplates = + { + new FuncDataTemplate( + (vm1,ns) => + { + return new Grid + { + ColumnDefinitions = { new(GridLength.Auto), new(1, GridUnitType.Star) }, + RowDefinitions = { new(GridLength.Auto), new(GridLength.Auto), new(new GridLength(1, GridUnitType.Star)) }, + Children = + { + new TextBlock + { + Text = "FirstName", + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Left, + [Grid.ColumnProperty] = 0, + [Grid.RowProperty] = 0, + }, + new TextBox + { + [TextBox.TextProperty.Bind()] = new Binding(nameof(ViewModel1.FirstName)), + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Stretch, + [Grid.ColumnProperty] = 1, + [Grid.RowProperty] = 0, + }, + new TextBlock + { + Text = "LastName", + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Left, + [Grid.ColumnProperty] = 0, + [Grid.RowProperty] = 1, + }, + new TextBox + { + [TextBox.TextProperty.Bind()] = new Binding(nameof(ViewModel1.LastName)), + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Stretch, + [Grid.ColumnProperty] = 1, + [Grid.RowProperty] = 1, + }, + } + }; + }), + new FuncDataTemplate( + (vm2,ns) => + { + return new Grid + { + ColumnDefinitions = { new(GridLength.Auto), new(1, GridUnitType.Star) }, + RowDefinitions = { new(GridLength.Auto), new(GridLength.Auto), new(GridLength.Auto), new(new GridLength(1, GridUnitType.Star)) }, + Children = + { + new TextBlock + { + Text = "Car", + VerticalAlignment = VerticalAlignment.Center, + Margin = new Thickness(24, 12), + FontWeight = FontWeight.Bold, + FontSize = 24, + [Grid.ColumnProperty] = 0, + [Grid.ColumnSpanProperty] = 2, + [Grid.RowProperty] = 0, + }, + new TextBlock + { + Text = "Producer", + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Left, + [Grid.ColumnProperty] = 0, + [Grid.RowProperty] = 1, + }, + new TextBox + { + [TextBox.TextProperty.Bind()] = new Binding(nameof(ViewModel2.CarProducer)), + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Stretch, + [Grid.ColumnProperty] = 1, + [Grid.RowProperty] = 1, + }, + new TextBlock + { + Text = "Model", + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Left, + [Grid.ColumnProperty] = 0, + [Grid.RowProperty] = 2, + }, + new TextBox + { + [TextBox.TextProperty.Bind()] = new Binding(nameof(ViewModel2.CarModel)), + VerticalAlignment = VerticalAlignment.Center, + HorizontalAlignment = HorizontalAlignment.Stretch, + [Grid.ColumnProperty] = 1, + [Grid.RowProperty] = 2, + }, + } + }; + }), + },*/ + Content = tabControl = new TabControl + { + ItemsSource = new object[] + { + new ViewModel1 + { + FirstName = "Vasily", + LastName = "Pupkin", + }, + new ViewModel2 + { + CarProducer = "Fiat", + CarModel = "Uno", + }, + }, + SelectedIndex = 0, + }, + Styles= + { + new Style(x => x.Is()) + { + Setters = + { + new Setter{ Property = TabItem.ContentTemplateProperty, + Value = contentTemplate, + } + } + } + }, + Width = 640, + Height = 480, + }; + window.Show(); + window.Close(); + } + } + } +} From 11491e93bb109b334e2066bbe5529c0e92e93e36 Mon Sep 17 00:00:00 2001 From: Mikhail Poliudov Date: Wed, 14 Jan 2026 15:36:27 +0700 Subject: [PATCH 3/4] PR fixes --- .../Utilities/SafeEnumerableList.cs | 14 +- src/Avalonia.Base/Visual.Composition.cs | 3 +- src/Avalonia.Base/Visual.cs | 2 +- .../SafeEnumerateCollectionTests.cs | 115 +-------------- .../Utils/SafeEnumerableListTests.cs | 131 ++++++++++++++++++ 5 files changed, 146 insertions(+), 119 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/Utils/SafeEnumerableListTests.cs diff --git a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs b/src/Avalonia.Base/Utilities/SafeEnumerableList.cs index b0601e7639..2c87194c8e 100644 --- a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs +++ b/src/Avalonia.Base/Utilities/SafeEnumerableList.cs @@ -20,6 +20,8 @@ namespace Avalonia.Utilities public event NotifyCollectionChangedEventHandler? CollectionChanged; public event PropertyChangedEventHandler? PropertyChanged; + //for test purposes + internal AvaloniaList Inner => _list; public SafeEnumerableList() { _list.CollectionChanged += List_CollectionChanged; @@ -107,32 +109,32 @@ namespace Avalonia.Utilities public void AddRange(IEnumerable items) { - ((IAvaloniaList)GetList()).AddRange(items); + GetList().AddRange(items); } public void InsertRange(int index, IEnumerable items) { - ((IAvaloniaList)GetList()).InsertRange(index, items); + GetList().InsertRange(index, items); } public void Move(int oldIndex, int newIndex) { - ((IAvaloniaList)GetList()).Move(oldIndex, newIndex); + GetList().Move(oldIndex, newIndex); } public void MoveRange(int oldIndex, int count, int newIndex) { - ((IAvaloniaList)GetList()).MoveRange(oldIndex, count, newIndex); + GetList().MoveRange(oldIndex, count, newIndex); } public void RemoveAll(IEnumerable items) { - ((IAvaloniaList)GetList()).RemoveAll(items); + GetList().RemoveAll(items); } public void RemoveRange(int index, int count) { - ((IAvaloniaList)GetList()).RemoveRange(index, count); + GetList().RemoveRange(index, count); } public int Count => _list.Count; diff --git a/src/Avalonia.Base/Visual.Composition.cs b/src/Avalonia.Base/Visual.Composition.cs index f521f2a3f2..79741a5492 100644 --- a/src/Avalonia.Base/Visual.Composition.cs +++ b/src/Avalonia.Base/Visual.Composition.cs @@ -3,7 +3,6 @@ using Avalonia.Collections.Pooled; using Avalonia.Media; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Server; -using Avalonia.VisualTree; namespace Avalonia; @@ -45,7 +44,7 @@ public partial class Visual if(CompositionVisual == null) return; var compositionChildren = CompositionVisual.Children; - var visualChildren = (AvaloniaList)VisualChildren; + var visualChildren = VisualChildren; PooledList<(Visual visual, int index)>? sortedChildren = null; if (HasNonUniformZIndexChildren && visualChildren.Count > 1) diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 40ba64562d..25cf15a848 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -154,7 +154,7 @@ namespace Avalonia // Disable transitions until we're added to the visual tree. DisableTransitions(); - var visualChildren = new AvaloniaList(); + var visualChildren = new SafeEnumerableList(); visualChildren.ResetBehavior = ResetBehavior.Remove; visualChildren.Validator = this; visualChildren.CollectionChanged += VisualChildrenChanged; diff --git a/tests/Avalonia.Base.UnitTests/Collections/SafeEnumerateCollectionTests.cs b/tests/Avalonia.Base.UnitTests/Collections/SafeEnumerateCollectionTests.cs index e31a20d4c8..e74b1d0bed 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/SafeEnumerateCollectionTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/SafeEnumerateCollectionTests.cs @@ -19,14 +19,14 @@ namespace Avalonia.Base.UnitTests.Collections { private class ViewModel1 { - public string FirstName { get; set; } - public string LastName { get; set; } + public string FirstName { get; set; } = ""; + public string LastName { get; set; } = ""; } private class ViewModel2 { - public string CarProducer { get; set; } - public string CarModel { get; set; } + public string CarProducer { get; set; } = ""; + public string CarModel { get; set; } = ""; } [Fact] @@ -50,114 +50,9 @@ namespace Avalonia.Base.UnitTests.Collections } }; }); - /*IDataTemplate windowTemplate = new FuncDataTemplate((i, ns) => - { - - });*/ + var window = new Window { - /*DataTemplates = - { - new FuncDataTemplate( - (vm1,ns) => - { - return new Grid - { - ColumnDefinitions = { new(GridLength.Auto), new(1, GridUnitType.Star) }, - RowDefinitions = { new(GridLength.Auto), new(GridLength.Auto), new(new GridLength(1, GridUnitType.Star)) }, - Children = - { - new TextBlock - { - Text = "FirstName", - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Left, - [Grid.ColumnProperty] = 0, - [Grid.RowProperty] = 0, - }, - new TextBox - { - [TextBox.TextProperty.Bind()] = new Binding(nameof(ViewModel1.FirstName)), - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Stretch, - [Grid.ColumnProperty] = 1, - [Grid.RowProperty] = 0, - }, - new TextBlock - { - Text = "LastName", - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Left, - [Grid.ColumnProperty] = 0, - [Grid.RowProperty] = 1, - }, - new TextBox - { - [TextBox.TextProperty.Bind()] = new Binding(nameof(ViewModel1.LastName)), - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Stretch, - [Grid.ColumnProperty] = 1, - [Grid.RowProperty] = 1, - }, - } - }; - }), - new FuncDataTemplate( - (vm2,ns) => - { - return new Grid - { - ColumnDefinitions = { new(GridLength.Auto), new(1, GridUnitType.Star) }, - RowDefinitions = { new(GridLength.Auto), new(GridLength.Auto), new(GridLength.Auto), new(new GridLength(1, GridUnitType.Star)) }, - Children = - { - new TextBlock - { - Text = "Car", - VerticalAlignment = VerticalAlignment.Center, - Margin = new Thickness(24, 12), - FontWeight = FontWeight.Bold, - FontSize = 24, - [Grid.ColumnProperty] = 0, - [Grid.ColumnSpanProperty] = 2, - [Grid.RowProperty] = 0, - }, - new TextBlock - { - Text = "Producer", - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Left, - [Grid.ColumnProperty] = 0, - [Grid.RowProperty] = 1, - }, - new TextBox - { - [TextBox.TextProperty.Bind()] = new Binding(nameof(ViewModel2.CarProducer)), - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Stretch, - [Grid.ColumnProperty] = 1, - [Grid.RowProperty] = 1, - }, - new TextBlock - { - Text = "Model", - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Left, - [Grid.ColumnProperty] = 0, - [Grid.RowProperty] = 2, - }, - new TextBox - { - [TextBox.TextProperty.Bind()] = new Binding(nameof(ViewModel2.CarModel)), - VerticalAlignment = VerticalAlignment.Center, - HorizontalAlignment = HorizontalAlignment.Stretch, - [Grid.ColumnProperty] = 1, - [Grid.RowProperty] = 2, - }, - } - }; - }), - },*/ Content = tabControl = new TabControl { ItemsSource = new object[] diff --git a/tests/Avalonia.Controls.UnitTests/Utils/SafeEnumerableListTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/SafeEnumerableListTests.cs new file mode 100644 index 0000000000..7ac9f0006a --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Utils/SafeEnumerableListTests.cs @@ -0,0 +1,131 @@ +using System.Collections.Generic; +using Avalonia.UnitTests; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Utils +{ + public class SafeEnumerableListTests : ScopedTestBase + { + [Fact] + public void List_Is_Not_Copied_Outside_Enumeration() + { + var target = new SafeEnumerableList(); + var inner = target.Inner; + + target.Add("foo"); + target.Add("bar"); + target.Remove("foo"); + + Assert.Same(inner, target.Inner); + } + + [Fact] + public void List_Is_Copied_Outside_Enumeration() + { + var target = new SafeEnumerableList(); + var inner = target.Inner; + + target.Add("foo"); + + foreach (var i in target) + { + Assert.Same(inner, target.Inner); + target.Add("bar"); + Assert.NotSame(inner, target.Inner); + Assert.Equal("foo", i); + } + + inner = target.Inner; + + foreach (var i in target) + { + target.Add("baz"); + Assert.NotSame(inner, target.Inner); + } + + Assert.Equal(new List { "foo", "bar", "baz", "baz" }, target); + } + + [Fact] + public void List_Is_Not_Copied_After_Enumeration() + { + var target = new SafeEnumerableList(); + var inner = target.Inner; + + target.Add("foo"); + + foreach (var i in target) + { + target.Add("bar"); + Assert.NotSame(inner, target.Inner); + inner = target.Inner; + Assert.Equal("foo", i); + } + + target.Add("baz"); + Assert.Same(inner, target.Inner); + } + + [Fact] + public void List_Is_Copied_Only_Once_During_Enumeration() + { + var target = new SafeEnumerableList(); + var inner = target.Inner; + + target.Add("foo"); + + foreach (var i in target) + { + target.Add("bar"); + Assert.NotSame(inner, target.Inner); + inner = target.Inner; + target.Add("baz"); + Assert.Same(inner, target.Inner); + } + + target.Add("baz"); + } + + [Fact] + public void List_Is_Copied_During_Nested_Enumerations() + { + var target = new SafeEnumerableList(); + var initialInner = target.Inner; + var firstItems = new List(); + var secondItems = new List(); + Collections.AvaloniaList firstInner; + Collections.AvaloniaList secondInner; + + target.Add("foo"); + + foreach (var i in target) + { + target.Add("bar"); + + firstInner = target.Inner; + Assert.NotSame(initialInner, firstInner); + + foreach (var j in target) + { + target.Add("baz"); + + secondInner = target.Inner; + Assert.NotSame(firstInner, secondInner); + + secondItems.Add(j); + } + + firstItems.Add(i); + } + + Assert.Equal(new List { "foo" }, firstItems); + Assert.Equal(new List { "foo", "bar" }, secondItems); + Assert.Equal(new List { "foo", "bar", "baz", "baz" }, target); + + var finalInner = target.Inner; + target.Add("final"); + Assert.Same(finalInner, target.Inner); + } + } +} From 35cdea07853e48b94da76d7200d85cb7444012e9 Mon Sep 17 00:00:00 2001 From: Mikhail Poliudov Date: Fri, 23 Jan 2026 18:36:36 +0700 Subject: [PATCH 4/4] fix - added IList implementation to SafeEnumerableList (there was an error using old DevTools, may be actual for new DevTools) --- .../Utilities/SafeEnumerableList.cs | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs b/src/Avalonia.Base/Utilities/SafeEnumerableList.cs index 2c87194c8e..4e0e7ddf9a 100644 --- a/src/Avalonia.Base/Utilities/SafeEnumerableList.cs +++ b/src/Avalonia.Base/Utilities/SafeEnumerableList.cs @@ -11,7 +11,7 @@ using Avalonia.Collections; namespace Avalonia.Utilities { - internal class SafeEnumerableList : IAvaloniaList, INotifyCollectionChanged + internal class SafeEnumerableList : IAvaloniaList, IList, INotifyCollectionChanged { private AvaloniaList _list = new(); private int _generation; @@ -137,6 +137,36 @@ namespace Avalonia.Utilities GetList().RemoveRange(index, count); } + public int Add(object? value) + { + return ((IList)GetList()).Add(value); + } + + public bool Contains(object? value) + { + return ((IList)_list).Contains(value); + } + + public int IndexOf(object? value) + { + return ((IList)_list).IndexOf(value); + } + + public void Insert(int index, object? value) + { + ((IList)GetList()).Insert(index, value); + } + + public void Remove(object? value) + { + ((IList)GetList()).Remove(value); + } + + public void CopyTo(Array array, int index) + { + ((ICollection)_list).CopyTo(array, index); + } + public int Count => _list.Count; public bool IsReadOnly => ((ICollection)_list).IsReadOnly; @@ -144,6 +174,13 @@ namespace Avalonia.Utilities public ResetBehavior ResetBehavior { get => _list.ResetBehavior; set => _list.ResetBehavior = value; } public IAvaloniaListItemValidator? Validator { get => _list.Validator; set => _list.Validator = value; } + public bool IsFixedSize => ((IList)_list).IsFixedSize; + + public bool IsSynchronized => ((ICollection)_list).IsSynchronized; + + public object SyncRoot => ((ICollection)_list).SyncRoot; + + object? IList.this[int index] { get => ((IList)_list)[index]; set => ((IList)GetList())[index] = value; } public T this[int index] { get => _list[index]; set => GetList()[index] = value; } public struct SafeListEnumerator : IEnumerator