A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

309 lines
11 KiB

// This source file is adapted from the WinUI project.
// (https://github.com/microsoft/microsoft-ui-xaml)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a standardized view of the supported interactions between an items collection
/// and an items control.
/// </summary>
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 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.
/// </summary>
/// <param name="source">The data source.</param>
private protected ItemsSourceView(IEnumerable source)
{
_inner = source switch
{
ItemsSourceView => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
IList list => list,
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>())
};
}
/// <summary>
/// Gets the number of items in the collection.
/// </summary>
public int Count => Inner.Count;
/// <summary>
/// Gets the inner collection.
/// </summary>
public IList Inner => _inner;
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <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>
public event NotifyCollectionChangedEventHandler? CollectionChanged
{
add
{
AddListenerIfNecessary();
_collectionChanged += value;
}
remove
{
_collectionChanged -= value;
RemoveListenerIfNecessary();
}
}
/// <summary>
/// Occurs when a collection has finished changing and all <see cref="CollectionChanged"/>
/// event handlers have been notified.
/// </summary>
internal event NotifyCollectionChangedEventHandler? PostCollectionChanged
{
add
{
AddListenerIfNecessary();
_postCollectionChanged += value;
}
remove
{
_postCollectionChanged -= value;
RemoveListenerIfNecessary();
}
}
private void AddListenerIfNecessary()
{
if (!_listening)
{
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.AddListener(incc, this);
_listening = true;
}
}
private void RemoveListenerIfNecessary()
{
if (_listening && _collectionChanged is null && _postCollectionChanged is null)
{
if (_inner is INotifyCollectionChanged incc)
CollectionChangedEventManager.Instance.RemoveListener(incc, this);
_listening = false;
}
}
/// <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];
/// <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>
/// Gets or creates an <see cref="ItemsSourceView"/> 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 ItemsSourceView GetOrCreate(IEnumerable? items)
{
return items switch
{
ItemsSourceView isv => isv,
null => Empty,
_ => new ItemsSourceView(items)
};
}
/// <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 ItemsSourceView<T> GetOrCreate<T>(IEnumerable? items)
{
return items switch
{
ItemsSourceView<T> isv => isv,
null => ItemsSourceView<T>.Empty,
_ => new ItemsSourceView<T>(items)
};
}
/// <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 ItemsSourceView<T> GetOrCreate<T>(IEnumerable<T>? items)
{
return items switch
{
ItemsSourceView<T> isv => isv,
null => ItemsSourceView<T>.Empty,
_ => new ItemsSourceView<T>(items)
};
}
public IEnumerator<object?> GetEnumerator()
{
static IEnumerator<object> EnumerateItems(IList list)
{
foreach (var o in list)
yield return o;
}
var inner = Inner;
return inner switch
{
IEnumerable<object> e => e.GetEnumerator(),
_ => EnumerateItems(inner),
};
}
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
}
void ICollectionChangedListener.Changed(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
_collectionChanged?.Invoke(this, e);
}
void ICollectionChangedListener.PostChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
{
_postCollectionChanged?.Invoke(this, e);
}
/// <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 sealed class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
{
/// <summary>
/// Gets an empty <see cref="ItemsSourceView"/>
/// </summary>
public new static ItemsSourceView<T> Empty { get; } = new ItemsSourceView<T>(Array.Empty<T>());
/// <summary>
/// Initializes a new instance of the ItemsSourceView class for the specified data source.
/// </summary>
/// <param name="source">The data source.</param>
internal ItemsSourceView(IEnumerable<T> source)
: base(source)
{
}
internal ItemsSourceView(IEnumerable source)
: base(source)
{
}
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
public new T this[int index] => GetAt(index);
/// <summary>
/// Retrieves the item at the specified index.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item.</returns>
public new T GetAt(int index) => (T)Inner[index]!;
public new IEnumerator<T> GetEnumerator()
{
static IEnumerator<T> EnumerateItems(IList list)
{
foreach (var o in list)
yield return (T)o;
}
var inner = Inner;
return inner switch
{
IEnumerable<T> e => e.GetEnumerator(),
_ => EnumerateItems(inner),
};
}
IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
}
}