committed by
GitHub
7 changed files with 463 additions and 34 deletions
@ -0,0 +1,218 @@ |
|||
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<T> : IAvaloniaList<T>, IList, INotifyCollectionChanged |
|||
{ |
|||
private AvaloniaList<T> _list = new(); |
|||
private int _generation; |
|||
private int _enumCount = 0; |
|||
|
|||
public event NotifyCollectionChangedEventHandler? CollectionChanged; |
|||
public event PropertyChangedEventHandler? PropertyChanged; |
|||
|
|||
//for test purposes
|
|||
internal AvaloniaList<T> Inner => _list; |
|||
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<T> IEnumerable<T>.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() |
|||
{ |
|||
return GetEnumerator(); |
|||
} |
|||
|
|||
private AvaloniaList<T> 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<T> items) |
|||
{ |
|||
GetList().AddRange(items); |
|||
} |
|||
|
|||
public void InsertRange(int index, IEnumerable<T> items) |
|||
{ |
|||
GetList().InsertRange(index, items); |
|||
} |
|||
|
|||
public void Move(int oldIndex, int newIndex) |
|||
{ |
|||
GetList().Move(oldIndex, newIndex); |
|||
} |
|||
|
|||
public void MoveRange(int oldIndex, int count, int newIndex) |
|||
{ |
|||
GetList().MoveRange(oldIndex, count, newIndex); |
|||
} |
|||
|
|||
public void RemoveAll(IEnumerable<T> items) |
|||
{ |
|||
GetList().RemoveAll(items); |
|||
} |
|||
|
|||
public void RemoveRange(int index, int count) |
|||
{ |
|||
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<T>)_list).IsReadOnly; |
|||
|
|||
public ResetBehavior ResetBehavior { get => _list.ResetBehavior; set => _list.ResetBehavior = value; } |
|||
public IAvaloniaListItemValidator<T>? 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<T> |
|||
{ |
|||
private readonly SafeEnumerableList<T> _owner; |
|||
private readonly int _generation; |
|||
private readonly IEnumerator<T> _enumerator; |
|||
|
|||
public SafeListEnumerator(SafeEnumerableList<T> owner, AvaloniaList<T> list) |
|||
{ |
|||
_owner = owner; |
|||
_generation = owner._generation; |
|||
++owner._enumCount; |
|||
_enumerator = list.GetEnumerator(); |
|||
} |
|||
|
|||
public bool MoveNext() |
|||
=> _enumerator.MoveNext(); |
|||
|
|||
public T Current => _enumerator.Current; |
|||
|
|||
object IEnumerator.Current => _enumerator.Current!; |
|||
|
|||
public void Reset() => throw new InvalidOperationException(); |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_generation == _owner._generation) |
|||
{ |
|||
--_owner._enumCount; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
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<object>( |
|||
(i, ns) => |
|||
{ |
|||
return |
|||
new Border() |
|||
{ |
|||
BorderBrush = Brushes.Red, |
|||
BorderThickness = new Thickness(4), |
|||
Padding = new Thickness(3), |
|||
Child = new ContentControl |
|||
{ |
|||
Content = i, |
|||
} |
|||
}; |
|||
}); |
|||
|
|||
var window = new Window |
|||
{ |
|||
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<TabItem>()) |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter{ Property = TabItem.ContentTemplateProperty, |
|||
Value = contentTemplate, |
|||
} |
|||
} |
|||
} |
|||
}, |
|||
Width = 640, |
|||
Height = 480, |
|||
}; |
|||
window.Show(); |
|||
window.Close(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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<string>(); |
|||
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<string>(); |
|||
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<string> { "foo", "bar", "baz", "baz" }, target); |
|||
} |
|||
|
|||
[Fact] |
|||
public void List_Is_Not_Copied_After_Enumeration() |
|||
{ |
|||
var target = new SafeEnumerableList<string>(); |
|||
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<string>(); |
|||
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<string>(); |
|||
var initialInner = target.Inner; |
|||
var firstItems = new List<string>(); |
|||
var secondItems = new List<string>(); |
|||
Collections.AvaloniaList<string> firstInner; |
|||
Collections.AvaloniaList<string> 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<string> { "foo" }, firstItems); |
|||
Assert.Equal(new List<string> { "foo", "bar" }, secondItems); |
|||
Assert.Equal(new List<string> { "foo", "bar", "baz", "baz" }, target); |
|||
|
|||
var finalInner = target.Inner; |
|||
target.Add("final"); |
|||
Assert.Same(finalInner, target.Inner); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue