|
|
|
@ -13,24 +13,22 @@ using Avalonia.Controls.Utils; |
|
|
|
namespace Avalonia.Controls |
|
|
|
{ |
|
|
|
/// <summary>
|
|
|
|
/// Represents a standardized view of the supported interactions between a given ItemsSource
|
|
|
|
/// object and an <see cref="ItemsRepeater"/> control.
|
|
|
|
/// Represents a standardized view of the supported interactions between an items collection
|
|
|
|
/// and an items control.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// Components written to work with ItemsRepeater should consume the
|
|
|
|
/// <see cref="ItemsRepeater.Items"/> via ItemsSourceView since this provides a normalized
|
|
|
|
/// view of the Items. That way, each component does not need to know if the source is an
|
|
|
|
/// IEnumerable, an IList, or something else.
|
|
|
|
/// </remarks>
|
|
|
|
public class ItemsSourceView : INotifyCollectionChanged, IDisposable |
|
|
|
public class ItemsSourceView : IReadOnlyList<object?>, |
|
|
|
INotifyCollectionChanged, |
|
|
|
ICollectionChangedListener |
|
|
|
{ |
|
|
|
/// <summary>
|
|
|
|
/// Gets an empty <see cref="ItemsSourceView"/>
|
|
|
|
/// </summary>
|
|
|
|
public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>()); |
|
|
|
|
|
|
|
private IList? _inner; |
|
|
|
private readonly IList _inner; |
|
|
|
private NotifyCollectionChangedEventHandler? _collectionChanged; |
|
|
|
private NotifyCollectionChangedEventHandler? _postCollectionChanged; |
|
|
|
private bool _listening; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
|
|
|
|
@ -38,15 +36,15 @@ namespace Avalonia.Controls |
|
|
|
/// <param name="source">The data source.</param>
|
|
|
|
public ItemsSourceView(IEnumerable source) |
|
|
|
{ |
|
|
|
source = source ?? throw new ArgumentNullException(nameof(source)); |
|
|
|
_inner = source switch |
|
|
|
{ |
|
|
|
ItemsSourceView _ => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), |
|
|
|
ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)), |
|
|
|
IList list => list, |
|
|
|
INotifyCollectionChanged _ => throw new ArgumentException( |
|
|
|
"Collection implements INotifyCollectionChanged by not IList.", |
|
|
|
INotifyCollectionChanged => throw new ArgumentException( |
|
|
|
"Collection implements INotifyCollectionChanged but not IList.", |
|
|
|
nameof(source)), |
|
|
|
IEnumerable<object> iObj => new List<object>(iObj), |
|
|
|
null => throw new ArgumentNullException(nameof(source)), |
|
|
|
_ => new List<object>(source.Cast<object>()) |
|
|
|
}; |
|
|
|
} |
|
|
|
@ -56,26 +54,10 @@ namespace Avalonia.Controls |
|
|
|
/// </summary>
|
|
|
|
public int Count => Inner.Count; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets a value that indicates whether the items source can provide a unique key for each item.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// TODO: Not yet implemented in Avalonia.
|
|
|
|
/// </remarks>
|
|
|
|
public bool HasKeyIndexMapping => false; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the inner collection.
|
|
|
|
/// </summary>
|
|
|
|
public IList Inner |
|
|
|
{ |
|
|
|
get |
|
|
|
{ |
|
|
|
if (_inner is null) |
|
|
|
ThrowDisposed(); |
|
|
|
return _inner!; |
|
|
|
} |
|
|
|
} |
|
|
|
public IList Inner => _inner; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Retrieves the item at the specified index.
|
|
|
|
@ -84,6 +66,14 @@ namespace Avalonia.Controls |
|
|
|
/// <returns>The item.</returns>
|
|
|
|
public object? this[int index] => GetAt(index); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets a value that indicates whether the items source can provide a unique key for each item.
|
|
|
|
/// </summary>
|
|
|
|
/// <remarks>
|
|
|
|
/// TODO: Not yet implemented in Avalonia.
|
|
|
|
/// </remarks>
|
|
|
|
internal bool HasKeyIndexMapping => false; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Occurs when the collection has changed to indicate the reason for the change and which items changed.
|
|
|
|
/// </summary>
|
|
|
|
@ -91,114 +81,133 @@ namespace Avalonia.Controls |
|
|
|
{ |
|
|
|
add |
|
|
|
{ |
|
|
|
if (_collectionChanged is null && Inner is INotifyCollectionChanged incc) |
|
|
|
{ |
|
|
|
incc.CollectionChanged += OnCollectionChanged; |
|
|
|
} |
|
|
|
|
|
|
|
AddListenerIfNecessary(); |
|
|
|
_collectionChanged += value; |
|
|
|
} |
|
|
|
|
|
|
|
remove |
|
|
|
{ |
|
|
|
_collectionChanged -= value; |
|
|
|
|
|
|
|
if (_collectionChanged is null && Inner is INotifyCollectionChanged incc) |
|
|
|
{ |
|
|
|
incc.CollectionChanged -= OnCollectionChanged; |
|
|
|
} |
|
|
|
RemoveListenerIfNecessary(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <inheritdoc/>
|
|
|
|
public void Dispose() |
|
|
|
/// <summary>
|
|
|
|
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
|
|
|
|
/// event handlers have been notified.
|
|
|
|
/// </summary>
|
|
|
|
internal event NotifyCollectionChangedEventHandler? PostCollectionChanged |
|
|
|
{ |
|
|
|
if (_inner is INotifyCollectionChanged incc) |
|
|
|
add |
|
|
|
{ |
|
|
|
incc.CollectionChanged -= OnCollectionChanged; |
|
|
|
AddListenerIfNecessary(); |
|
|
|
_postCollectionChanged += value; |
|
|
|
} |
|
|
|
|
|
|
|
_inner = null; |
|
|
|
remove |
|
|
|
{ |
|
|
|
_postCollectionChanged -= value; |
|
|
|
RemoveListenerIfNecessary(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Retrieves the item at the specified index.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="index">The index.</param>
|
|
|
|
/// <returns>The item.</returns>
|
|
|
|
public object? GetAt(int index) => Inner[index]; |
|
|
|
|
|
|
|
public int IndexOf(object? item) => Inner.IndexOf(item); |
|
|
|
|
|
|
|
public static ItemsSourceView GetOrCreate(IEnumerable? items) |
|
|
|
private void AddListenerIfNecessary() |
|
|
|
{ |
|
|
|
if (items is ItemsSourceView isv) |
|
|
|
if (!_listening) |
|
|
|
{ |
|
|
|
return isv; |
|
|
|
if (_inner is INotifyCollectionChanged incc) |
|
|
|
CollectionChangedEventManager.Instance.AddListener(incc, this); |
|
|
|
_listening = true; |
|
|
|
} |
|
|
|
else if (items is null) |
|
|
|
{ |
|
|
|
return Empty; |
|
|
|
} |
|
|
|
else |
|
|
|
} |
|
|
|
|
|
|
|
private void RemoveListenerIfNecessary() |
|
|
|
{ |
|
|
|
if (_listening && _collectionChanged is null && _postCollectionChanged is null) |
|
|
|
{ |
|
|
|
return new ItemsSourceView(items); |
|
|
|
if (_inner is INotifyCollectionChanged incc) |
|
|
|
CollectionChangedEventManager.Instance.RemoveListener(incc, this); |
|
|
|
_listening = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Retrieves the index of the item that has the specified unique identifier (key).
|
|
|
|
/// Retrieves the item at the specified index.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="index">The index.</param>
|
|
|
|
/// <returns>The key</returns>
|
|
|
|
/// <remarks>
|
|
|
|
/// TODO: Not yet implemented in Avalonia.
|
|
|
|
/// </remarks>
|
|
|
|
public string KeyFromIndex(int index) |
|
|
|
{ |
|
|
|
throw new NotImplementedException(); |
|
|
|
} |
|
|
|
/// <returns>The item.</returns>
|
|
|
|
public object? GetAt(int index) => Inner[index]; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Determines the index of a specific item in the collection.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="item">The object to locate in the collection.</param>
|
|
|
|
/// <returns>The index of value if found in the list; otherwise, -1.</returns>
|
|
|
|
public int IndexOf(object? item) => Inner.IndexOf(item); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Retrieves the unique identifier (key) for the item at the specified index.
|
|
|
|
/// Gets or creates an <see cref="ItemsSourceView"/> for the specified enumerable.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="key">The key.</param>
|
|
|
|
/// <returns>The index.</returns>
|
|
|
|
/// <param name="items">The enumerable.</param>
|
|
|
|
/// <remarks>
|
|
|
|
/// TODO: Not yet implemented in Avalonia.
|
|
|
|
/// This method handles the following three cases:
|
|
|
|
/// - If <paramref name="items"/> is null, returns <see cref="Empty"/>
|
|
|
|
/// - If <paramref name="items"/> is an <see cref="ItemsSourceView"/> returns the existing
|
|
|
|
/// <see cref="ItemsSourceView"/>
|
|
|
|
/// - Otherwise creates a new <see cref="ItemsSourceView"/>
|
|
|
|
/// </remarks>
|
|
|
|
public int IndexFromKey(string key) |
|
|
|
public static ItemsSourceView GetOrCreate(IEnumerable? items) |
|
|
|
{ |
|
|
|
throw new NotImplementedException(); |
|
|
|
return items switch |
|
|
|
{ |
|
|
|
ItemsSourceView isv => isv, |
|
|
|
null => Empty, |
|
|
|
_ => new ItemsSourceView(items) |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
internal void AddListener(ICollectionChangedListener listener) |
|
|
|
public IEnumerator<object?> GetEnumerator() |
|
|
|
{ |
|
|
|
if (Inner is INotifyCollectionChanged incc) |
|
|
|
static IEnumerator<object> EnumerateItems(IList list) |
|
|
|
{ |
|
|
|
CollectionChangedEventManager.Instance.AddListener(incc, listener); |
|
|
|
foreach (var o in list) |
|
|
|
yield return o; |
|
|
|
} |
|
|
|
|
|
|
|
var inner = Inner; |
|
|
|
|
|
|
|
return inner switch |
|
|
|
{ |
|
|
|
IEnumerable<object> e => e.GetEnumerator(), |
|
|
|
_ => EnumerateItems(inner), |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
internal void RemoveListener(ICollectionChangedListener listener) |
|
|
|
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); |
|
|
|
|
|
|
|
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) |
|
|
|
{ |
|
|
|
if (Inner is INotifyCollectionChanged incc) |
|
|
|
{ |
|
|
|
CollectionChangedEventManager.Instance.RemoveListener(incc, listener); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args) |
|
|
|
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) |
|
|
|
{ |
|
|
|
_collectionChanged?.Invoke(this, args); |
|
|
|
_collectionChanged?.Invoke(this, e); |
|
|
|
} |
|
|
|
|
|
|
|
private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) |
|
|
|
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e) |
|
|
|
{ |
|
|
|
OnItemsSourceChanged(e); |
|
|
|
_postCollectionChanged?.Invoke(this, e); |
|
|
|
} |
|
|
|
|
|
|
|
private void ThrowDisposed() => throw new ObjectDisposedException(nameof(ItemsSourceView)); |
|
|
|
/// <summary>
|
|
|
|
/// Retrieves the index of the item that has the specified unique identifier (key).
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="index">The index.</param>
|
|
|
|
/// <returns>The key</returns>
|
|
|
|
/// <remarks>
|
|
|
|
/// TODO: Not yet implemented in Avalonia.
|
|
|
|
/// </remarks>
|
|
|
|
internal string KeyFromIndex(int index) => throw new NotImplementedException(); |
|
|
|
} |
|
|
|
|
|
|
|
public class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T> |
|
|
|
@ -227,9 +236,7 @@ namespace Avalonia.Controls |
|
|
|
/// </summary>
|
|
|
|
/// <param name="index">The index.</param>
|
|
|
|
/// <returns>The item.</returns>
|
|
|
|
#pragma warning disable CS8603
|
|
|
|
public new T this[int index] => GetAt(index); |
|
|
|
#pragma warning restore CS8603
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Retrieves the item at the specified index.
|
|
|
|
@ -238,23 +245,44 @@ namespace Avalonia.Controls |
|
|
|
/// <returns>The item.</returns>
|
|
|
|
public new T GetAt(int index) => (T)Inner[index]!; |
|
|
|
|
|
|
|
public IEnumerator<T> GetEnumerator() => Inner.Cast<T>().GetEnumerator(); |
|
|
|
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); |
|
|
|
|
|
|
|
public static new ItemsSourceView<T> GetOrCreate(IEnumerable? items) |
|
|
|
public new IEnumerator<T> GetEnumerator() |
|
|
|
{ |
|
|
|
if (items is ItemsSourceView<T> isv) |
|
|
|
static IEnumerator<T> EnumerateItems(IList list) |
|
|
|
{ |
|
|
|
return isv; |
|
|
|
foreach (var o in list) |
|
|
|
yield return (T)o; |
|
|
|
} |
|
|
|
else if (items is null) |
|
|
|
|
|
|
|
var inner = Inner; |
|
|
|
|
|
|
|
return inner switch |
|
|
|
{ |
|
|
|
return Empty; |
|
|
|
} |
|
|
|
else |
|
|
|
IEnumerable<T> e => e.GetEnumerator(), |
|
|
|
_ => EnumerateItems(inner), |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator(); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or creates an <see cref="ItemsSourceView{T}"/> for the specified enumerable.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="items">The enumerable.</param>
|
|
|
|
/// <remarks>
|
|
|
|
/// This method handles the following three cases:
|
|
|
|
/// - If <paramref name="items"/> is null, returns <see cref="Empty"/>
|
|
|
|
/// - If <paramref name="items"/> is an <see cref="ItemsSourceView"/> returns the existing
|
|
|
|
/// <see cref="ItemsSourceView"/>
|
|
|
|
/// - Otherwise creates a new <see cref="ItemsSourceView"/>
|
|
|
|
/// </remarks>
|
|
|
|
public static new ItemsSourceView<T> GetOrCreate(IEnumerable? items) |
|
|
|
{ |
|
|
|
return items switch |
|
|
|
{ |
|
|
|
return new ItemsSourceView<T>(items); |
|
|
|
} |
|
|
|
ItemsSourceView<T> isv => isv, |
|
|
|
null => Empty, |
|
|
|
_ => new ItemsSourceView<T>(items) |
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|