Merge branch 'master' into aleader/notifications-registry

This commit is contained in:
Andrew Leader 2020-10-04 21:16:16 -07:00 коммит произвёл GitHub
Родитель 04a7e71b60 2b610c4497
Коммит 389c6e1443
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
66 изменённых файлов: 2333 добавлений и 660 удалений

Просмотреть файл

@ -125,7 +125,7 @@ namespace Microsoft.Toolkit.HighPerformance
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator T(Box<T> box)
{
return Unsafe.Unbox<T>(box);
return (T)(object)box;
}
/// <summary>
@ -180,7 +180,6 @@ namespace Microsoft.Toolkit.HighPerformance
/// <summary>
/// Throws an <see cref="InvalidCastException"/> when a cast from an invalid <see cref="object"/> is attempted.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowInvalidCastExceptionForGetFrom()
{
throw new InvalidCastException($"Can't cast the input object to the type Box<{typeof(T)}>");

Просмотреть файл

@ -33,6 +33,11 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// </summary>
private const int DefaultInitialBufferSize = 256;
/// <summary>
/// The <see cref="ArrayPool{T}"/> instance used to rent <see cref="array"/>.
/// </summary>
private readonly ArrayPool<T> pool;
/// <summary>
/// The underlying <typeparamref name="T"/> array.
/// </summary>
@ -49,36 +54,52 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// Initializes a new instance of the <see cref="ArrayPoolBufferWriter{T}"/> class.
/// </summary>
public ArrayPoolBufferWriter()
: this(ArrayPool<T>.Shared, DefaultInitialBufferSize)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ArrayPoolBufferWriter{T}"/> class.
/// </summary>
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
public ArrayPoolBufferWriter(ArrayPool<T> pool)
: this(pool, DefaultInitialBufferSize)
{
// Since we're using pooled arrays, we can rent the buffer with the
// default size immediately, we don't need to use lazy initialization
// to save unnecessary memory allocations in this case.
this.array = ArrayPool<T>.Shared.Rent(DefaultInitialBufferSize);
this.index = 0;
}
/// <summary>
/// Initializes a new instance of the <see cref="ArrayPoolBufferWriter{T}"/> class.
/// </summary>
/// <param name="initialCapacity">The minimum capacity with which to initialize the underlying buffer.</param>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="initialCapacity"/> is not positive (i.e. less than or equal to 0).
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="initialCapacity"/> is not valid.</exception>
public ArrayPoolBufferWriter(int initialCapacity)
: this(ArrayPool<T>.Shared, initialCapacity)
{
if (initialCapacity <= 0)
{
ThrowArgumentOutOfRangeExceptionForInitialCapacity();
}
}
this.array = ArrayPool<T>.Shared.Rent(initialCapacity);
/// <summary>
/// Initializes a new instance of the <see cref="ArrayPoolBufferWriter{T}"/> class.
/// </summary>
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
/// <param name="initialCapacity">The minimum capacity with which to initialize the underlying buffer.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="initialCapacity"/> is not valid.</exception>
public ArrayPoolBufferWriter(ArrayPool<T> pool, int initialCapacity)
{
// Since we're using pooled arrays, we can rent the buffer with the
// default size immediately, we don't need to use lazy initialization
// to save unnecessary memory allocations in this case.
// Additionally, we don't need to manually throw the exception if
// the requested size is not valid, as that'll be thrown automatically
// by the array pool in use when we try to rent an array with that size.
this.pool = pool;
this.array = pool.Rent(initialCapacity);
this.index = 0;
}
/// <summary>
/// Finalizes an instance of the <see cref="ArrayPoolBufferWriter{T}"/> class.
/// </summary>
~ArrayPoolBufferWriter() => this.Dispose();
~ArrayPoolBufferWriter() => Dispose();
/// <inheritdoc/>
Memory<T> IMemoryOwner<T>.Memory
@ -182,6 +203,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
}
array.AsSpan(0, this.index).Clear();
this.index = 0;
}
@ -250,7 +272,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
{
int minimumSize = this.index + sizeHint;
ArrayPool<T>.Shared.Resize(ref this.array, minimumSize);
this.pool.Resize(ref this.array, minimumSize);
}
}
@ -268,7 +290,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
this.array = null;
ArrayPool<T>.Shared.Return(array);
this.pool.Return(array);
}
/// <inheritdoc/>
@ -286,19 +308,9 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
return $"Microsoft.Toolkit.HighPerformance.Buffers.ArrayPoolBufferWriter<{typeof(T)}>[{this.index}]";
}
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the initial capacity is invalid.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForInitialCapacity()
{
throw new ArgumentOutOfRangeException("initialCapacity", "The initial capacity must be a positive value");
}
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the requested count is negative.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForNegativeCount()
{
throw new ArgumentOutOfRangeException("count", "The count can't be a negative value");
@ -307,7 +319,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the size hint is negative.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForNegativeSizeHint()
{
throw new ArgumentOutOfRangeException("sizeHint", "The size hint can't be a negative value");
@ -316,7 +327,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the requested count is negative.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentExceptionForAdvancedTooFar()
{
throw new ArgumentException("The buffer writer has advanced too far");
@ -325,7 +335,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Throws an <see cref="ObjectDisposedException"/> when <see cref="array"/> is <see langword="null"/>.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException("The current buffer has already been disposed");

Просмотреть файл

@ -106,7 +106,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <inheritdoc/>
public Memory<T> GetMemory(int sizeHint = 0)
{
this.ValidateSizeHint(sizeHint);
ValidateSizeHint(sizeHint);
return this.memory.Slice(this.index);
}
@ -114,7 +114,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <inheritdoc/>
public Span<T> GetSpan(int sizeHint = 0)
{
this.ValidateSizeHint(sizeHint);
ValidateSizeHint(sizeHint);
return this.memory.Slice(this.index).Span;
}
@ -159,7 +159,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the requested count is negative.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForNegativeCount()
{
throw new ArgumentOutOfRangeException("count", "The count can't be a negative value");
@ -168,7 +167,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the size hint is negative.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForNegativeSizeHint()
{
throw new ArgumentOutOfRangeException("sizeHint", "The size hint can't be a negative value");
@ -177,7 +175,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the requested count is negative.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentExceptionForAdvancedTooFar()
{
throw new ArgumentException("The buffer writer has advanced too far");
@ -186,7 +183,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Throws an <see cref="ArgumentException"/> when the requested size exceeds the capacity.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentExceptionForCapacityExceeded()
{
throw new ArgumentException("The buffer writer doesn't have enough capacity left");

Просмотреть файл

@ -32,6 +32,11 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
private readonly int length;
#pragma warning restore IDE0032
/// <summary>
/// The <see cref="ArrayPool{T}"/> instance used to rent <see cref="array"/>.
/// </summary>
private readonly ArrayPool<T> pool;
/// <summary>
/// The underlying <typeparamref name="T"/> array.
/// </summary>
@ -41,12 +46,14 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// Initializes a new instance of the <see cref="MemoryOwner{T}"/> class with the specified parameters.
/// </summary>
/// <param name="length">The length of the new memory buffer to use.</param>
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
/// <param name="mode">Indicates the allocation mode to use for the new buffer to rent.</param>
private MemoryOwner(int length, AllocationMode mode)
private MemoryOwner(int length, ArrayPool<T> pool, AllocationMode mode)
{
this.start = 0;
this.length = length;
this.array = ArrayPool<T>.Shared.Rent(length);
this.pool = pool;
this.array = pool.Rent(length);
if (mode == AllocationMode.Clear)
{
@ -57,20 +64,22 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Initializes a new instance of the <see cref="MemoryOwner{T}"/> class with the specified parameters.
/// </summary>
/// <param name="array">The input <typeparamref name="T"/> array to use.</param>
/// <param name="start">The starting offset within <paramref name="array"/>.</param>
/// <param name="length">The length of the array to use.</param>
private MemoryOwner(T[] array, int start, int length)
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance currently in use.</param>
/// <param name="array">The input <typeparamref name="T"/> array to use.</param>
private MemoryOwner(int start, int length, ArrayPool<T> pool, T[] array)
{
this.start = start;
this.length = length;
this.pool = pool;
this.array = array;
}
/// <summary>
/// Finalizes an instance of the <see cref="MemoryOwner{T}"/> class.
/// </summary>
~MemoryOwner() => this.Dispose();
~MemoryOwner() => Dispose();
/// <summary>
/// Gets an empty <see cref="MemoryOwner{T}"/> instance.
@ -79,7 +88,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
public static MemoryOwner<T> Empty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new MemoryOwner<T>(0, AllocationMode.Default);
get => new MemoryOwner<T>(0, ArrayPool<T>.Shared, AllocationMode.Default);
}
/// <summary>
@ -91,7 +100,19 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MemoryOwner<T> Allocate(int size) => new MemoryOwner<T>(size, AllocationMode.Default);
public static MemoryOwner<T> Allocate(int size) => new MemoryOwner<T>(size, ArrayPool<T>.Shared, AllocationMode.Default);
/// <summary>
/// Creates a new <see cref="MemoryOwner{T}"/> instance with the specified parameters.
/// </summary>
/// <param name="size">The length of the new memory buffer to use.</param>
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance currently in use.</param>
/// <returns>A <see cref="MemoryOwner{T}"/> instance of the requested length.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="size"/> is not valid.</exception>
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MemoryOwner<T> Allocate(int size, ArrayPool<T> pool) => new MemoryOwner<T>(size, pool, AllocationMode.Default);
/// <summary>
/// Creates a new <see cref="MemoryOwner{T}"/> instance with the specified parameters.
@ -103,7 +124,20 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MemoryOwner<T> Allocate(int size, AllocationMode mode) => new MemoryOwner<T>(size, mode);
public static MemoryOwner<T> Allocate(int size, AllocationMode mode) => new MemoryOwner<T>(size, ArrayPool<T>.Shared, mode);
/// <summary>
/// Creates a new <see cref="MemoryOwner{T}"/> instance with the specified parameters.
/// </summary>
/// <param name="size">The length of the new memory buffer to use.</param>
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance currently in use.</param>
/// <param name="mode">Indicates the allocation mode to use for the new buffer to rent.</param>
/// <returns>A <see cref="MemoryOwner{T}"/> instance of the requested length.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="size"/> is not valid.</exception>
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static MemoryOwner<T> Allocate(int size, ArrayPool<T> pool, AllocationMode mode) => new MemoryOwner<T>(size, pool, mode);
/// <summary>
/// Gets the number of items in the current instance
@ -210,7 +244,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
ThrowInvalidLengthException();
}
return new MemoryOwner<T>(array!, start, length);
return new MemoryOwner<T>(start, length, this.pool, array!);
}
/// <inheritdoc/>
@ -227,7 +261,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
this.array = null;
ArrayPool<T>.Shared.Return(array);
this.pool.Return(array);
}
/// <inheritdoc/>
@ -251,7 +285,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Throws an <see cref="ObjectDisposedException"/> when <see cref="array"/> is <see langword="null"/>.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(nameof(MemoryOwner<T>), "The current buffer has already been disposed");
@ -260,7 +293,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the <see cref="start"/> is invalid.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowInvalidOffsetException()
{
throw new ArgumentOutOfRangeException(nameof(start), "The input start parameter was not valid");
@ -269,7 +301,6 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the <see cref="length"/> is invalid.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowInvalidLengthException()
{
throw new ArgumentOutOfRangeException(nameof(length), "The input length parameter was not valid");

Просмотреть файл

@ -42,6 +42,11 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
private readonly int length;
#pragma warning restore IDE0032
/// <summary>
/// The <see cref="ArrayPool{T}"/> instance used to rent <see cref="array"/>.
/// </summary>
private readonly ArrayPool<T> pool;
/// <summary>
/// The underlying <typeparamref name="T"/> array.
/// </summary>
@ -51,11 +56,13 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// Initializes a new instance of the <see cref="SpanOwner{T}"/> struct with the specified parameters.
/// </summary>
/// <param name="length">The length of the new memory buffer to use.</param>
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
/// <param name="mode">Indicates the allocation mode to use for the new buffer to rent.</param>
private SpanOwner(int length, AllocationMode mode)
private SpanOwner(int length, ArrayPool<T> pool, AllocationMode mode)
{
this.length = length;
this.array = ArrayPool<T>.Shared.Rent(length);
this.pool = pool;
this.array = pool.Rent(length);
if (mode == AllocationMode.Clear)
{
@ -70,7 +77,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
public static SpanOwner<T> Empty
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new SpanOwner<T>(0, AllocationMode.Default);
get => new SpanOwner<T>(0, ArrayPool<T>.Shared, AllocationMode.Default);
}
/// <summary>
@ -82,7 +89,19 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SpanOwner<T> Allocate(int size) => new SpanOwner<T>(size, AllocationMode.Default);
public static SpanOwner<T> Allocate(int size) => new SpanOwner<T>(size, ArrayPool<T>.Shared, AllocationMode.Default);
/// <summary>
/// Creates a new <see cref="SpanOwner{T}"/> instance with the specified parameters.
/// </summary>
/// <param name="size">The length of the new memory buffer to use.</param>
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
/// <returns>A <see cref="SpanOwner{T}"/> instance of the requested length.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="size"/> is not valid.</exception>
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SpanOwner<T> Allocate(int size, ArrayPool<T> pool) => new SpanOwner<T>(size, pool, AllocationMode.Default);
/// <summary>
/// Creates a new <see cref="SpanOwner{T}"/> instance with the specified parameters.
@ -94,7 +113,20 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SpanOwner<T> Allocate(int size, AllocationMode mode) => new SpanOwner<T>(size, mode);
public static SpanOwner<T> Allocate(int size, AllocationMode mode) => new SpanOwner<T>(size, ArrayPool<T>.Shared, mode);
/// <summary>
/// Creates a new <see cref="SpanOwner{T}"/> instance with the specified parameters.
/// </summary>
/// <param name="size">The length of the new memory buffer to use.</param>
/// <param name="pool">The <see cref="ArrayPool{T}"/> instance to use.</param>
/// <param name="mode">Indicates the allocation mode to use for the new buffer to rent.</param>
/// <returns>A <see cref="SpanOwner{T}"/> instance of the requested length.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="size"/> is not valid.</exception>
/// <remarks>This method is just a proxy for the <see langword="private"/> constructor, for clarity.</remarks>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SpanOwner<T> Allocate(int size, ArrayPool<T> pool, AllocationMode mode) => new SpanOwner<T>(size, pool, mode);
/// <summary>
/// Gets the number of items in the current instance
@ -111,7 +143,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
public Span<T> Span
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => new Span<T>(array, 0, this.length);
get => new Span<T>(this.array, 0, this.length);
}
/// <summary>
@ -122,7 +154,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ref T DangerousGetReference()
{
return ref array.DangerousGetReference();
return ref this.array.DangerousGetReference();
}
/// <summary>
@ -131,7 +163,7 @@ namespace Microsoft.Toolkit.HighPerformance.Buffers
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
ArrayPool<T>.Shared.Return(array);
this.pool.Return(this.array);
}
/// <inheritdoc/>

Просмотреть файл

@ -205,7 +205,6 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the <see cref="column"/> is invalid.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForInvalidColumn()
{
throw new ArgumentOutOfRangeException(nameof(column), "The target column parameter was not valid");

Просмотреть файл

@ -160,7 +160,6 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the <see cref="row"/> is invalid.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForInvalidRow()
{
throw new ArgumentOutOfRangeException(nameof(row), "The target row parameter was not valid");

Просмотреть файл

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -42,8 +43,9 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
/// Implements the duck-typed <see cref="IEnumerable{T}.GetEnumerator"/> method.
/// </summary>
/// <returns>An <see cref="ReadOnlySpanEnumerable{T}"/> instance targeting the current <see cref="ReadOnlySpan{T}"/> value.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpanEnumerable<T> GetEnumerator() => this;
public readonly ReadOnlySpanEnumerable<T> GetEnumerator() => this;
/// <summary>
/// Implements the duck-typed <see cref="System.Collections.IEnumerator.MoveNext"/> method.
@ -67,7 +69,7 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
/// <summary>
/// Gets the duck-typed <see cref="IEnumerator{T}.Current"/> property.
/// </summary>
public Item Current
public readonly Item Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get

Просмотреть файл

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.HighPerformance.Enumerables
@ -54,8 +55,9 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
/// Implements the duck-typed <see cref="IEnumerable{T}.GetEnumerator"/> method.
/// </summary>
/// <returns>An <see cref="ReadOnlySpanTokenizer{T}"/> instance targeting the current <see cref="ReadOnlySpan{T}"/> value.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ReadOnlySpanTokenizer<T> GetEnumerator() => this;
public readonly ReadOnlySpanTokenizer<T> GetEnumerator() => this;
/// <summary>
/// Implements the duck-typed <see cref="System.Collections.IEnumerator.MoveNext"/> method.
@ -94,7 +96,7 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
/// <summary>
/// Gets the duck-typed <see cref="IEnumerator{T}.Current"/> property.
/// </summary>
public ReadOnlySpan<T> Current
public readonly ReadOnlySpan<T> Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.span.Slice(this.start, this.end - this.start);

Просмотреть файл

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -42,8 +43,9 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
/// Implements the duck-typed <see cref="IEnumerable{T}.GetEnumerator"/> method.
/// </summary>
/// <returns>An <see cref="SpanEnumerable{T}"/> instance targeting the current <see cref="Span{T}"/> value.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SpanEnumerable<T> GetEnumerator() => this;
public readonly SpanEnumerable<T> GetEnumerator() => this;
/// <summary>
/// Implements the duck-typed <see cref="System.Collections.IEnumerator.MoveNext"/> method.
@ -67,7 +69,7 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
/// <summary>
/// Gets the duck-typed <see cref="IEnumerator{T}.Current"/> property.
/// </summary>
public Item Current
public readonly Item Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
@ -76,12 +78,12 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
ref T r0 = ref MemoryMarshal.GetReference(this.span);
ref T ri = ref Unsafe.Add(ref r0, this.index);
// On .NET Standard 2.1 we can save 4 bytes by piggybacking
// the current index in the length of the wrapped span.
// We're going to use the first item as the target reference,
// and the length as a host for the current original offset.
// This is not possible on .NET Standard 2.1 as we lack
// the API to create spans from arbitrary references.
// On .NET Standard 2.1 and .NET Core (or on any target that offers runtime
// support for the Span<T> types), we can save 4 bytes by piggybacking the
// current index in the length of the wrapped span. We're going to use the
// first item as the target reference, and the length as a host for the
// current original offset. This is not possible on eg. .NET Standard 2.0,
// as we lack the API to create Span<T>-s from arbitrary references.
return new Item(ref ri, this.index);
#else
return new Item(this.span, this.index);

Просмотреть файл

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.HighPerformance.Enumerables
@ -54,8 +55,9 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
/// Implements the duck-typed <see cref="IEnumerable{T}.GetEnumerator"/> method.
/// </summary>
/// <returns>An <see cref="SpanTokenizer{T}"/> instance targeting the current <see cref="Span{T}"/> value.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SpanTokenizer<T> GetEnumerator() => this;
public readonly SpanTokenizer<T> GetEnumerator() => this;
/// <summary>
/// Implements the duck-typed <see cref="System.Collections.IEnumerator.MoveNext"/> method.
@ -94,7 +96,7 @@ namespace Microsoft.Toolkit.HighPerformance.Enumerables
/// <summary>
/// Gets the duck-typed <see cref="IEnumerator{T}.Current"/> property.
/// </summary>
public Span<T> Current
public readonly Span<T> Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.span.Slice(this.start, this.end - this.start);

Просмотреть файл

@ -46,7 +46,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
T[] newArray = pool.Rent(newSize);
int itemsToCopy = Math.Min(array.Length, newSize);
array.AsSpan(0, itemsToCopy).CopyTo(newArray);
Array.Copy(array, 0, newArray, 0, itemsToCopy);
pool.Return(array, clearArray);

Просмотреть файл

@ -102,7 +102,6 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
/// <summary>
/// Throws an <see cref="ArgumentException"/> when trying to write too many bytes to the target writer.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentExceptionForEndOfBuffer()
{
throw new ArgumentException("The current buffer writer can't contain the requested input data.");

Просмотреть файл

@ -164,7 +164,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
/// <param name="value">The <typeparamref name="T"/> value to look for.</param>
/// <returns>The number of occurrences of <paramref name="value"/> in <paramref name="span"/>.</returns>
[Pure]
[MethodImpl(MethodImplOptions.NoInlining)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int Count<T>(this ReadOnlySpan<T> span, T value)
where T : IEquatable<T>
{

Просмотреть файл

@ -223,7 +223,6 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the given reference is out of range.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
internal static void ThrowArgumentOutOfRangeExceptionForInvalidReference()
{
throw new ArgumentOutOfRangeException("value", "The input reference does not belong to an element of the input span");

Просмотреть файл

@ -245,7 +245,6 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when <see cref="Read{T}"/> fails.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowInvalidOperationExceptionForEndOfStream()
{
throw new InvalidOperationException("The stream didn't contain enough data to read the requested item");

Просмотреть файл

@ -5,7 +5,9 @@
using System;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
#if !NETCOREAPP3_1
using System.Runtime.InteropServices;
#endif
using Microsoft.Toolkit.HighPerformance.Enumerables;
using Microsoft.Toolkit.HighPerformance.Helpers.Internals;
@ -107,7 +109,7 @@ namespace Microsoft.Toolkit.HighPerformance.Extensions
/// {
/// // Access the index and value of each item here...
/// int index = item.Index;
/// string value = item.Value;
/// char value = item.Value;
/// }
/// </code>
/// The compiler will take care of properly setting up the <see langword="foreach"/> loop with the type returned from this method.

Просмотреть файл

@ -3,7 +3,6 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.HighPerformance.Helpers
{
@ -15,7 +14,6 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when an invalid parameter is specified for the minimum actions per thread.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForInvalidMinimumActionsPerThread()
{
// Having the argument name here manually typed is
@ -30,7 +28,6 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when an invalid start parameter is specified for 1D loops.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForStartGreaterThanEnd()
{
throw new ArgumentOutOfRangeException("start", "The start parameter must be less than or equal to end");
@ -39,7 +36,6 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
/// <summary>
/// Throws an <see cref="ArgumentException"/> when a range has an index starting from an end.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentExceptionForRangeIndexFromEnd(string name)
{
throw new ArgumentException("The bounds of the range can't start from an end", name);
@ -48,7 +44,6 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when an invalid top parameter is specified for 2D loops.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForTopGreaterThanBottom()
{
throw new ArgumentOutOfRangeException("top", "The top parameter must be less than or equal to bottom");
@ -57,7 +52,6 @@ namespace Microsoft.Toolkit.HighPerformance.Helpers
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when an invalid left parameter is specified for 2D loops.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForLeftGreaterThanRight()
{
throw new ArgumentOutOfRangeException("left", "The left parameter must be less than or equal to right");

Просмотреть файл

@ -129,7 +129,6 @@ namespace Microsoft.Toolkit.HighPerformance
/// <summary>
/// Throws a <see cref="InvalidOperationException"/> when trying to access <see cref="Value"/> for a default instance.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowInvalidOperationException()
{
throw new InvalidOperationException("The current instance doesn't have a value that can be accessed");

Просмотреть файл

@ -113,7 +113,6 @@ namespace Microsoft.Toolkit.HighPerformance
/// <summary>
/// Throws a <see cref="InvalidOperationException"/> when trying to access <see cref="Value"/> for a default instance.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowInvalidOperationException()
{
throw new InvalidOperationException("The current instance doesn't have a value that can be accessed");

Просмотреть файл

@ -4,7 +4,6 @@
using System;
using System.IO;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.HighPerformance.Streams
{
@ -16,7 +15,6 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when setting the <see cref="Stream.Position"/> property.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForPosition()
{
throw new ArgumentOutOfRangeException(nameof(Position), "The value for the property was not in the valid range.");
@ -25,7 +23,6 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
/// <summary>
/// Throws an <see cref="ArgumentNullException"/> when an input buffer is <see langword="null"/>.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentNullExceptionForBuffer()
{
throw new ArgumentNullException("buffer", "The buffer is null.");
@ -34,7 +31,6 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the input count is negative.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForOffset()
{
throw new ArgumentOutOfRangeException("offset", "Offset can't be negative.");
@ -43,7 +39,6 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
/// <summary>
/// Throws an <see cref="ArgumentOutOfRangeException"/> when the input count is negative.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentOutOfRangeExceptionForCount()
{
throw new ArgumentOutOfRangeException("count", "Count can't be negative.");
@ -52,7 +47,6 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
/// <summary>
/// Throws an <see cref="ArgumentException"/> when the sum of offset and count exceeds the length of the target buffer.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentExceptionForLength()
{
throw new ArgumentException("The sum of offset and count can't be larger than the buffer length.", "buffer");
@ -61,7 +55,6 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
/// <summary>
/// Throws a <see cref="NotSupportedException"/> when trying to write on a readonly stream.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowNotSupportedExceptionForCanWrite()
{
throw new NotSupportedException("The current stream doesn't support writing.");
@ -70,7 +63,6 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
/// <summary>
/// Throws an <see cref="ArgumentException"/> when trying to write too many bytes to the target stream.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowArgumentExceptionForEndOfStreamOnWrite()
{
throw new ArgumentException("The current stream can't contain the requested input data.");
@ -79,7 +71,6 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
/// <summary>
/// Throws a <see cref="NotSupportedException"/> when trying to set the length of the stream.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowNotSupportedExceptionForSetLength()
{
throw new NotSupportedException("Setting the length is not supported for this stream.");
@ -89,7 +80,6 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
/// Throws an <see cref="ArgumentException"/> when using an invalid seek mode.
/// </summary>
/// <returns>Nothing, as this method throws unconditionally.</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
private static long ThrowArgumentExceptionForSeekOrigin()
{
throw new ArgumentException("The input seek mode is not valid.", "origin");
@ -98,7 +88,6 @@ namespace Microsoft.Toolkit.HighPerformance.Streams
/// <summary>
/// Throws an <see cref="ObjectDisposedException"/> when using a disposed <see cref="Stream"/> instance.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowObjectDisposedException()
{
throw new ObjectDisposedException(nameof(memory), "The current stream has already been disposed");

Просмотреть файл

@ -25,11 +25,11 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel
/// Initializes a new instance of the <see cref="ObservableRecipient"/> class.
/// </summary>
/// <remarks>
/// This constructor will produce an instance that will use the <see cref="Messaging.Messenger.Default"/> instance
/// This constructor will produce an instance that will use the <see cref="WeakReferenceMessenger.Default"/> instance
/// to perform requested operations. It will also be available locally through the <see cref="Messenger"/> property.
/// </remarks>
protected ObservableRecipient()
: this(Messaging.Messenger.Default)
: this(WeakReferenceMessenger.Default)
{
}
@ -78,7 +78,7 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel
/// <remarks>
/// The base implementation registers all messages for this recipients that have been declared
/// explicitly through the <see cref="IRecipient{TMessage}"/> interface, using the default channel.
/// For more details on how this works, see the <see cref="MessengerExtensions.RegisterAll"/> method.
/// For more details on how this works, see the <see cref="IMessengerExtensions.RegisterAll"/> method.
/// If you need more fine tuned control, want to register messages individually or just prefer
/// the lambda-style syntax for message registration, override this method and register manually.
/// </remarks>

Просмотреть файл

@ -7,8 +7,70 @@ using System.Diagnostics.Contracts;
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// A <see langword="delegate"/> used to represent actions to invoke when a message is received.
/// The recipient is given as an input argument to allow message registrations to avoid creating
/// closures: if an instance method on a recipient needs to be invoked it is possible to just
/// cast the recipient to the right type and then access the local method from that instance.
/// </summary>
/// <typeparam name="TRecipient">The type of recipient for the message.</typeparam>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <param name="recipient">The recipient that is receiving the message.</param>
/// <param name="message">The message being received.</param>
public delegate void MessageHandler<in TRecipient, in TMessage>(TRecipient recipient, TMessage message)
where TRecipient : class
where TMessage : class;
/// <summary>
/// An interface for a type providing the ability to exchange messages between different objects.
/// This can be useful to decouple different modules of an application without having to keep strong
/// references to types being referenced. It is also possible to send messages to specific channels, uniquely
/// identified by a token, and to have different messengers in different sections of an applications.
/// In order to use the <see cref="IMessenger"/> functionalities, first define a message type, like so:
/// <code>
/// public sealed class LoginCompletedMessage { }
/// </code>
/// Then, register your a recipient for this message:
/// <code>
/// Messenger.Default.Register&lt;MyRecipientType, LoginCompletedMessage&gt;(this, (r, m) =>
/// {
/// // Handle the message here...
/// });
/// </code>
/// The message handler here is a lambda expression taking two parameters: the recipient and the message.
/// This is done to avoid the allocations for the closures that would've been generated if the expression
/// had captured the current instance. The recipient type parameter is used so that the recipient can be
/// directly accessed within the handler without the need to manually perform type casts. This allows the
/// code to be less verbose and more reliable, as all the checks are done just at build time. If the handler
/// is defined within the same type as the recipient, it is also possible to directly access private members.
/// This allows the message handler to be a static method, which enables the C# compiler to perform a number
/// of additional memory optimizations (such as caching the delegate, avoiding unnecessary memory allocations).
/// Finally, send a message when needed, like so:
/// <code>
/// Messenger.Default.Send&lt;LoginCompletedMessage&gt;();
/// </code>
/// Additionally, the method group syntax can also be used to specify the message handler
/// to invoke when receiving a message, if a method with the right signature is available
/// in the current scope. This is helpful to keep the registration and handling logic separate.
/// Following up from the previous example, consider a class having this method:
/// <code>
/// private static void Receive(MyRecipientType recipient, LoginCompletedMessage message)
/// {
/// // Handle the message there
/// }
/// </code>
/// The registration can then be performed in a single line like so:
/// <code>
/// Messenger.Default.Register(this, Receive);
/// </code>
/// The C# compiler will automatically convert that expression to a <see cref="MessageHandler{TRecipient,TMessage}"/> instance
/// compatible with <see cref="IMessengerExtensions.Register{TRecipient,TMessage}(IMessenger,TRecipient,MessageHandler{TRecipient,TMessage})"/>.
/// This will also work if multiple overloads of that method are available, each handling a different
/// message type: the C# compiler will automatically pick the right one for the current message type.
/// It is also possible to register message handlers explicitly using the <see cref="IRecipient{TMessage}"/> interface.
/// To do so, the recipient just needs to implement the interface and then call the
/// <see cref="IMessengerExtensions.RegisterAll(IMessenger,object)"/> extension, which will automatically register
/// all the handlers that are declared by the recipient type. Registration for individual handlers is supported as well.
/// </summary>
public interface IMessenger
{
@ -28,13 +90,15 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
/// <summary>
/// Registers a recipient for a given type of message.
/// </summary>
/// <typeparam name="TRecipient">The type of recipient for the message.</typeparam>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <param name="token">A token used to determine the receiving channel to use.</param>
/// <param name="action">The <see cref="Action{T}"/> to invoke when a message is received.</param>
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
void Register<TMessage, TToken>(object recipient, TToken token, Action<TMessage> action)
void Register<TRecipient, TMessage, TToken>(TRecipient recipient, TToken token, MessageHandler<TRecipient, TMessage> handler)
where TRecipient : class
where TMessage : class
where TToken : IEquatable<TToken>;
@ -83,6 +147,14 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
where TMessage : class
where TToken : IEquatable<TToken>;
/// <summary>
/// Performs a cleanup on the current messenger.
/// Invoking this method does not unregister any of the currently registered
/// recipient, and it can be used to perform cleanup operations such as
/// trimming the internal data structures of a messenger implementation.
/// </summary>
void Cleanup();
/// <summary>
/// Resets the <see cref="IMessenger"/> instance and unregisters all the existing recipients.
/// </summary>

Просмотреть файл

@ -8,19 +8,20 @@ using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.Toolkit.Mvvm.Messaging.Internals;
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// Extensions for the <see cref="IMessenger"/> type.
/// </summary>
public static partial class MessengerExtensions
public static class IMessengerExtensions
{
/// <summary>
/// A class that acts as a container to load the <see cref="MethodInfo"/> instance linked to
/// the <see cref="Register{TMessage,TToken}(IMessenger,IRecipient{TMessage},TToken)"/> method.
/// This class is needed to avoid forcing the initialization code in the static constructor to run as soon as
/// the <see cref="MessengerExtensions"/> type is referenced, even if that is done just to use methods
/// the <see cref="IMessengerExtensions"/> type is referenced, even if that is done just to use methods
/// that do not actually require this <see cref="MethodInfo"/> instance to be available.
/// We're effectively using this type to leverage the lazy loading of static constructors done by the runtime.
/// </summary>
@ -32,7 +33,7 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
static MethodInfos()
{
RegisterIRecipient = (
from methodInfo in typeof(MessengerExtensions).GetMethods()
from methodInfo in typeof(IMessengerExtensions).GetMethods()
where methodInfo.Name == nameof(Register) &&
methodInfo.IsGenericMethod &&
methodInfo.GetGenericArguments().Length == 2
@ -174,7 +175,7 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
public static void Register<TMessage>(this IMessenger messenger, IRecipient<TMessage> recipient)
where TMessage : class
{
messenger.Register<TMessage, Unit>(recipient, default, recipient.Receive);
messenger.Register<IRecipient<TMessage>, TMessage, Unit>(recipient, default, (r, m) => r.Receive(m));
}
/// <summary>
@ -191,7 +192,7 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
where TMessage : class
where TToken : IEquatable<TToken>
{
messenger.Register<TMessage, TToken>(recipient, token, recipient.Receive);
messenger.Register<IRecipient<TMessage>, TMessage, TToken>(recipient, token, (r, m) => r.Receive(m));
}
/// <summary>
@ -200,13 +201,47 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <param name="action">The <see cref="Action{T}"/> to invoke when a message is received.</param>
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
public static void Register<TMessage>(this IMessenger messenger, object recipient, Action<TMessage> action)
public static void Register<TMessage>(this IMessenger messenger, object recipient, MessageHandler<object, TMessage> handler)
where TMessage : class
{
messenger.Register(recipient, default(Unit), action);
messenger.Register(recipient, default(Unit), handler);
}
/// <summary>
/// Registers a recipient for a given type of message.
/// </summary>
/// <typeparam name="TRecipient">The type of recipient for the message.</typeparam>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
/// <remarks>This method will use the default channel to perform the requested registration.</remarks>
public static void Register<TRecipient, TMessage>(this IMessenger messenger, TRecipient recipient, MessageHandler<TRecipient, TMessage> handler)
where TRecipient : class
where TMessage : class
{
messenger.Register(recipient, default(Unit), handler);
}
/// <summary>
/// Registers a recipient for a given type of message.
/// </summary>
/// <typeparam name="TMessage">The type of message to receive.</typeparam>
/// <typeparam name="TToken">The type of token to use to pick the messages to receive.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to register the recipient.</param>
/// <param name="recipient">The recipient that will receive the messages.</param>
/// <param name="token">A token used to determine the receiving channel to use.</param>
/// <param name="handler">The <see cref="MessageHandler{TRecipient,TMessage}"/> to invoke when a message is received.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to register the same message twice.</exception>
public static void Register<TMessage, TToken>(this IMessenger messenger, object recipient, TToken token, MessageHandler<object, TMessage> handler)
where TMessage : class
where TToken : IEquatable<TToken>
{
messenger.Register(recipient, token, handler);
}
/// <summary>

Просмотреть файл

@ -130,7 +130,11 @@ namespace Microsoft.Collections.Extensions
this.entries = InitialEntries;
}
/// <inheritdoc cref="Dictionary{TKey,TValue}.ContainsKey"/>
/// <summary>
/// Checks whether or not the dictionary contains a pair with a specified key.
/// </summary>
/// <param name="key">The key to look for.</param>
/// <returns>Whether or not the key was present in the dictionary.</returns>
public bool ContainsKey(TKey key)
{
Entry[] entries = this.entries;
@ -176,7 +180,18 @@ namespace Microsoft.Collections.Extensions
}
/// <inheritdoc/>
public bool TryRemove(TKey key, out object? result)
public bool TryRemove(TKey key)
{
return TryRemove(key, out _);
}
/// <summary>
/// Tries to remove a value with a specified key, if present.
/// </summary>
/// <param name="key">The key of the value to remove.</param>
/// <param name="result">The removed value, if it was present.</param>
/// <returns>Whether or not the key was present.</returns>
public bool TryRemove(TKey key, out TValue? result)
{
Entry[] entries = this.entries;
int bucketIndex = key.GetHashCode() & (this.buckets.Length - 1);
@ -218,13 +233,6 @@ namespace Microsoft.Collections.Extensions
return false;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Remove(TKey key)
{
return TryRemove(key, out _);
}
/// <summary>
/// Gets the value for the specified key, or, if the key is not present,
/// adds an entry and returns the value by ref. This makes it possible to

Просмотреть файл

@ -14,18 +14,10 @@ namespace Microsoft.Collections.Extensions
where TKey : IEquatable<TKey>
{
/// <summary>
/// Tries to remove a value with a specified key.
/// Tries to remove a value with a specified key, if present.
/// </summary>
/// <param name="key">The key of the value to remove.</param>
/// <param name="result">The removed value, if it was present.</param>
/// <returns>.Whether or not the key was present.</returns>
bool TryRemove(TKey key, out object? result);
/// <summary>
/// Removes an item from the dictionary with the specified key, if present.
/// </summary>
/// <param name="key">The key of the item to remove.</param>
/// <returns>Whether or not an item was removed.</returns>
bool Remove(TKey key);
/// <returns>Whether or not the key was present.</returns>
bool TryRemove(TKey key);
}
}

Просмотреть файл

@ -0,0 +1,83 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.Mvvm.Messaging.Internals
{
/// <summary>
/// A simple type representing an immutable pair of types.
/// </summary>
/// <remarks>
/// This type replaces a simple <see cref="ValueTuple{T1,T2}"/> as it's faster in its
/// <see cref="GetHashCode"/> and <see cref="IEquatable{T}.Equals(T)"/> methods, and because
/// unlike a value tuple it exposes its fields as immutable. Additionally, the
/// <see cref="TMessage"/> and <see cref="TToken"/> fields provide additional clarity reading
/// the code compared to <see cref="ValueTuple{T1,T2}.Item1"/> and <see cref="ValueTuple{T1,T2}.Item2"/>.
/// </remarks>
internal readonly struct Type2 : IEquatable<Type2>
{
/// <summary>
/// The type of registered message.
/// </summary>
public readonly Type TMessage;
/// <summary>
/// The type of registration token.
/// </summary>
public readonly Type TToken;
/// <summary>
/// Initializes a new instance of the <see cref="Type2"/> struct.
/// </summary>
/// <param name="tMessage">The type of registered message.</param>
/// <param name="tToken">The type of registration token.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Type2(Type tMessage, Type tToken)
{
TMessage = tMessage;
TToken = tToken;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Type2 other)
{
// We can't just use reference equality, as that's technically not guaranteed
// to work and might fail in very rare cases (eg. with type forwarding between
// different assemblies). Instead, we can use the == operator to compare for
// equality, which still avoids the callvirt overhead of calling Type.Equals,
// and is also implemented as a JIT intrinsic on runtimes such as .NET Core.
return
TMessage == other.TMessage &&
TToken == other.TToken;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Type2 other && Equals(other);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
unchecked
{
// To combine the two hashes, we can simply use the fast djb2 hash algorithm.
// This is not a problem in this case since we already know that the base
// RuntimeHelpers.GetHashCode method is providing hashes with a good enough distribution.
int hash = RuntimeHelpers.GetHashCode(TMessage);
hash = (hash << 5) + hash;
hash += RuntimeHelpers.GetHashCode(TToken);
return hash;
}
}
}
}

Просмотреть файл

@ -0,0 +1,35 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.Mvvm.Messaging.Internals
{
/// <summary>
/// An empty type representing a generic token with no specific value.
/// </summary>
internal readonly struct Unit : IEquatable<Unit>
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Unit other)
{
return true;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Unit;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
return 0;
}
}
}

Просмотреть файл

@ -1,41 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Runtime.CompilerServices;
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// Extensions for the <see cref="IMessenger"/> type.
/// </summary>
public static partial class MessengerExtensions
{
/// <summary>
/// An empty type representing a generic token with no specific value.
/// </summary>
private readonly struct Unit : IEquatable<Unit>
{
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Unit other)
{
return true;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Unit;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
return 0;
}
}
}
}

Просмотреть файл

@ -8,71 +8,46 @@ using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Collections.Extensions;
using Microsoft.Toolkit.Mvvm.Messaging.Internals;
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// A type that can be used to exchange messages between different objects.
/// This can be useful to decouple different modules of an application without having to keep strong
/// references to types being referenced. It is also possible to send messages to specific channels, uniquely
/// identified by a token, and to have different messengers in different sections of an applications.
/// In order to use the <see cref="IMessenger"/> functionalities, first define a message type, like so:
/// <code>
/// public sealed class LoginCompletedMessage { }
/// </code>
/// Then, register your a recipient for this message:
/// <code>
/// Messenger.Default.Register&lt;LoginCompletedMessage&gt;(this, m =>
/// {
/// // Handle the message here...
/// });
/// </code>
/// Finally, send a message when needed, like so:
/// <code>
/// Messenger.Default.Send&lt;LoginCompletedMessage&gt;();
/// </code>
/// Additionally, the method group syntax can also be used to specify the action
/// to invoke when receiving a message, if a method with the right signature is available
/// in the current scope. This is helpful to keep the registration and handling logic separate.
/// Following up from the previous example, consider a class having this method:
/// <code>
/// private void Receive(LoginCompletedMessage message)
/// {
/// // Handle the message there
/// }
/// </code>
/// The registration can then be performed in a single line like so:
/// <code>
/// Messenger.Default.Register&lt;LoginCompletedMessage&gt;(this, Receive);
/// </code>
/// The C# compiler will automatically convert that expression to an <see cref="Action{T}"/> instance
/// compatible with the <see cref="MessengerExtensions.Register{T}(IMessenger,object,Action{T})"/> method.
/// This will also work if multiple overloads of that method are available, each handling a different
/// message type: the C# compiler will automatically pick the right one for the current message type.
/// For info on the other available features, check the <see cref="IMessenger"/> interface.
/// A class providing a reference implementation for the <see cref="IMessenger"/> interface.
/// </summary>
public sealed class Messenger : IMessenger
/// <remarks>
/// This <see cref="IMessenger"/> implementation uses strong references to track the registered
/// recipients, so it is necessary to manually unregister them when they're no longer needed.
/// </remarks>
public sealed class StrongReferenceMessenger : IMessenger
{
// The Messenger class uses the following logic to link stored instances together:
// This messenger uses the following logic to link stored instances together:
// --------------------------------------------------------------------------------------------------------
// DictionarySlim<Recipient, HashSet<IMapping>> recipientsMap;
// | \________________[*]IDictionarySlim<Recipient, IDictionarySlim<TToken>>
// | \___ / / /
// | ________(recipients registrations)___________\________/ / __/
// | / _______(channel registrations)_____\___________________/ /
// | / / \ /
// DictionarySlim<Recipient, DictionarySlim<TToken, Action<TMessage>>> mapping = Mapping<TMessage, TToken>
// / / \ / /
// ___(Type2.tToken)____/ / \______/___________________/
// /________________(Type2.tMessage)____/ /
// / ________________________________________/
// | \____________/_________ /
// | ________(recipients registrations)____________________/ \ /
// | / ____(channel registrations)________________\____________/
// | / / \
// DictionarySlim<Recipient, DictionarySlim<TToken, MessageHandler<TRecipient, TMessage>>> mapping = Mapping<TMessage, TToken>
// / / /
// ___(Type2.TToken)____/ / /
// /________________(Type2.TMessage)________________________/ /
// / ____________________________________________________________/
// / /
// DictionarySlim<Type2, IMapping> typesMap;
// --------------------------------------------------------------------------------------------------------
// Each combination of <TMessage, TToken> results in a concrete Mapping<TMessage, TToken> type, which holds
// the references from registered recipients to handlers. The handlers are stored in a <TToken, Action<TMessage>>
// dictionary, so that each recipient can have up to one registered handler for a given token, for each
// message type. Each mapping is stored in the types map, which associates each pair of concrete types to its
// Each combination of <TMessage, TToken> results in a concrete Mapping<TMessage, TToken> type, which holds the references
// from registered recipients to handlers. The handlers are stored in a <TToken, MessageHandler<object, TMessage>> dictionary,
// so that each recipient can have up to one registered handler for a given token, for each message type.
// Note that the registered handlers are only stored as object references, even if they were actually of type
// MessageHandler<TRecipient, TMessage>, to avoid unnecessary unsafe casts. Each handler is also generic with respect to the
// recipient type, in order to allow the messenger to track and invoke type-specific handlers without using reflection and
// without having to capture the input handler in a proxy delegate, causing one extra memory allocations and adding overhead.
// This allows users to retain type information on each registered recipient, instead of having to manually cast each recipient
// to the right type within the handler. The type conversion is guaranteed to be respected due to how the messenger type
// itself works - as registered handlers are always invoked on their respective recipients.
// Each mapping is stored in the types map, which associates each pair of concrete types to its
// mapping instance. Mapping instances are exposed as IMapping items, as each will be a closed type over
// a different combination of TMessage and TToken generic type parameters. Each existing recipient is also stored in
// the main recipients map, along with a set of all the existing dictionaries of handlers for that recipient (for all
@ -109,9 +84,9 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
private readonly DictionarySlim<Type2, IMapping> typesMap = new DictionarySlim<Type2, IMapping>();
/// <summary>
/// Gets the default <see cref="Messenger"/> instance.
/// Gets the default <see cref="StrongReferenceMessenger"/> instance.
/// </summary>
public static Messenger Default { get; } = new Messenger();
public static StrongReferenceMessenger Default { get; } = new StrongReferenceMessenger();
/// <inheritdoc/>
public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
@ -132,7 +107,8 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
}
/// <inheritdoc/>
public void Register<TMessage, TToken>(object recipient, TToken token, Action<TMessage> action)
public void Register<TRecipient, TMessage, TToken>(TRecipient recipient, TToken token, MessageHandler<TRecipient, TMessage> handler)
where TRecipient : class
where TMessage : class
where TToken : IEquatable<TToken>
{
@ -141,22 +117,20 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
// Get the <TMessage, TToken> registration list for this recipient
Mapping<TMessage, TToken> mapping = GetOrAddMapping<TMessage, TToken>();
var key = new Recipient(recipient);
ref DictionarySlim<TToken, Action<TMessage>>? map = ref mapping.GetOrAddValueRef(key);
ref DictionarySlim<TToken, object>? map = ref mapping.GetOrAddValueRef(key);
map ??= new DictionarySlim<TToken, Action<TMessage>>();
map ??= new DictionarySlim<TToken, object>();
// Add the new registration entry
ref Action<TMessage>? handler = ref map.GetOrAddValueRef(token);
ref object? registeredHandler = ref map.GetOrAddValueRef(token);
if (!(handler is null))
if (!(registeredHandler is null))
{
ThrowInvalidOperationExceptionForDuplicateRegistration();
}
handler = action;
// Update the total counter for handlers for the current type parameters
mapping.TotalHandlersCount++;
// Treat the input delegate as if it was covariant (see comments below in the Send method)
registeredHandler = handler;
// Make sure this registration map is tracked for the current recipient
ref HashSet<IMapping>? set = ref this.recipientsMap.GetOrAddValueRef(key);
@ -183,37 +157,23 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
// Removes all the lists of registered handlers for the recipient
foreach (IMapping mapping in set!)
{
if (mapping.TryRemove(key, out object? handlersMap))
if (mapping.TryRemove(key) &&
mapping.Count == 0)
{
// If this branch is taken, it means the target recipient to unregister
// had at least one registered handler for the current <TToken, TMessage>
// pair of type parameters, which here is masked out by the IMapping interface.
// Before removing the handlers, we need to retrieve the count of how many handlers
// are being removed, in order to update the total counter for the mapping.
// Just casting the dictionary to the base interface and accessing the Count
// property directly gives us O(1) access time to retrieve this count.
// The handlers map is the IDictionary<TToken, TMessage> instance for the mapping.
int handlersCount = Unsafe.As<IDictionarySlim>(handlersMap).Count;
mapping.TotalHandlersCount -= handlersCount;
if (mapping.Count == 0)
{
// Maps here are really of type Mapping<,> and with unknown type arguments.
// If after removing the current recipient a given map becomes empty, it means
// that there are no registered recipients at all for a given pair of message
// and token types. In that case, we also remove the map from the types map.
// The reason for keeping a key in each mapping is that removing items from a
// dictionary (a hashed collection) only costs O(1) in the best case, while
// if we had tried to iterate the whole dictionary every time we would have
// paid an O(n) minimum cost for each single remove operation.
this.typesMap.Remove(mapping.TypeArguments);
}
// Maps here are really of type Mapping<,> and with unknown type arguments.
// If after removing the current recipient a given map becomes empty, it means
// that there are no registered recipients at all for a given pair of message
// and token types. In that case, we also remove the map from the types map.
// The reason for keeping a key in each mapping is that removing items from a
// dictionary (a hashed collection) only costs O(1) in the best case, while
// if we had tried to iterate the whole dictionary every time we would have
// paid an O(n) minimum cost for each single remove operation.
this.typesMap.TryRemove(mapping.TypeArguments, out _);
}
}
// Remove the associated set in the recipients map
this.recipientsMap.Remove(key);
this.recipientsMap.TryRemove(key, out _);
}
}
@ -222,7 +182,7 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
where TToken : IEquatable<TToken>
{
bool lockTaken = false;
IDictionarySlim<Recipient, IDictionarySlim<TToken>>[]? maps = null;
object[]? maps = null;
int i = 0;
// We use an explicit try/finally block here instead of the lock syntax so that we can use a single
@ -243,11 +203,16 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
return;
}
// Copy the candidate mappings for the target recipient to a local
// array, as we can't modify the contents of the set while iterating it.
// The rented buffer is oversized and will also include mappings for
// handlers of messages that are registered through a different token.
maps = ArrayPool<IDictionarySlim<Recipient, IDictionarySlim<TToken>>>.Shared.Rent(set!.Count);
// Copy the candidate mappings for the target recipient to a local array, as we can't modify the
// contents of the set while iterating it. The rented buffer is oversized and will also include
// mappings for handlers of messages that are registered through a different token. Note that
// we're using just an object array to minimize the number of total rented buffers, that would
// just remain in the shared pool unused, other than when they are rented here. Instead, we're
// using a type that would possibly also be used by the users of the library, which increases
// the opportunities to reuse existing buffers for both. When we need to reference an item
// stored in the buffer with the type we know it will have, we use Unsafe.As<T> to avoid the
// expensive type check in the cast, since we already know the assignment will be valid.
maps = ArrayPool<object>.Shared.Rent(set!.Count);
foreach (IMapping item in set)
{
@ -265,8 +230,10 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
// without having to know the concrete type in advance, and without having
// to deal with reflection: we can just check if the type of the closed interface
// matches with the token type currently in use, and operate on those instances.
foreach (IDictionarySlim<Recipient, IDictionarySlim<TToken>> map in maps.AsSpan(0, i))
foreach (object obj in maps.AsSpan(0, i))
{
var map = Unsafe.As<IDictionarySlim<Recipient, IDictionarySlim<TToken>>>(obj);
// We don't need whether or not the map contains the recipient, as the
// sequence of maps has already been copied from the set containing all
// the mappings for the target recipients: it is guaranteed to be here.
@ -274,36 +241,22 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
// Try to remove the registered handler for the input token,
// for the current message type (unknown from here).
if (holder.Remove(token))
if (holder.TryRemove(token) &&
holder.Count == 0)
{
// As above, we need to update the total number of registered handlers for the map.
// In this case we also know that the current TToken type parameter is of interest
// for the current method, as we're only unsubscribing handlers using that token.
// This is because we're already working on the final <TToken, TMessage> mapping,
// which associates a single handler with a given token, for a given recipient.
// This means that we don't have to retrieve the count to subtract in this case,
// we're just removing a single handler at a time. So, we just decrement the total.
Unsafe.As<IMapping>(map).TotalHandlersCount--;
// If the map is empty, remove the recipient entirely from its container
map.TryRemove(key);
if (holder.Count == 0)
// If no handlers are left at all for the recipient, across all
// message types and token types, remove the set of mappings
// entirely for the current recipient, and lost the strong
// reference to it as well. This is the same situation that
// would've been achieved by just calling UnregisterAll(recipient).
if (map.Count == 0 &&
set.Remove(Unsafe.As<IMapping>(map)) &&
set.Count == 0)
{
// If the map is empty, remove the recipient entirely from its container
map.Remove(key);
if (map.Count == 0)
{
// If no handlers are left at all for the recipient, across all
// message types and token types, remove the set of mappings
// entirely for the current recipient, and lost the strong
// reference to it as well. This is the same situation that
// would've been achieved by just calling UnregisterAll(recipient).
set.Remove(Unsafe.As<IMapping>(map));
if (set.Count == 0)
{
this.recipientsMap.Remove(key);
}
}
this.recipientsMap.TryRemove(key, out _);
}
}
}
@ -324,7 +277,7 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
{
maps.AsSpan(0, i).Clear();
ArrayPool<IDictionarySlim<Recipient, IDictionarySlim<TToken>>>.Shared.Return(maps);
ArrayPool<object>.Shared.Return(maps);
}
}
}
@ -344,84 +297,92 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
var key = new Recipient(recipient);
if (!mapping!.TryGetValue(key, out DictionarySlim<TToken, Action<TMessage>>? dictionary))
if (!mapping!.TryGetValue(key, out DictionarySlim<TToken, object>? dictionary))
{
return;
}
// Remove the target handler
if (dictionary!.Remove(token))
if (dictionary!.TryRemove(token, out _) &&
dictionary.Count == 0)
{
// Decrement the total count, as above
mapping.TotalHandlersCount--;
// If the map is empty, it means that the current recipient has no remaining
// registered handlers for the current <TMessage, TToken> combination, regardless,
// of the specific token value (ie. the channel used to receive messages of that type).
// We can remove the map entirely from this container, and remove the link to the map itself
// to the current mapping between existing registered recipients (or entire recipients too).
if (dictionary.Count == 0)
mapping.TryRemove(key, out _);
HashSet<IMapping> set = this.recipientsMap[key];
if (set.Remove(mapping) &&
set.Count == 0)
{
mapping.Remove(key);
HashSet<IMapping> set = this.recipientsMap[key];
set.Remove(mapping);
if (set.Count == 0)
{
this.recipientsMap.Remove(key);
}
this.recipientsMap.TryRemove(key, out _);
}
}
}
}
/// <inheritdoc/>
public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
public unsafe TMessage Send<TMessage, TToken>(TMessage message, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
Action<TMessage>[] entries;
object[] handlers;
object[] recipients;
ref object handlersRef = ref Unsafe.AsRef<object>(null);
ref object recipientsRef = ref Unsafe.AsRef<object>(null);
int i = 0;
lock (this.recipientsMap)
{
// Check whether there are any registered recipients
if (!TryGetMapping(out Mapping<TMessage, TToken>? mapping))
_ = TryGetMapping(out Mapping<TMessage, TToken>? mapping);
// We need to make a local copy of the currently registered handlers, since users might
// try to unregister (or register) new handlers from inside one of the currently existing
// handlers. We can use memory pooling to reuse arrays, to minimize the average memory
// usage. In practice, we usually just need to pay the small overhead of copying the items.
// The current mapping contains all the currently registered recipients and handlers for
// the <TMessage, TToken> combination in use. In the worst case scenario, all recipients
// will have a registered handler with a token matching the input one, meaning that we could
// have at worst a number of pending handlers to invoke equal to the total number of recipient
// in the mapping. This relies on the fact that tokens are unique, and that there is only
// one handler associated with a given token. We can use this upper bound as the requested
// size for each array rented from the pool, which guarantees that we'll have enough space.
int totalHandlersCount = mapping?.Count ?? 0;
if (totalHandlersCount == 0)
{
return message;
}
// We need to make a local copy of the currently registered handlers,
// since users might try to unregister (or register) new handlers from
// inside one of the currently existing handlers. We can use memory pooling
// to reuse arrays, to minimize the average memory usage. In practice,
// we usually just need to pay the small overhead of copying the items.
entries = ArrayPool<Action<TMessage>>.Shared.Rent(mapping!.TotalHandlersCount);
handlers = ArrayPool<object>.Shared.Rent(totalHandlersCount);
recipients = ArrayPool<object>.Shared.Rent(totalHandlersCount);
handlersRef = ref handlers[0];
recipientsRef = ref recipients[0];
// Copy the handlers to the local collection.
// Both types being enumerate expose a struct enumerator,
// so we're not actually allocating the enumerator here.
// The array is oversized at this point, since it also includes
// handlers for different tokens. We can reuse the same variable
// to count the number of matching handlers to invoke later on.
// This will be the array slice with valid actions in the rented buffer.
var mappingEnumerator = mapping.GetEnumerator();
// This will be the array slice with valid handler in the rented buffer.
var mappingEnumerator = mapping!.GetEnumerator();
// Explicit enumerator usage here as we're using a custom one
// that doesn't expose the single standard Current property.
while (mappingEnumerator.MoveNext())
{
var pairsEnumerator = mappingEnumerator.Value.GetEnumerator();
object recipient = mappingEnumerator.Key.Target;
while (pairsEnumerator.MoveNext())
// Pick the target handler, if the token is a match for the recipient
if (mappingEnumerator.Value.TryGetValue(token, out object? handler))
{
// Only select the ones with a matching token
if (pairsEnumerator.Key.Equals(token))
{
entries[i++] = pairsEnumerator.Value;
}
// We can manually offset here to skip the bounds checks in this inner loop when
// indexing the array (the size is already verified and guaranteed to be enough).
Unsafe.Add(ref handlersRef, (IntPtr)(void*)(uint)i) = handler!;
Unsafe.Add(ref recipientsRef, (IntPtr)(void*)(uint)i++) = recipient;
}
}
}
@ -429,23 +390,44 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
try
{
// Invoke all the necessary handlers on the local copy of entries
foreach (var entry in entries.AsSpan(0, i))
for (int j = 0; j < i; j++)
{
entry(message);
// We're doing an unsafe cast to skip the type checks again.
// See the comments in the UnregisterAll method for more info.
object handler = Unsafe.Add(ref handlersRef, (IntPtr)(void*)(uint)j);
object recipient = Unsafe.Add(ref recipientsRef, (IntPtr)(void*)(uint)j);
// Here we perform an unsafe cast to enable covariance for delegate types.
// We know that the input recipient will always respect the type constraints
// of each original input delegate, and doing so allows us to still invoke
// them all from here without worrying about specific generic type arguments.
Unsafe.As<MessageHandler<object, TMessage>>(handler)(recipient, message);
}
}
finally
{
// As before, we also need to clear it first to avoid having potentially long
// lasting memory leaks due to leftover references being stored in the pool.
entries.AsSpan(0, i).Clear();
handlers.AsSpan(0, i).Clear();
recipients.AsSpan(0, i).Clear();
ArrayPool<Action<TMessage>>.Shared.Return(entries);
ArrayPool<object>.Shared.Return(handlers);
ArrayPool<object>.Shared.Return(recipients);
}
return message;
}
/// <inheritdoc/>
void IMessenger.Cleanup()
{
// The current implementation doesn't require any kind of cleanup operation, as
// all the internal data structures are already kept in sync whenever a recipient
// is added or removed. This method is implemented through an explicit interface
// implementation so that developers using this type directly will not see it in
// the API surface (as it wouldn't be useful anyway, since it's a no-op here).
}
/// <inheritdoc/>
public void Reset()
{
@ -515,7 +497,7 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
/// This type is defined for simplicity and as a workaround for the lack of support for using type aliases
/// over open generic types in C# (using type aliases can only be used for concrete, closed types).
/// </remarks>
private sealed class Mapping<TMessage, TToken> : DictionarySlim<Recipient, DictionarySlim<TToken, Action<TMessage>>>, IMapping
private sealed class Mapping<TMessage, TToken> : DictionarySlim<Recipient, DictionarySlim<TToken, object>>, IMapping
where TMessage : class
where TToken : IEquatable<TToken>
{
@ -529,9 +511,6 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
/// <inheritdoc/>
public Type2 TypeArguments { get; }
/// <inheritdoc/>
public int TotalHandlersCount { get; set; }
}
/// <summary>
@ -544,11 +523,6 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
/// Gets the <see cref="Type2"/> instance representing the current type arguments.
/// </summary>
Type2 TypeArguments { get; }
/// <summary>
/// Gets or sets the total number of handlers in the current instance.
/// </summary>
int TotalHandlersCount { get; set; }
}
/// <summary>
@ -567,7 +541,7 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
/// <summary>
/// The registered recipient.
/// </summary>
private readonly object target;
public readonly object Target;
/// <summary>
/// Initializes a new instance of the <see cref="Recipient"/> struct.
@ -576,14 +550,14 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Recipient(object target)
{
this.target = target;
Target = target;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Recipient other)
{
return ReferenceEquals(this.target, other.target);
return ReferenceEquals(Target, other.Target);
}
/// <inheritdoc/>
@ -596,81 +570,7 @@ namespace Microsoft.Toolkit.Mvvm.Messaging
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
return RuntimeHelpers.GetHashCode(this.target);
}
}
/// <summary>
/// A simple type representing an immutable pair of types.
/// </summary>
/// <remarks>
/// This type replaces a simple <see cref="ValueTuple{T1,T2}"/> as it's faster in its
/// <see cref="GetHashCode"/> and <see cref="IEquatable{T}.Equals(T)"/> methods, and because
/// unlike a value tuple it exposes its fields as immutable. Additionally, the
/// <see cref="tMessage"/> and <see cref="tToken"/> fields provide additional clarity reading
/// the code compared to <see cref="ValueTuple{T1,T2}.Item1"/> and <see cref="ValueTuple{T1,T2}.Item2"/>.
/// </remarks>
private readonly struct Type2 : IEquatable<Type2>
{
/// <summary>
/// The type of registered message.
/// </summary>
private readonly Type tMessage;
/// <summary>
/// The type of registration token.
/// </summary>
private readonly Type tToken;
/// <summary>
/// Initializes a new instance of the <see cref="Type2"/> struct.
/// </summary>
/// <param name="tMessage">The type of registered message.</param>
/// <param name="tToken">The type of registration token.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Type2(Type tMessage, Type tToken)
{
this.tMessage = tMessage;
this.tToken = tToken;
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Equals(Type2 other)
{
// We can't just use reference equality, as that's technically not guaranteed
// to work and might fail in very rare cases (eg. with type forwarding between
// different assemblies). Instead, we can use the == operator to compare for
// equality, which still avoids the callvirt overhead of calling Type.Equals,
// and is also implemented as a JIT intrinsic on runtimes such as .NET Core.
return
this.tMessage == other.tMessage &&
this.tToken == other.tToken;
}
/// <inheritdoc/>
public override bool Equals(object? obj)
{
return obj is Type2 other && Equals(other);
}
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override int GetHashCode()
{
unchecked
{
// To combine the two hashes, we can simply use the fast djb2 hash algorithm.
// This is not a problem in this case since we already know that the base
// RuntimeHelpers.GetHashCode method is providing hashes with a good enough distribution.
int hash = RuntimeHelpers.GetHashCode(this.tMessage);
hash = (hash << 5) + hash;
hash += RuntimeHelpers.GetHashCode(this.tToken);
return hash;
}
return RuntimeHelpers.GetHashCode(this.Target);
}
}

Просмотреть файл

@ -0,0 +1,488 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using Microsoft.Collections.Extensions;
using Microsoft.Toolkit.Mvvm.Messaging.Internals;
#if NETSTANDARD2_1
using RecipientsTable = System.Runtime.CompilerServices.ConditionalWeakTable<object, Microsoft.Collections.Extensions.IDictionarySlim>;
#else
using RecipientsTable = Microsoft.Toolkit.Mvvm.Messaging.WeakReferenceMessenger.ConditionalWeakTable<object, Microsoft.Collections.Extensions.IDictionarySlim>;
#endif
namespace Microsoft.Toolkit.Mvvm.Messaging
{
/// <summary>
/// A class providing a reference implementation for the <see cref="IMessenger"/> interface.
/// </summary>
/// <remarks>
/// This <see cref="IMessenger"/> implementation uses weak references to track the registered
/// recipients, so it is not necessary to manually unregister them when they're no longer needed.
/// </remarks>
public sealed class WeakReferenceMessenger : IMessenger
{
// This messenger uses the following logic to link stored instances together:
// --------------------------------------------------------------------------------------------------------
// DictionarySlim<TToken, MessageHandler<TRecipient, TMessage>> mapping
// / / /
// ___(Type2.TToken)___/ / /
// /_________________(Type2.TMessage)______________________/ /
// / ___________________________/
// / /
// DictionarySlim<Type2, ConditionalWeakTable<object, IDictionarySlim>> recipientsMap;
// --------------------------------------------------------------------------------------------------------
// Just like in the strong reference variant, each pair of message and token types is used as a key in the
// recipients map. In this case, the values in the dictionary are ConditionalWeakTable<,> instances, that
// link each registered recipient to a map of currently registered handlers, through a weak reference.
// The value in each conditional table is Dictionary<TToken, MessageHandler<TRecipient, TMessage>>, using
// the same unsafe cast as before to allow the generic handler delegates to be invoked without knowing
// what type each recipient was registered with, and without the need to use reflection.
/// <summary>
/// The map of currently registered recipients for all message types.
/// </summary>
private readonly DictionarySlim<Type2, RecipientsTable> recipientsMap = new DictionarySlim<Type2, RecipientsTable>();
/// <summary>
/// Gets the default <see cref="WeakReferenceMessenger"/> instance.
/// </summary>
public static WeakReferenceMessenger Default { get; } = new WeakReferenceMessenger();
/// <inheritdoc/>
public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
{
Type2 type2 = new Type2(typeof(TMessage), typeof(TToken));
// Get the conditional table associated with the target recipient, for the current pair
// of token and message types. If it exists, check if there is a matching token.
return
this.recipientsMap.TryGetValue(type2, out RecipientsTable? table) &&
table!.TryGetValue(recipient, out IDictionarySlim? mapping) &&
Unsafe.As<DictionarySlim<TToken, object>>(mapping).ContainsKey(token);
}
}
/// <inheritdoc/>
public void Register<TRecipient, TMessage, TToken>(TRecipient recipient, TToken token, MessageHandler<TRecipient, TMessage> handler)
where TRecipient : class
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
{
Type2 type2 = new Type2(typeof(TMessage), typeof(TToken));
// Get the conditional table for the pair of type arguments, or create it if it doesn't exist
ref RecipientsTable? mapping = ref this.recipientsMap.GetOrAddValueRef(type2);
mapping ??= new RecipientsTable();
// Get or create the handlers dictionary for the target recipient
var map = Unsafe.As<DictionarySlim<TToken, object>>(mapping.GetValue(recipient, _ => new DictionarySlim<TToken, object>()));
// Add the new registration entry
ref object? registeredHandler = ref map.GetOrAddValueRef(token);
if (!(registeredHandler is null))
{
ThrowInvalidOperationExceptionForDuplicateRegistration();
}
// Store the input handler
registeredHandler = handler;
}
}
/// <inheritdoc/>
public void UnregisterAll(object recipient)
{
lock (this.recipientsMap)
{
var enumerator = this.recipientsMap.GetEnumerator();
// Traverse all the existing conditional tables and remove all the ones
// with the target recipient as key. We don't perform a cleanup here,
// as that is responsability of a separate method defined below.
while (enumerator.MoveNext())
{
enumerator.Value.Remove(recipient);
}
}
}
/// <inheritdoc/>
public void UnregisterAll<TToken>(object recipient, TToken token)
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
{
var enumerator = this.recipientsMap.GetEnumerator();
// Same as above, with the difference being that this time we only go through
// the conditional tables having a matching token type as key, and that we
// only try to remove handlers with a matching token, if any.
while (enumerator.MoveNext())
{
if (enumerator.Key.TToken == typeof(TToken) &&
enumerator.Value.TryGetValue(recipient, out IDictionarySlim mapping))
{
Unsafe.As<DictionarySlim<TToken, object>>(mapping).TryRemove(token, out _);
}
}
}
}
/// <inheritdoc/>
public void Unregister<TMessage, TToken>(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
lock (this.recipientsMap)
{
var type2 = new Type2(typeof(TMessage), typeof(TToken));
var enumerator = this.recipientsMap.GetEnumerator();
// Traverse all the existing token and message pairs matching the current type
// arguments, and remove all the handlers with a matching token, as above.
while (enumerator.MoveNext())
{
if (enumerator.Key.Equals(type2) &&
enumerator.Value.TryGetValue(recipient, out IDictionarySlim mapping))
{
Unsafe.As<DictionarySlim<TToken, object>>(mapping).TryRemove(token, out _);
}
}
}
}
/// <inheritdoc/>
public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
ArrayPoolBufferWriter<object> recipients;
ArrayPoolBufferWriter<object> handlers;
lock (this.recipientsMap)
{
Type2 type2 = new Type2(typeof(TMessage), typeof(TToken));
// Try to get the target table
if (!this.recipientsMap.TryGetValue(type2, out RecipientsTable? table))
{
return message;
}
recipients = ArrayPoolBufferWriter<object>.Create();
handlers = ArrayPoolBufferWriter<object>.Create();
// We need a local, temporary copy of all the pending recipients and handlers to
// invoke, to avoid issues with handlers unregistering from messages while we're
// holding the lock. To do this, we can just traverse the conditional table in use
// to enumerate all the existing recipients for the token and message types pair
// corresponding to the generic arguments for this invocation, and then track the
// handlers with a matching token, and their corresponding recipients.
foreach (KeyValuePair<object, IDictionarySlim> pair in table!)
{
var map = Unsafe.As<DictionarySlim<TToken, object>>(pair.Value);
if (map.TryGetValue(token, out object? handler))
{
recipients.Add(pair.Key);
handlers.Add(handler!);
}
}
}
try
{
ReadOnlySpan<object>
recipientsSpan = recipients.Span,
handlersSpan = handlers.Span;
for (int i = 0; i < recipientsSpan.Length; i++)
{
// Just like in the other messenger, here we need an unsafe cast to be able to
// invoke a generic delegate with a contravariant input argument, with a less
// derived reference, without reflection. This is guaranteed to work by how the
// messenger tracks registered recipients and their associated handlers, so the
// type conversion will always be valid (the recipients are the rigth instances).
Unsafe.As<MessageHandler<object, TMessage>>(handlersSpan[i])(recipientsSpan[i], message);
}
}
finally
{
recipients.Dispose();
handlers.Dispose();
}
return message;
}
/// <inheritdoc/>
public void Cleanup()
{
lock (this.recipientsMap)
{
using ArrayPoolBufferWriter<Type2> type2s = ArrayPoolBufferWriter<Type2>.Create();
using ArrayPoolBufferWriter<object> emptyRecipients = ArrayPoolBufferWriter<object>.Create();
var enumerator = this.recipientsMap.GetEnumerator();
// First, we go through all the currently registered pairs of token and message types.
// These represents all the combinations of generic arguments with at least one registered
// handler, with the exception of those with recipients that have already been collected.
while (enumerator.MoveNext())
{
emptyRecipients.Reset();
bool hasAtLeastOneHandler = false;
// Go through the currently alive recipients to look for those with no handlers left. We track
// the ones we find to remove them outside of the loop (can't modify during enumeration).
foreach (KeyValuePair<object, IDictionarySlim> pair in enumerator.Value)
{
if (pair.Value.Count == 0)
{
emptyRecipients.Add(pair.Key);
}
else
{
hasAtLeastOneHandler = true;
}
}
// Remove the handler maps for recipients that are still alive but with no handlers
foreach (object recipient in emptyRecipients.Span)
{
enumerator.Value.Remove(recipient);
}
// Track the type combinations with no recipients or handlers left
if (!hasAtLeastOneHandler)
{
type2s.Add(enumerator.Key);
}
}
// Remove all the mappings with no handlers left
foreach (Type2 key in type2s.Span)
{
this.recipientsMap.TryRemove(key, out _);
}
}
}
/// <inheritdoc/>
public void Reset()
{
lock (this.recipientsMap)
{
this.recipientsMap.Clear();
}
}
#if !NETSTANDARD2_1
/// <summary>
/// A wrapper for <see cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}"/>
/// that backports the enumerable support to .NET Standard 2.0 through an auxiliary list.
/// </summary>
/// <typeparam name="TKey">Tke key of items to store in the table.</typeparam>
/// <typeparam name="TValue">The values to store in the table.</typeparam>
internal sealed class ConditionalWeakTable<TKey, TValue>
where TKey : class
where TValue : class?
{
/// <summary>
/// The underlying <see cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}"/> instance.
/// </summary>
private readonly System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue> table;
/// <summary>
/// A supporting linked list to store keys in <see cref="table"/>. This is needed to expose
/// the ability to enumerate existing keys when there is no support for that in the BCL.
/// </summary>
private readonly LinkedList<WeakReference<TKey>> keys;
/// <summary>
/// Initializes a new instance of the <see cref="ConditionalWeakTable{TKey, TValue}"/> class.
/// </summary>
public ConditionalWeakTable()
{
this.table = new System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue>();
this.keys = new LinkedList<WeakReference<TKey>>();
}
/// <inheritdoc cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}.TryGetValue"/>
public bool TryGetValue(TKey key, out TValue value)
{
return this.table.TryGetValue(key, out value);
}
/// <inheritdoc cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}.GetValue"/>
public TValue GetValue(TKey key, System.Runtime.CompilerServices.ConditionalWeakTable<TKey, TValue>.CreateValueCallback createValueCallback)
{
// Get or create the value. When this method returns, the key will be present in the table
TValue value = this.table.GetValue(key, createValueCallback);
// Check if the list of keys contains the given key.
// If it does, we can just stop here and return the result.
foreach (WeakReference<TKey> node in this.keys)
{
if (node.TryGetTarget(out TKey? target) &&
ReferenceEquals(target, key))
{
return value;
}
}
// Add the key to the list of weak references to track it
this.keys.AddFirst(new WeakReference<TKey>(key));
return value;
}
/// <inheritdoc cref="System.Runtime.CompilerServices.ConditionalWeakTable{TKey,TValue}.Remove"/>
public bool Remove(TKey key)
{
return this.table.Remove(key);
}
/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
{
for (LinkedListNode<WeakReference<TKey>>? node = this.keys.First; !(node is null);)
{
LinkedListNode<WeakReference<TKey>>? next = node.Next;
// Get the key and value for the current node
if (node.Value.TryGetTarget(out TKey? target) &&
this.table.TryGetValue(target!, out TValue value))
{
yield return new KeyValuePair<TKey, TValue>(target, value);
}
else
{
// If the current key has been collected, trim the list
this.keys.Remove(node);
}
node = next;
}
}
}
#endif
/// <summary>
/// A simple buffer writer implementation using pooled arrays.
/// </summary>
/// <typeparam name="T">The type of items to store in the list.</typeparam>
/// <remarks>
/// This type is a <see langword="ref"/> <see langword="struct"/> to avoid the object allocation and to
/// enable the pattern-based <see cref="IDisposable"/> support. We aren't worried with consumers not
/// using this type correctly since it's private and only accessible within the parent type.
/// </remarks>
private ref struct ArrayPoolBufferWriter<T>
{
/// <summary>
/// The default buffer size to use to expand empty arrays.
/// </summary>
private const int DefaultInitialBufferSize = 128;
/// <summary>
/// The underlying <typeparamref name="T"/> array.
/// </summary>
private T[] array;
/// <summary>
/// The starting offset within <see cref="array"/>.
/// </summary>
private int index;
/// <summary>
/// Creates a new instance of the <see cref="ArrayPoolBufferWriter{T}"/> struct.
/// </summary>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ArrayPoolBufferWriter<T> Create()
{
return new ArrayPoolBufferWriter<T> { array = ArrayPool<T>.Shared.Rent(DefaultInitialBufferSize) };
}
/// <summary>
/// Gets a <see cref="ReadOnlySpan{T}"/> with the current items.
/// </summary>
public ReadOnlySpan<T> Span
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.array.AsSpan(0, this.index);
}
/// <summary>
/// Adds a new item to the current collection.
/// </summary>
/// <param name="item">The item to add.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Add(T item)
{
if (this.index == this.array.Length)
{
ResizeBuffer();
}
this.array[this.index++] = item;
}
/// <summary>
/// Resets the underlying array and the stored items.
/// </summary>
public void Reset()
{
Array.Clear(this.array, 0, this.index);
this.index = 0;
}
/// <summary>
/// Resizes <see cref="array"/> when there is no space left for new items.
/// </summary>
[MethodImpl(MethodImplOptions.NoInlining)]
private void ResizeBuffer()
{
T[] rent = ArrayPool<T>.Shared.Rent(this.index << 2);
Array.Copy(this.array, 0, rent, 0, this.index);
Array.Clear(this.array, 0, this.index);
ArrayPool<T>.Shared.Return(this.array);
this.array = rent;
}
/// <inheritdoc cref="IDisposable.Dispose"/>
public void Dispose()
{
Array.Clear(this.array, 0, this.index);
ArrayPool<T>.Shared.Return(this.array);
}
}
/// <summary>
/// Throws an <see cref="InvalidOperationException"/> when trying to add a duplicate handler.
/// </summary>
private static void ThrowInvalidOperationExceptionForDuplicateRegistration()
{
throw new InvalidOperationException("The target recipient has already subscribed to the target message");
}
}
}

Просмотреть файл

@ -4,6 +4,7 @@
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Title>Windows Community Toolkit MVVM Toolkit</Title>
<Description>
This package includes a .NET Standard MVVM library with helpers such as:

Просмотреть файл

@ -50,13 +50,12 @@
Margin="5,0,0,0"
Width="50">
<AppBarButton.Icon>
<PathIcon
Data="{StaticResource GithubIcon}"
Width="50"
Height="50"
Margin="-10">
<PathIcon Margin="-3"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Data="{StaticResource GithubIcon}">
<PathIcon.RenderTransform>
<CompositeTransform TranslateY="8" TranslateX="3" />
<CompositeTransform TranslateY="5" TranslateX="-5" />
</PathIcon.RenderTransform>
</PathIcon>
</AppBarButton.Icon>

Просмотреть файл

@ -155,10 +155,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls.Markdown.Render
var columnIndex = Grid.GetColumn(child);
var rowIndex = Grid.GetRow(child);
var rect = new Rect(0, 0, 0, 0)
{
X = _borderThickness
};
var rect = new Rect(_borderThickness, 0, 0, 0);
for (int col = 0; col < columnIndex; col++)
{

Просмотреть файл

@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Toolkit.Uwp.Extensions;
using Microsoft.Toolkit.Uwp.UI.Extensions;
using Windows.Foundation;
using Windows.UI;
@ -283,7 +284,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
double centerLeft = 0;
double centerTop = 0;
Clip = new RectangleGeometry { Rect = new Rect(0, 0, finalSize.Width, finalSize.Height) };
Clip = new RectangleGeometry { Rect = finalSize.ToRect() };
for (int i = 0; i < Children.Count; i++)
{

Просмотреть файл

@ -6,6 +6,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Graphics.Canvas;
using Microsoft.Toolkit.Uwp.Extensions;
using Windows.Foundation;
using Windows.Graphics.DirectX;
using Windows.Graphics.Display;
@ -98,7 +99,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
for (var j = colorStartX; j < colorEndX; j++)
{
var color = colors[((i - colorStartY) * width) + (j - colorStartX)];
drawingSession.FillRectangle(new Rect(startPoint, size), color);
drawingSession.FillRectangle(startPoint.ToRect(size), color);
startPoint.X += PreviewPixelsPerRawPixel;
}

Просмотреть файл

@ -4,6 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.Toolkit.Uwp.Extensions;
using Windows.Foundation;
using Windows.UI.Core;
using Windows.UI.Xaml;
@ -216,8 +217,8 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
}
var transform = TargetElement.TransformToVisual(content);
var position = transform.TransformPoint(default(Point));
_eyedropper.WorkArea = new Rect(position, new Size(TargetElement.ActualWidth, TargetElement.ActualHeight));
var position = transform.TransformPoint(default);
_eyedropper.WorkArea = position.ToRect(TargetElement.ActualWidth, TargetElement.ActualHeight);
if (ControlHelpers.IsXamlRootAvailable && XamlRoot != null)
{
_eyedropper.XamlRoot = XamlRoot;

Просмотреть файл

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Microsoft.Toolkit.Uwp.Extensions;
using Windows.Foundation;
using Windows.UI.Composition;
using Windows.UI.Xaml;
@ -117,7 +118,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
rectKeyframes.Add(new DiscreteObjectKeyFrame
{
KeyTime = KeyTime.FromTimeSpan(time),
Value = new Rect(startPoint, endPoint)
Value = startPoint.ToRect(endPoint)
});
}

Просмотреть файл

@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Toolkit.Uwp.Extensions;
using Windows.Foundation;
using Windows.System;
using Windows.UI.Core;
@ -98,7 +99,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
private void ImageCropperThumb_KeyUp(object sender, KeyRoutedEventArgs e)
{
var selectedRect = new Rect(new Point(_startX, _startY), new Point(_endX, _endY));
var selectedRect = new Point(_startX, _startY).ToRect(new Point(_endX, _endY));
var croppedRect = _inverseImageTransform.TransformBounds(selectedRect);
if (croppedRect.Width > MinCropSize.Width && croppedRect.Height > MinCropSize.Height)
{
@ -111,7 +112,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
private void ImageCropperThumb_ManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
var selectedRect = new Rect(new Point(_startX, _startY), new Point(_endX, _endY));
var selectedRect = new Point(_startX, _startY).ToRect(new Point(_endX, _endY));
var croppedRect = _inverseImageTransform.TransformBounds(selectedRect);
if (croppedRect.Width > MinCropSize.Width && croppedRect.Height > MinCropSize.Height)
{
@ -155,7 +156,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
offsetY = Math.Max(offsetY, _restrictedSelectRect.Y - _startY);
}
var selectedRect = new Rect(new Point(_startX, _startY), new Point(_endX, _endY));
var selectedRect = new Point(_startX, _startY).ToRect(new Point(_endX, _endY));
selectedRect.X += offsetX;
selectedRect.Y += offsetY;
var croppedRect = _inverseImageTransform.TransformBounds(selectedRect);

Просмотреть файл

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
using Microsoft.Graphics.Canvas.Geometry;
using Microsoft.Toolkit.Uwp.Extensions;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
@ -299,7 +300,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
break;
}
return new Rect(startPoint, endPoint);
return startPoint.ToRect(endPoint);
}
/// <summary>

Просмотреть файл

@ -4,6 +4,7 @@
using System;
using System.Numerics;
using Microsoft.Toolkit.Uwp.Extensions;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Hosting;
@ -111,7 +112,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
var startPoint = new Point(_startX, _startY);
var endPoint = new Point(_endX, _endY);
var currentSelectedRect = new Rect(startPoint, endPoint);
var currentSelectedRect = startPoint.ToRect(endPoint);
switch (position)
{
case ThumbPosition.Top:
@ -251,7 +252,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
var isEffectiveRegion = IsSafePoint(_restrictedSelectRect, startPoint) &&
IsSafePoint(_restrictedSelectRect, endPoint);
var selectedRect = new Rect(startPoint, endPoint);
var selectedRect = startPoint.ToRect(endPoint);
if (!isEffectiveRegion)
{
if (!IsCornerThumb(position) && TryGetContainedRect(_restrictedSelectRect, ref selectedRect))
@ -268,8 +269,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
selectedRect.Union(CanvasRect);
if (selectedRect != CanvasRect)
{
var croppedRect = _inverseImageTransform.TransformBounds(
new Rect(startPoint, endPoint));
var croppedRect = _inverseImageTransform.TransformBounds(startPoint.ToRect(endPoint));
croppedRect.Intersect(_restrictedCropRect);
_currentCroppedRect = croppedRect;
var viewportRect = GetUniformRect(CanvasRect, selectedRect.Width / selectedRect.Height);
@ -460,7 +460,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
case CropShape.Rectangular:
if (_innerGeometry is RectangleGeometry rectangleGeometry)
{
var to = new Rect(new Point(_startX, _startY), new Point(_endX, _endY));
var to = new Point(_startX, _startY).ToRect(new Point(_endX, _endY));
if (animate)
{
var storyboard = new Storyboard();

Просмотреть файл

@ -4,8 +4,8 @@
using System;
using System.Threading.Tasks;
using Microsoft.Toolkit.Uwp.Extensions;
using Windows.Foundation;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
@ -120,7 +120,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
{
get
{
var realMinSelectSize = _imageTransform.TransformBounds(new Rect(default(Point), MinCropSize));
var realMinSelectSize = _imageTransform.TransformBounds(MinCropSize.ToRect());
var minLength = Math.Min(realMinSelectSize.Width, realMinSelectSize.Height);
if (minLength < MinSelectedLength)
{

Просмотреть файл

@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Toolkit.Uwp.Extensions;
using Microsoft.Toolkit.Uwp.UI.Extensions;
using Windows.Foundation;
using Windows.UI.Xaml;
@ -131,7 +132,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
var y_normalized = (finalSize.Height / 2) - y - (element.DesiredSize.Height / 2);
var point = new Point(x_normalized, y_normalized);
element.Arrange(new Rect(point, element.DesiredSize));
element.Arrange(point.ToRect(element.DesiredSize));
var elementProperties = new OrbitViewElementProperties()
{

Просмотреть файл

@ -5,8 +5,8 @@
using System;
using System.Collections;
using System.Collections.Specialized;
using Microsoft.Toolkit.Uwp.Extensions;
using Microsoft.Toolkit.Uwp.Helpers;
using Windows.Foundation;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
@ -143,7 +143,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
}
// Set clip to control
Clip = new RectangleGeometry() { Rect = new Rect(default(Point), e.NewSize) };
Clip = new RectangleGeometry { Rect = e.NewSize.ToRect() };
}
private void RotatorTile_Loaded(object sender, RoutedEventArgs e)

Просмотреть файл

@ -152,7 +152,7 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls
// Make sure all overflown elements have no size.
foreach (var child in _overflow)
{
child.Arrange(new Rect(0, 0, 0, 0));
child.Arrange(default);
}
_overflow = new List<UIElement>(); // Reset for next time.

Просмотреть файл

@ -0,0 +1,58 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using Point = Windows.Foundation.Point;
using Rect = Windows.Foundation.Rect;
using Size = Windows.Foundation.Size;
namespace Microsoft.Toolkit.Uwp.Extensions
{
/// <summary>
/// Extensions for the <see cref="Point"/> type.
/// </summary>
public static class PointExtensions
{
/// <summary>
/// Creates a new <see cref="Rect"/> of the specified size, starting at a given point.
/// </summary>
/// <param name="point">The input <see cref="Point"/> value to convert.</param>
/// <param name="width">The width of the rectangle.</param>
/// <param name="height">The height of the rectangle.</param>
/// <returns>A <see cref="Rect"/> value of the specified size, starting at the given point.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rect ToRect(this Point point, double width, double height)
{
return new Rect(point.X, point.Y, width, height);
}
/// <summary>
/// Creates a new <see cref="Rect"/> ending at the specified point, starting at the given coordinates.
/// </summary>
/// <param name="point">The input <see cref="Point"/> value to convert.</param>
/// <param name="end">The ending position for the rectangle.</param>
/// <returns>A <see cref="Rect"/> value between the two specified points.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rect ToRect(this Point point, Point end)
{
return new Rect(point, end);
}
/// <summary>
/// Creates a new <see cref="Rect"/> of the specified size, starting at the given coordinates.
/// </summary>
/// <param name="point">The input <see cref="Point"/> value to convert.</param>
/// <param name="size">The size of the rectangle to create.</param>
/// <returns>A <see cref="Rect"/> value of the specified size, starting at the given coordinates.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rect ToRect(this Point point, Size size)
{
return new Rect(point, size);
}
}
}

Просмотреть файл

@ -0,0 +1,57 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using Point = Windows.Foundation.Point;
using Rect = Windows.Foundation.Rect;
using Size = Windows.Foundation.Size;
namespace Microsoft.Toolkit.Uwp.Extensions
{
/// <summary>
/// Extensions for the <see cref="Size"/> type.
/// </summary>
public static class SizeExtensions
{
/// <summary>
/// Creates a new <see cref="Rect"/> of the specified size, starting at the origin.
/// </summary>
/// <param name="size">The input <see cref="Size"/> value to convert.</param>
/// <returns>A <see cref="Rect"/> value of the specified size, starting at the origin.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rect ToRect(this Size size)
{
return new Rect(0, 0, size.Width, size.Height);
}
/// <summary>
/// Creates a new <see cref="Rect"/> of the specified size, starting at the given coordinates.
/// </summary>
/// <param name="size">The input <see cref="Size"/> value to convert.</param>
/// <param name="x">The horizontal offset.</param>
/// <param name="y">The vertical offset.</param>
/// <returns>A <see cref="Rect"/> value of the specified size, starting at the given coordinates.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rect ToRect(this Size size, double x, double y)
{
return new Rect(x, y, size.Width, size.Height);
}
/// <summary>
/// Creates a new <see cref="Rect"/> of the specified size, starting at the given position.
/// </summary>
/// <param name="size">The input <see cref="Size"/> value to convert.</param>
/// <param name="point">The starting position to use.</param>
/// <returns>A <see cref="Rect"/> value of the specified size, starting at the given position.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Rect ToRect(this Size size, Point point)
{
return new Rect(point, size);
}
}
}

Просмотреть файл

@ -39,7 +39,8 @@ namespace Microsoft.Toolkit.Extensions
[typeof(double)] = "double",
[typeof(decimal)] = "decimal",
[typeof(object)] = "object",
[typeof(string)] = "string"
[typeof(string)] = "string",
[typeof(void)] = "void"
};
/// <summary>
@ -56,7 +57,7 @@ namespace Microsoft.Toolkit.Extensions
public static string ToTypeString(this Type type)
{
// Local function to create the formatted string for a given type
static string FormatDisplayString(Type type)
static string FormatDisplayString(Type type, int genericTypeOffset, ReadOnlySpan<Type> typeArguments)
{
// Primitive types use the keyword name
if (BuiltInTypesMap.TryGetValue(type, out string? typeName))
@ -64,62 +65,138 @@ namespace Microsoft.Toolkit.Extensions
return typeName!;
}
// Generic types
if (
#if NETSTANDARD1_4
type.GetTypeInfo().IsGenericType &&
#else
type.IsGenericType &&
#endif
type.FullName is { } fullName &&
fullName.Split('`') is { } tokens &&
tokens.Length > 0 &&
tokens[0] is { } genericName &&
genericName.Length > 0)
{
var typeArguments = type.GetGenericArguments().Select(FormatDisplayString);
// Nullable<T> types are displayed as T?
var genericType = type.GetGenericTypeDefinition();
if (genericType == typeof(Nullable<>))
{
return $"{typeArguments.First()}?";
}
// ValueTuple<T1, T2> types are displayed as (T1, T2)
if (genericType == typeof(ValueTuple<>) ||
genericType == typeof(ValueTuple<,>) ||
genericType == typeof(ValueTuple<,,>) ||
genericType == typeof(ValueTuple<,,,>) ||
genericType == typeof(ValueTuple<,,,,>) ||
genericType == typeof(ValueTuple<,,,,,>) ||
genericType == typeof(ValueTuple<,,,,,,>) ||
genericType == typeof(ValueTuple<,,,,,,,>))
{
return $"({string.Join(", ", typeArguments)})";
}
// Standard generic types are displayed as Foo<T>
return $"{genericName}<{string.Join(", ", typeArguments)}>";
}
// Array types are displayed as Foo[]
if (type.IsArray)
{
var elementType = type.GetElementType();
var elementType = type.GetElementType()!;
var rank = type.GetArrayRank();
return $"{FormatDisplayString(elementType)}[{new string(',', rank - 1)}]";
return $"{FormatDisplayString(elementType, 0, elementType.GetGenericArguments())}[{new string(',', rank - 1)}]";
}
return type.ToString();
// By checking generic types here we are only interested in specific cases,
// ie. nullable value types or value typles. We have a separate path for custom
// generic types, as we can't rely on this API in that case, as it doesn't show
// a difference between nested types that are themselves generic, or nested simple
// types from a generic declaring type. To deal with that, we need to manually track
// the offset within the array of generic arguments for the whole constructed type.
if (type.IsGenericType())
{
var genericTypeDefinition = type.GetGenericTypeDefinition();
// Nullable<T> types are displayed as T?
if (genericTypeDefinition == typeof(Nullable<>))
{
var nullableArguments = type.GetGenericArguments();
return $"{FormatDisplayString(nullableArguments[0], 0, nullableArguments)}?";
}
// ValueTuple<T1, T2> types are displayed as (T1, T2)
if (genericTypeDefinition == typeof(ValueTuple<>) ||
genericTypeDefinition == typeof(ValueTuple<,>) ||
genericTypeDefinition == typeof(ValueTuple<,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,,,>) ||
genericTypeDefinition == typeof(ValueTuple<,,,,,,,>))
{
var formattedTypes = type.GetGenericArguments().Select(t => FormatDisplayString(t, 0, t.GetGenericArguments()));
return $"({string.Join(", ", formattedTypes)})";
}
}
string displayName;
// Generic types
if (type.Name.Contains('`'))
{
// Retrieve the current generic arguments for the current type (leaf or not)
var tokens = type.Name.Split('`');
var genericArgumentsCount = int.Parse(tokens[1]);
var typeArgumentsOffset = typeArguments.Length - genericTypeOffset - genericArgumentsCount;
var currentTypeArguments = typeArguments.Slice(typeArgumentsOffset, genericArgumentsCount).ToArray();
var formattedTypes = currentTypeArguments.Select(t => FormatDisplayString(t, 0, t.GetGenericArguments()));
// Standard generic types are displayed as Foo<T>
displayName = $"{tokens[0]}<{string.Join(", ", formattedTypes)}>";
// Track the current offset for the shared generic arguments list
genericTypeOffset += genericArgumentsCount;
}
else
{
// Simple custom types
displayName = type.Name;
}
// If the type is nested, recursively format the hierarchy as well
if (type.IsNested)
{
var openDeclaringType = type.DeclaringType!;
var rootGenericArguments = typeArguments.Slice(0, typeArguments.Length - genericTypeOffset).ToArray();
// If the declaring type is generic, we need to reconstruct the closed type
// manually, as the declaring type instance doesn't retain type information.
if (rootGenericArguments.Length > 0)
{
var closedDeclaringType = openDeclaringType.GetGenericTypeDefinition().MakeGenericType(rootGenericArguments);
return $"{FormatDisplayString(closedDeclaringType, genericTypeOffset, typeArguments)}.{displayName}";
}
return $"{FormatDisplayString(openDeclaringType, genericTypeOffset, typeArguments)}.{displayName}";
}
return $"{type.Namespace}.{displayName}";
}
// Atomically get or build the display string for the current type.
// Manually create a static lambda here to enable caching of the generated closure.
// This is a workaround for the missing caching for method group conversions, and should
// be removed once this issue is resolved: https://github.com/dotnet/roslyn/issues/5835.
return DisplayNames.GetValue(type, t => FormatDisplayString(t));
return DisplayNames.GetValue(type, t =>
{
// By-ref types are displayed as T&
if (t.IsByRef)
{
t = t.GetElementType()!;
return $"{FormatDisplayString(t, 0, t.GetGenericArguments())}&";
}
// Pointer types are displayed as T*
if (t.IsPointer)
{
int depth = 0;
// Calculate the pointer indirection level
while (t.IsPointer)
{
depth++;
t = t.GetElementType()!;
}
return $"{FormatDisplayString(t, 0, t.GetGenericArguments())}{new string('*', depth)}";
}
// Standard path for concrete types
return FormatDisplayString(t, 0, t.GetGenericArguments());
});
}
/// <summary>
/// Returns whether or not a given type is generic.
/// </summary>
/// <param name="type">The input type.</param>
/// <returns>Whether or not the input type is generic.</returns>
[Pure]
private static bool IsGenericType(this Type type)
{
#if NETSTANDARD1_4
return type.GetTypeInfo().IsGenericType;
#else
return type.IsGenericType;
#endif
}
#if NETSTANDARD1_4
@ -128,6 +205,7 @@ namespace Microsoft.Toolkit.Extensions
/// </summary>
/// <param name="type">The input type.</param>
/// <returns>An array of types representing the generic arguments.</returns>
[Pure]
private static Type[] GetGenericArguments(this Type type)
{
return type.GetTypeInfo().GenericTypeParameters;
@ -139,6 +217,7 @@ namespace Microsoft.Toolkit.Extensions
/// <param name="type">The input type.</param>
/// <param name="value">The type to check against.</param>
/// <returns><see langword="true"/> if <paramref name="type"/> is an instance of <paramref name="value"/>, <see langword="false"/> otherwise.</returns>
[Pure]
internal static bool IsInstanceOfType(this Type type, object value)
{
return type.GetTypeInfo().IsAssignableFrom(value.GetType().GetTypeInfo());

Просмотреть файл

@ -9,6 +9,7 @@ using System.Linq;
using Microsoft.Toolkit.HighPerformance.Buffers;
using Microsoft.Toolkit.HighPerformance.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using UnitTests.HighPerformance.Shared.Buffers;
namespace UnitTests.HighPerformance.Buffers
{
@ -54,6 +55,61 @@ namespace UnitTests.HighPerformance.Buffers
Assert.ThrowsException<ObjectDisposedException>(() => writer.Advance(1));
}
[TestCategory("ArrayPoolBufferWriterOfT")]
[TestMethod]
public void Test_ArrayPoolBufferWriterOfT_AllocateFromCustomPoolAndGetMemoryAndSpan()
{
var pool = new TrackingArrayPool<byte>();
using (var writer = new ArrayPoolBufferWriter<byte>(pool))
{
Assert.AreEqual(pool.RentedArrays.Count, 1);
Assert.AreEqual(writer.Capacity, 256);
Assert.AreEqual(writer.FreeCapacity, 256);
Assert.AreEqual(writer.WrittenCount, 0);
Assert.IsTrue(writer.WrittenMemory.IsEmpty);
Assert.IsTrue(writer.WrittenSpan.IsEmpty);
Span<byte> span = writer.GetSpan(43);
Assert.IsTrue(span.Length >= 43);
writer.Advance(43);
Assert.AreEqual(writer.Capacity, 256);
Assert.AreEqual(writer.FreeCapacity, 256 - 43);
Assert.AreEqual(writer.WrittenCount, 43);
Assert.AreEqual(writer.WrittenMemory.Length, 43);
Assert.AreEqual(writer.WrittenSpan.Length, 43);
Assert.ThrowsException<ArgumentOutOfRangeException>(() => writer.Advance(-1));
Assert.ThrowsException<ArgumentOutOfRangeException>(() => writer.GetMemory(-1));
Assert.ThrowsException<ArgumentException>(() => writer.Advance(1024));
writer.Dispose();
Assert.ThrowsException<ObjectDisposedException>(() => writer.WrittenMemory);
Assert.ThrowsException<ObjectDisposedException>(() => writer.WrittenSpan.Length);
Assert.ThrowsException<ObjectDisposedException>(() => writer.Capacity);
Assert.ThrowsException<ObjectDisposedException>(() => writer.FreeCapacity);
Assert.ThrowsException<ObjectDisposedException>(() => writer.Clear());
Assert.ThrowsException<ObjectDisposedException>(() => writer.Advance(1));
}
Assert.AreEqual(pool.RentedArrays.Count, 0);
}
[TestCategory("ArrayPoolBufferWriterOfT")]
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Test_ArrayPoolBufferWriterOfT_InvalidRequestedSize()
{
var writer = new ArrayPoolBufferWriter<byte>(-1);
Assert.Fail("You shouldn't be here");
}
[TestCategory("ArrayPoolBufferWriterOfT")]
[TestMethod]
public void Test_ArrayPoolBufferWriterOfT_Clear()

Просмотреть файл

@ -3,10 +3,12 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Toolkit.HighPerformance.Buffers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using UnitTests.HighPerformance.Shared.Buffers;
namespace UnitTests.HighPerformance.Buffers
{
@ -30,6 +32,39 @@ namespace UnitTests.HighPerformance.Buffers
Assert.IsTrue(buffer.Span.ToArray().All(i => i == 42));
}
[TestCategory("MemoryOwnerOfT")]
[TestMethod]
public void Test_MemoryOwnerOfT_AllocateFromCustomPoolAndGetMemoryAndSpan()
{
var pool = new TrackingArrayPool<int>();
using (var buffer = MemoryOwner<int>.Allocate(127, pool))
{
Assert.AreEqual(pool.RentedArrays.Count, 1);
Assert.IsTrue(buffer.Length == 127);
Assert.IsTrue(buffer.Memory.Length == 127);
Assert.IsTrue(buffer.Span.Length == 127);
buffer.Span.Fill(42);
Assert.IsTrue(buffer.Memory.Span.ToArray().All(i => i == 42));
Assert.IsTrue(buffer.Span.ToArray().All(i => i == 42));
}
Assert.AreEqual(pool.RentedArrays.Count, 0);
}
[TestCategory("MemoryOwnerOfT")]
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Test_MemoryOwnerOfT_InvalidRequestedSize()
{
using var buffer = MemoryOwner<int>.Allocate(-1);
Assert.Fail("You shouldn't be here");
}
[TestCategory("MemoryOwnerOfT")]
[TestMethod]
[ExpectedException(typeof(ObjectDisposedException))]

Просмотреть файл

@ -2,10 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Toolkit.HighPerformance.Buffers;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using UnitTests.HighPerformance.Shared.Buffers;
namespace UnitTests.HighPerformance.Buffers
{
@ -27,6 +29,37 @@ namespace UnitTests.HighPerformance.Buffers
Assert.IsTrue(buffer.Span.ToArray().All(i => i == 42));
}
[TestCategory("SpanOwnerOfT")]
[TestMethod]
public void Test_SpanOwnerOfT_AllocateFromCustomPoolAndGetMemoryAndSpan()
{
var pool = new TrackingArrayPool<int>();
using (var buffer = SpanOwner<int>.Allocate(127, pool))
{
Assert.AreEqual(pool.RentedArrays.Count, 1);
Assert.IsTrue(buffer.Length == 127);
Assert.IsTrue(buffer.Span.Length == 127);
buffer.Span.Fill(42);
Assert.IsTrue(buffer.Span.ToArray().All(i => i == 42));
}
Assert.AreEqual(pool.RentedArrays.Count, 0);
}
[TestCategory("SpanOwnerOfT")]
[TestMethod]
[ExpectedException(typeof(ArgumentOutOfRangeException))]
public void Test_SpanOwnerOfT_InvalidRequestedSize()
{
using var buffer = SpanOwner<int>.Allocate(-1);
Assert.Fail("You shouldn't be here");
}
[TestCategory("HashCodeOfT")]
[TestMethod]
public void Test_SpanOwnerOfT_PooledBuffersAndClear()

Просмотреть файл

@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Buffers;
using System.Collections.Generic;
namespace UnitTests.HighPerformance.Shared.Buffers
{
public sealed class TrackingArrayPool<T> : ArrayPool<T>
{
private readonly ArrayPool<T> pool = ArrayPool<T>.Create();
private readonly HashSet<T[]> arrays = new HashSet<T[]>();
/// <summary>
/// Gets the collection of currently rented out arrays
/// </summary>
public IReadOnlyCollection<T[]> RentedArrays => this.arrays;
/// <inheritdoc/>
public override T[] Rent(int minimumLength)
{
T[] array = this.pool.Rent(minimumLength);
this.arrays.Add(array);
return array;
}
/// <inheritdoc/>
public override void Return(T[] array, bool clearArray = false)
{
this.arrays.Remove(array);
this.pool.Return(array, clearArray);
}
}
}

Просмотреть файл

@ -14,6 +14,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Buffers\Test_ArrayPoolBufferWriter{T}.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Buffers\Test_MemoryOwner{T}.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Buffers\Test_SpanOwner{T}.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Buffers\TrackingArrayPool{T}.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\Test_ArrayExtensions.2D.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\Test_ArrayExtensions.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Extensions\Test_ArrayPoolExtensions.cs" />

Просмотреть файл

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Toolkit.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -15,32 +14,148 @@ namespace UnitTests.Extensions
{
[TestCategory("TypeExtensions")]
[TestMethod]
public void Test_TypeExtensions_BuiltInTypes()
[DataRow("bool", typeof(bool))]
[DataRow("int", typeof(int))]
[DataRow("float", typeof(float))]
[DataRow("double", typeof(double))]
[DataRow("decimal", typeof(decimal))]
[DataRow("object", typeof(object))]
[DataRow("string", typeof(string))]
[DataRow("void", typeof(void))]
public void Test_TypeExtensions_BuiltInTypes(string name, Type type)
{
Assert.AreEqual("bool", typeof(bool).ToTypeString());
Assert.AreEqual("int", typeof(int).ToTypeString());
Assert.AreEqual("float", typeof(float).ToTypeString());
Assert.AreEqual("double", typeof(double).ToTypeString());
Assert.AreEqual("decimal", typeof(decimal).ToTypeString());
Assert.AreEqual("object", typeof(object).ToTypeString());
Assert.AreEqual("string", typeof(string).ToTypeString());
Assert.AreEqual(name, type.ToTypeString());
}
[TestCategory("TypeExtensions")]
[TestMethod]
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009", Justification = "Nullable value tuple type")]
public void Test_TypeExtensions_GenericTypes()
[DataRow("int?", typeof(int?))]
[DataRow("System.DateTime?", typeof(DateTime?))]
[DataRow("(int, float)", typeof((int, float)))]
[DataRow("(double?, string, int)?", typeof((double?, string, int)?))]
[DataRow("int[]", typeof(int[]))]
[DataRow("int[,]", typeof(int[,]))]
[DataRow("System.Span<float>", typeof(Span<float>))]
[DataRow("System.Memory<char>", typeof(Memory<char>))]
[DataRow("System.Collections.Generic.IEnumerable<int>", typeof(IEnumerable<int>))]
[DataRow("System.Collections.Generic.Dictionary<int, System.Collections.Generic.List<float>>", typeof(Dictionary<int, List<float>>))]
public void Test_TypeExtensions_GenericTypes(string name, Type type)
{
Assert.AreEqual(name, type.ToTypeString());
}
[TestCategory("TypeExtensions")]
[TestMethod]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal", typeof(Animal))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat", typeof(Animal.Cat))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Dog", typeof(Animal.Dog))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int?>", typeof(Animal.Rabbit<int?>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<string>", typeof(Animal.Rabbit<string>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo", typeof(Animal.Rabbit<int>.Foo))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<(string, int)?>.Foo", typeof(Animal.Rabbit<(string, int)?>.Foo))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo<string>", typeof(Animal.Rabbit<int>.Foo<string>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int>.Foo<int[]>", typeof(Animal.Rabbit<int>.Foo<int[]>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<string[]>.Foo<object>", typeof(Animal.Rabbit<string[]>.Foo<object>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<(string, int)?>.Foo<(int, int?)>", typeof(Animal.Rabbit<(string, int)?>.Foo<(int, int?)>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime>", typeof(Animal.Llama<float, DateTime>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<string, (int?, object)>", typeof(Animal.Llama<string, (int?, object)>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<string, (int?, object)?>.Foo", typeof(Animal.Llama<string, (int?, object)?>.Foo))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime>.Foo", typeof(Animal.Llama<float, DateTime>.Foo))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<string, (int?, object)?>.Foo<string>", typeof(Animal.Llama<string, (int?, object)?>.Foo<string>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime>.Foo<(float?, int)?>", typeof(Animal.Llama<float, DateTime>.Foo<(float?, int)?>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Vehicle<double>", typeof(Vehicle<double>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Vehicle<int?>[]", typeof(Vehicle<int?>[]))]
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Vehicle<int>>", typeof(List<Vehicle<int>>))]
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Animal.Rabbit<int?>>", typeof(List<Animal.Rabbit<int?>>))]
[DataRow("System.Collections.Generic.List<UnitTests.Extensions.Test_TypeExtensions.Animal.Llama<float, System.DateTime[]>>", typeof(List<Animal.Llama<float, DateTime[]>>))]
public void Test_TypeExtensions_NestedTypes(string name, Type type)
{
Assert.AreEqual(name, type.ToTypeString());
}
#pragma warning disable SA1015 // Closing generic brackets should be spaced correctly
[TestCategory("TypeExtensions")]
[TestMethod]
[DataRow("void*", typeof(void*))]
[DataRow("int**", typeof(int**))]
[DataRow("byte***", typeof(byte***))]
[DataRow("System.Guid*", typeof(Guid*))]
[DataRow("UnitTests.Extensions.Foo<int>*", typeof(Foo<int>*))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat**", typeof(Animal.Cat**))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<int>*", typeof(Animal.Cat<int>*))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<float>.Bar**", typeof(Animal.Cat<float>.Bar**))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<double>.Bar<int>***", typeof(Animal.Cat<double>.Bar<int>***))]
public void Test_TypeExtensions_PointerTypes(string name, Type type)
{
Assert.AreEqual(name, type.ToTypeString());
}
#pragma warning restore SA1015
[TestCategory("TypeExtensions")]
[TestMethod]
[DataRow("int&", typeof(int))]
[DataRow("byte&", typeof(byte))]
[DataRow("System.Guid&", typeof(Guid))]
[DataRow("UnitTests.Extensions.Foo<int>&", typeof(Foo<int>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat&", typeof(Animal.Cat))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<int>&", typeof(Animal.Cat<int>))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<float>.Bar&", typeof(Animal.Cat<float>.Bar))]
[DataRow("UnitTests.Extensions.Test_TypeExtensions.Animal.Cat<double>.Bar<int>&", typeof(Animal.Cat<double>.Bar<int>))]
public void Test_TypeExtensions_RefTypes(string name, Type type)
{
Assert.AreEqual(name, type.MakeByRefType().ToTypeString());
}
private class Animal
{
public struct Cat
{
}
public struct Cat<T1>
{
public struct Bar
{
}
public struct Bar<T2>
{
}
}
public class Dog
{
}
public class Rabbit<T>
{
public class Foo
{
}
public class Foo<T2>
{
}
}
public class Llama<T1, T2>
{
public class Foo
{
}
public class Foo<T3>
{
}
}
}
private class Vehicle<T>
{
Assert.AreEqual("int?", typeof(int?).ToTypeString());
Assert.AreEqual("System.DateTime?", typeof(DateTime?).ToTypeString());
Assert.AreEqual("(int, float)", typeof((int, float)).ToTypeString());
Assert.AreEqual("(double?, string, int)?", typeof((double?, string, int)?).ToTypeString());
Assert.AreEqual("int[]", typeof(int[]).ToTypeString());
Assert.AreEqual(typeof(int[,]).ToTypeString(), "int[,]");
Assert.AreEqual("System.Span<float>", typeof(Span<float>).ToTypeString());
Assert.AreEqual("System.Memory<char>", typeof(Memory<char>).ToTypeString());
Assert.AreEqual("System.Collections.Generic.IEnumerable<int>", typeof(IEnumerable<int>).ToTypeString());
Assert.AreEqual(typeof(Dictionary<int, List<float>>).ToTypeString(), "System.Collections.Generic.Dictionary<int, System.Collections.Generic.List<float>>");
}
}
internal struct Foo<T>
{
}
}

Просмотреть файл

@ -0,0 +1,141 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.Toolkit.Mvvm.Input;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Mvvm
{
[TestClass]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649", Justification = "Generic type")]
public class Test_AsyncRelayCommandOfT
{
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_AsyncRelayCommandOfT_AlwaysEnabled()
{
int ticks = 0;
var command = new AsyncRelayCommand<string>(async s =>
{
await Task.Delay(1000);
ticks = int.Parse(s);
await Task.Delay(1000);
});
Assert.IsTrue(command.CanExecute(null));
Assert.IsTrue(command.CanExecute("1"));
(object, EventArgs) args = default;
command.CanExecuteChanged += (s, e) => args = (s, e);
command.NotifyCanExecuteChanged();
Assert.AreSame(args.Item1, command);
Assert.AreSame(args.Item2, EventArgs.Empty);
Assert.IsNull(command.ExecutionTask);
Assert.IsFalse(command.IsRunning);
Task task = command.ExecuteAsync((object)"42");
Assert.IsNotNull(command.ExecutionTask);
Assert.AreSame(command.ExecutionTask, task);
Assert.IsTrue(command.IsRunning);
await task;
Assert.IsFalse(command.IsRunning);
Assert.AreEqual(ticks, 42);
command.Execute("2");
await command.ExecutionTask!;
Assert.AreEqual(ticks, 2);
Assert.ThrowsException<InvalidCastException>(() => command.Execute(new object()));
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_AsyncRelayCommandOfT_WithCanExecuteFunctionTrue()
{
int ticks = 0;
var command = new AsyncRelayCommand<string>(
s =>
{
ticks = int.Parse(s);
return Task.CompletedTask;
}, s => true);
Assert.IsTrue(command.CanExecute(null));
Assert.IsTrue(command.CanExecute("1"));
command.Execute("42");
Assert.AreEqual(ticks, 42);
command.Execute("2");
Assert.AreEqual(ticks, 2);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_AsyncRelayCommandOfT_WithCanExecuteFunctionFalse()
{
int ticks = 0;
var command = new AsyncRelayCommand<string>(
s =>
{
ticks = int.Parse(s);
return Task.CompletedTask;
}, s => false);
Assert.IsFalse(command.CanExecute(null));
Assert.IsFalse(command.CanExecute("1"));
command.Execute("2");
Assert.AreEqual(ticks, 0);
command.Execute("42");
Assert.AreEqual(ticks, 0);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_AsyncRelayCommandOfT_NullWithValueType()
{
int n = 0;
var command = new AsyncRelayCommand<int>(i =>
{
n = i;
return Task.CompletedTask;
});
// Special case for null value types
Assert.IsTrue(command.CanExecute(null));
command = new AsyncRelayCommand<int>(
i =>
{
n = i;
return Task.CompletedTask;
}, i => i > 0);
Assert.ThrowsException<NullReferenceException>(() => command.CanExecute(null));
}
}
}

Просмотреть файл

@ -15,13 +15,18 @@ namespace UnitTests.Mvvm
{
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_RequestMessage_Ok()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_RequestMessage_Ok(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
object test = null;
void Receive(NumberRequestMessage m)
void Receive(object recipient, NumberRequestMessage m)
{
test = recipient;
Assert.IsFalse(m.HasReceivedResponse);
m.Reply(42);
@ -33,28 +38,33 @@ namespace UnitTests.Mvvm
int result = messenger.Send<NumberRequestMessage>();
Assert.AreSame(test, recipient);
Assert.AreEqual(result, 42);
}
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
[ExpectedException(typeof(InvalidOperationException))]
public void Test_Messenger_RequestMessage_Fail_NoReply()
public void Test_Messenger_RequestMessage_Fail_NoReply(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
int result = messenger.Send<NumberRequestMessage>();
}
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
[ExpectedException(typeof(InvalidOperationException))]
public void Test_Messenger_RequestMessage_Fail_MultipleReplies()
public void Test_Messenger_RequestMessage_Fail_MultipleReplies(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
void Receive(NumberRequestMessage m)
void Receive(object recipient, NumberRequestMessage m)
{
m.Reply(42);
m.Reply(42);
@ -71,12 +81,14 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_Messenger_AsyncRequestMessage_Ok_Sync()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public async Task Test_Messenger_AsyncRequestMessage_Ok_Sync(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
void Receive(AsyncNumberRequestMessage m)
void Receive(object recipient, AsyncNumberRequestMessage m)
{
Assert.IsFalse(m.HasReceivedResponse);
@ -94,9 +106,11 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_Messenger_AsyncRequestMessage_Ok_Async()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public async Task Test_Messenger_AsyncRequestMessage_Ok_Async(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
async Task<int> GetNumberAsync()
@ -106,7 +120,7 @@ namespace UnitTests.Mvvm
return 42;
}
void Receive(AsyncNumberRequestMessage m)
void Receive(object recipient, AsyncNumberRequestMessage m)
{
Assert.IsFalse(m.HasReceivedResponse);
@ -124,23 +138,27 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
[ExpectedException(typeof(InvalidOperationException))]
public async Task Test_Messenger_AsyncRequestMessage_Fail_NoReply()
public async Task Test_Messenger_AsyncRequestMessage_Fail_NoReply(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
int result = await messenger.Send<AsyncNumberRequestMessage>();
}
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
[ExpectedException(typeof(InvalidOperationException))]
public async Task Test_Messenger_AsyncRequestMessage_Fail_MultipleReplies()
public async Task Test_Messenger_AsyncRequestMessage_Fail_MultipleReplies(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
void Receive(AsyncNumberRequestMessage m)
void Receive(object recipient, AsyncNumberRequestMessage m)
{
m.Reply(42);
m.Reply(42);
@ -157,12 +175,14 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_CollectionRequestMessage_Ok_NoReplies()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_CollectionRequestMessage_Ok_NoReplies(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
void Receive(NumbersCollectionRequestMessage m)
void Receive(object recipient, NumbersCollectionRequestMessage m)
{
}
@ -175,17 +195,36 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_CollectionRequestMessage_Ok_MultipleReplies()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_CollectionRequestMessage_Ok_MultipleReplies(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
object
recipient1 = new object(),
recipient2 = new object(),
recipient3 = new object();
recipient3 = new object(),
r1 = null,
r2 = null,
r3 = null;
void Receive1(NumbersCollectionRequestMessage m) => m.Reply(1);
void Receive2(NumbersCollectionRequestMessage m) => m.Reply(2);
void Receive3(NumbersCollectionRequestMessage m) => m.Reply(3);
void Receive1(object recipient, NumbersCollectionRequestMessage m)
{
r1 = recipient;
m.Reply(1);
}
void Receive2(object recipient, NumbersCollectionRequestMessage m)
{
r2 = recipient;
m.Reply(2);
}
void Receive3(object recipient, NumbersCollectionRequestMessage m)
{
r3 = recipient;
m.Reply(3);
}
messenger.Register<NumbersCollectionRequestMessage>(recipient1, Receive1);
messenger.Register<NumbersCollectionRequestMessage>(recipient2, Receive2);
@ -198,6 +237,10 @@ namespace UnitTests.Mvvm
responses.Add(response);
}
Assert.AreSame(r1, recipient1);
Assert.AreSame(r2, recipient2);
Assert.AreSame(r3, recipient3);
CollectionAssert.AreEquivalent(responses, new[] { 1, 2, 3 });
}
@ -207,12 +250,14 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_Messenger_AsyncCollectionRequestMessage_Ok_NoReplies()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public async Task Test_Messenger_AsyncCollectionRequestMessage_Ok_NoReplies(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
void Receive(AsyncNumbersCollectionRequestMessage m)
void Receive(object recipient, AsyncNumbersCollectionRequestMessage m)
{
}
@ -225,9 +270,11 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public async Task Test_Messenger_AsyncCollectionRequestMessage_Ok_MultipleReplies()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public async Task Test_Messenger_AsyncCollectionRequestMessage_Ok_MultipleReplies(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
object
recipient1 = new object(),
recipient2 = new object(),
@ -241,10 +288,10 @@ namespace UnitTests.Mvvm
return 3;
}
void Receive1(AsyncNumbersCollectionRequestMessage m) => m.Reply(1);
void Receive2(AsyncNumbersCollectionRequestMessage m) => m.Reply(Task.FromResult(2));
void Receive3(AsyncNumbersCollectionRequestMessage m) => m.Reply(GetNumberAsync());
void Receive4(AsyncNumbersCollectionRequestMessage m) => m.Reply(_ => GetNumberAsync());
void Receive1(object recipient, AsyncNumbersCollectionRequestMessage m) => m.Reply(1);
void Receive2(object recipient, AsyncNumbersCollectionRequestMessage m) => m.Reply(Task.FromResult(2));
void Receive3(object recipient, AsyncNumbersCollectionRequestMessage m) => m.Reply(GetNumberAsync());
void Receive4(object recipient, AsyncNumbersCollectionRequestMessage m) => m.Reply(_ => GetNumberAsync());
messenger.Register<AsyncNumbersCollectionRequestMessage>(recipient1, Receive1);
messenger.Register<AsyncNumbersCollectionRequestMessage>(recipient2, Receive2);

Просмотреть файл

@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Linq;
using System.Reflection;
using Microsoft.Toolkit.Mvvm.Messaging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -13,9 +15,11 @@ namespace UnitTests.Mvvm
{
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_UnregisterRecipientWithMessageType()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_UnregisterRecipientWithMessageType(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
messenger.Unregister<MessageA>(recipient);
@ -23,9 +27,11 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_UnregisterRecipientWithMessageTypeAndToken()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_UnregisterRecipientWithMessageTypeAndToken(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
messenger.Unregister<MessageA, string>(recipient, nameof(MessageA));
@ -33,9 +39,11 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_UnregisterRecipientWithToken()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_UnregisterRecipientWithToken(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
messenger.UnregisterAll(recipient, nameof(MessageA));
@ -43,9 +51,11 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_UnregisterRecipientWithRecipient()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_UnregisterRecipientWithRecipient(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
messenger.UnregisterAll(recipient);
@ -53,12 +63,14 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_RegisterAndUnregisterRecipientWithMessageType()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_RegisterAndUnregisterRecipientWithMessageType(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
messenger.Register<MessageA>(recipient, m => { });
messenger.Register<MessageA>(recipient, (r, m) => { });
messenger.Unregister<MessageA>(recipient);
@ -67,12 +79,14 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_RegisterAndUnregisterRecipientWithMessageTypeAndToken()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_RegisterAndUnregisterRecipientWithMessageTypeAndToken(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
messenger.Register<MessageA, string>(recipient, nameof(MessageA), m => { });
messenger.Register<MessageA, string>(recipient, nameof(MessageA), (r, m) => { });
messenger.Unregister<MessageA, string>(recipient, nameof(MessageA));
@ -81,12 +95,14 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_RegisterAndUnregisterRecipientWithToken()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_RegisterAndUnregisterRecipientWithToken(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
messenger.Register<MessageA, string>(recipient, nameof(MessageA), m => { });
messenger.Register<MessageA, string>(recipient, nameof(MessageA), (r, m) => { });
messenger.UnregisterAll(recipient, nameof(MessageA));
@ -95,12 +111,14 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_RegisterAndUnregisterRecipientWithRecipient()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_RegisterAndUnregisterRecipientWithRecipient(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
messenger.Register<MessageA, string>(recipient, nameof(MessageA), m => { });
messenger.Register<MessageA, string>(recipient, nameof(MessageA), (r, m) => { });
messenger.UnregisterAll(recipient);
@ -109,120 +127,145 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IsRegistered_Register_Send_UnregisterOfTMessage_WithNoToken()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_IsRegistered_Register_Send_UnregisterOfTMessage_WithNoToken(Type type)
{
var messenger = (IMessenger)Activator.CreateInstance(type);
object a = new object();
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA>(a));
Assert.IsFalse(messenger.IsRegistered<MessageA>(a));
object recipient = null;
string result = null;
messenger.Register<MessageA>(a, (r, m) =>
{
recipient = r;
result = m.Text;
});
Assert.IsTrue(messenger.IsRegistered<MessageA>(a));
messenger.Send(new MessageA { Text = nameof(MessageA) });
Assert.AreSame(recipient, a);
Assert.AreEqual(result, nameof(MessageA));
messenger.Unregister<MessageA>(a);
Assert.IsFalse(messenger.IsRegistered<MessageA>(a));
recipient = null;
result = null;
messenger.Send(new MessageA { Text = nameof(MessageA) });
Assert.IsNull(recipient);
Assert.IsNull(result);
}
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_IsRegistered_Register_Send_UnregisterRecipient_WithNoToken(Type type)
{
var messenger = (IMessenger)Activator.CreateInstance(type);
object a = new object();
Assert.IsFalse(messenger.IsRegistered<MessageA>(a));
string result = null;
Messenger.Default.Register<MessageA>(a, m => result = m.Text);
messenger.Register<MessageA>(a, (r, m) => result = m.Text);
Assert.IsTrue(Messenger.Default.IsRegistered<MessageA>(a));
Assert.IsTrue(messenger.IsRegistered<MessageA>(a));
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) });
messenger.Send(new MessageA { Text = nameof(MessageA) });
Assert.AreEqual(result, nameof(MessageA));
Messenger.Default.Unregister<MessageA>(a);
messenger.UnregisterAll(a);
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA>(a));
Assert.IsFalse(messenger.IsRegistered<MessageA>(a));
result = null;
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) });
messenger.Send(new MessageA { Text = nameof(MessageA) });
Assert.IsNull(result);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IsRegistered_Register_Send_UnregisterRecipient_WithNoToken()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_IsRegistered_Register_Send_UnregisterOfTMessage_WithToken(Type type)
{
var messenger = (IMessenger)Activator.CreateInstance(type);
object a = new object();
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA>(a));
Assert.IsFalse(messenger.IsRegistered<MessageA>(a));
string result = null;
Messenger.Default.Register<MessageA>(a, m => result = m.Text);
messenger.Register<MessageA, string>(a, nameof(MessageA), (r, m) => result = m.Text);
Assert.IsTrue(Messenger.Default.IsRegistered<MessageA>(a));
Assert.IsTrue(messenger.IsRegistered<MessageA, string>(a, nameof(MessageA)));
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) });
messenger.Send(new MessageA { Text = nameof(MessageA) }, nameof(MessageA));
Assert.AreEqual(result, nameof(MessageA));
Messenger.Default.UnregisterAll(a);
messenger.Unregister<MessageA, string>(a, nameof(MessageA));
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA>(a));
Assert.IsFalse(messenger.IsRegistered<MessageA, string>(a, nameof(MessageA)));
result = null;
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) });
messenger.Send(new MessageA { Text = nameof(MessageA) }, nameof(MessageA));
Assert.IsNull(result);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IsRegistered_Register_Send_UnregisterOfTMessage_WithToken()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_DuplicateRegistrationWithMessageType(Type type)
{
object a = new object();
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA>(a));
string result = null;
Messenger.Default.Register<MessageA, string>(a, nameof(MessageA), m => result = m.Text);
Assert.IsTrue(Messenger.Default.IsRegistered<MessageA, string>(a, nameof(MessageA)));
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) }, nameof(MessageA));
Assert.AreEqual(result, nameof(MessageA));
Messenger.Default.Unregister<MessageA, string>(a, nameof(MessageA));
Assert.IsFalse(Messenger.Default.IsRegistered<MessageA, string>(a, nameof(MessageA)));
result = null;
Messenger.Default.Send(new MessageA { Text = nameof(MessageA) }, nameof(MessageA));
Assert.IsNull(result);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_DuplicateRegistrationWithMessageType()
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
messenger.Register<MessageA>(recipient, m => { });
messenger.Register<MessageA>(recipient, (r, m) => { });
Assert.ThrowsException<InvalidOperationException>(() =>
{
messenger.Register<MessageA>(recipient, m => { });
messenger.Register<MessageA>(recipient, (r, m) => { });
});
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_DuplicateRegistrationWithMessageTypeAndToken()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_DuplicateRegistrationWithMessageTypeAndToken(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new object();
messenger.Register<MessageA, string>(recipient, nameof(MessageA), m => { });
messenger.Register<MessageA, string>(recipient, nameof(MessageA), (r, m) => { });
Assert.ThrowsException<InvalidOperationException>(() =>
{
messenger.Register<MessageA, string>(recipient, nameof(MessageA), m => { });
messenger.Register<MessageA, string>(recipient, nameof(MessageA), (r, m) => { });
});
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IRecipient_NoMessages()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_IRecipient_NoMessages(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new RecipientWithNoMessages();
messenger.RegisterAll(recipient);
@ -233,9 +276,11 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IRecipient_SomeMessages_NoToken()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_IRecipient_SomeMessages_NoToken(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new RecipientWithSomeMessages();
messenger.RegisterAll(recipient);
@ -264,9 +309,11 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_Messenger_IRecipient_SomeMessages_WithToken()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_IRecipient_SomeMessages_WithToken(Type type)
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new RecipientWithSomeMessages();
var token = nameof(Test_Messenger_IRecipient_SomeMessages_WithToken);
@ -297,12 +344,182 @@ namespace UnitTests.Mvvm
Assert.IsFalse(messenger.IsRegistered<MessageB>(recipient));
}
public sealed class RecipientWithNoMessages
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_RegisterWithTypeParameter(Type type)
{
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new RecipientWithNoMessages { Number = 42 };
int number = 0;
messenger.Register<RecipientWithNoMessages, MessageA>(recipient, (r, m) => number = r.Number);
messenger.Send<MessageA>();
Assert.AreEqual(number, 42);
}
public sealed class RecipientWithSomeMessages
: IRecipient<MessageA>, ICloneable, IRecipient<MessageB>
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger), false)]
[DataRow(typeof(WeakReferenceMessenger), true)]
public void Test_Messenger_Collect_Test(Type type, bool isWeak)
{
var messenger = (IMessenger)Activator.CreateInstance(type);
WeakReference weakRecipient;
void Test()
{
var recipient = new RecipientWithNoMessages { Number = 42 };
weakRecipient = new WeakReference(recipient);
messenger.Register<MessageA>(recipient, (r, m) => { });
Assert.IsTrue(messenger.IsRegistered<MessageA>(recipient));
Assert.IsTrue(weakRecipient.IsAlive);
GC.KeepAlive(recipient);
}
Test();
GC.Collect();
Assert.AreEqual(!isWeak, weakRecipient.IsAlive);
}
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_Reset(Type type)
{
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new RecipientWithSomeMessages();
messenger.RegisterAll(recipient);
Assert.IsTrue(messenger.IsRegistered<MessageA>(recipient));
Assert.IsTrue(messenger.IsRegistered<MessageB>(recipient));
messenger.Reset();
Assert.IsFalse(messenger.IsRegistered<MessageA>(recipient));
Assert.IsFalse(messenger.IsRegistered<MessageB>(recipient));
}
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_Default(Type type)
{
PropertyInfo defaultInfo = type.GetProperty("Default");
var default1 = defaultInfo!.GetValue(null);
var default2 = defaultInfo!.GetValue(null);
Assert.IsNotNull(default1);
Assert.IsNotNull(default2);
Assert.AreSame(default1, default2);
}
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_Cleanup(Type type)
{
var messenger = (IMessenger)Activator.CreateInstance(type);
var recipient = new RecipientWithSomeMessages();
messenger.Register<MessageA>(recipient);
Assert.IsTrue(messenger.IsRegistered<MessageA>(recipient));
void Test()
{
var recipient2 = new RecipientWithSomeMessages();
messenger.Register<MessageB>(recipient2);
Assert.IsTrue(messenger.IsRegistered<MessageB>(recipient2));
GC.KeepAlive(recipient2);
}
Test();
GC.Collect();
// Here we just check that calling Cleanup doesn't alter the state
// of the messenger. This method shouldn't really do anything visible
// to consumers, it's just a way for messengers to compact their data.
messenger.Cleanup();
Assert.IsTrue(messenger.IsRegistered<MessageA>(recipient));
}
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_Messenger_ManyRecipients(Type type)
{
var messenger = (IMessenger)Activator.CreateInstance(type);
void Test()
{
var recipients = Enumerable.Range(0, 512).Select(_ => new RecipientWithSomeMessages()).ToArray();
foreach (var recipient in recipients)
{
messenger.RegisterAll(recipient);
}
foreach (var recipient in recipients)
{
Assert.IsTrue(messenger.IsRegistered<MessageA>(recipient));
Assert.IsTrue(messenger.IsRegistered<MessageB>(recipient));
}
messenger.Send<MessageA>();
messenger.Send<MessageB>();
messenger.Send<MessageB>();
foreach (var recipient in recipients)
{
Assert.AreEqual(recipient.As, 1);
Assert.AreEqual(recipient.Bs, 2);
}
foreach (ref var recipient in recipients.AsSpan())
{
recipient = null;
}
}
Test();
GC.Collect();
// Just invoke a final cleanup to improve coverage, this is unrelated to this test in particular
messenger.Cleanup();
}
public sealed class RecipientWithNoMessages
{
public int Number { get; set; }
}
public sealed class RecipientWithSomeMessages :
IRecipient<MessageA>,
IRecipient<MessageB>,
ICloneable
{
public int As { get; private set; }

Просмотреть файл

@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Messaging;
using Microsoft.Toolkit.Mvvm.Messaging.Messages;
@ -14,9 +15,12 @@ namespace UnitTests.Mvvm
{
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableRecipient_Activation()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_ObservableRecipient_Activation(Type type)
{
var viewmodel = new SomeRecipient<int>();
var messenger = (IMessenger)Activator.CreateInstance(type);
var viewmodel = new SomeRecipient<int>(messenger);
Assert.IsFalse(viewmodel.IsActivatedCheck);
@ -33,18 +37,11 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableRecipient_Defaults()
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_ObservableRecipient_IsSame(Type type)
{
var viewmodel = new SomeRecipient<int>();
Assert.AreSame(viewmodel.CurrentMessenger, Messenger.Default);
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableRecipient_Injection()
{
var messenger = new Messenger();
var messenger = (IMessenger)Activator.CreateInstance(type);
var viewmodel = new SomeRecipient<int>(messenger);
Assert.AreSame(viewmodel.CurrentMessenger, messenger);
@ -52,14 +49,37 @@ namespace UnitTests.Mvvm
[TestCategory("Mvvm")]
[TestMethod]
public void Test_ObservableRecipient_Broadcast()
public void Test_ObservableRecipient_Default()
{
var messenger = new Messenger();
var viewmodel = new SomeRecipient<int>();
Assert.AreSame(viewmodel.CurrentMessenger, WeakReferenceMessenger.Default);
}
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_ObservableRecipient_Injection(Type type)
{
var messenger = (IMessenger)Activator.CreateInstance(type);
var viewmodel = new SomeRecipient<int>(messenger);
Assert.AreSame(viewmodel.CurrentMessenger, messenger);
}
[TestCategory("Mvvm")]
[TestMethod]
[DataRow(typeof(StrongReferenceMessenger))]
[DataRow(typeof(WeakReferenceMessenger))]
public void Test_ObservableRecipient_Broadcast(Type type)
{
var messenger = (IMessenger)Activator.CreateInstance(type);
var viewmodel = new SomeRecipient<int>(messenger);
PropertyChangedMessage<int> message = null;
messenger.Register<PropertyChangedMessage<int>>(messenger, m => message = m);
messenger.Register<PropertyChangedMessage<int>>(messenger, (r, m) => message = m);
viewmodel.Data = 42;
@ -97,7 +117,7 @@ namespace UnitTests.Mvvm
{
IsActivatedCheck = true;
Messenger.Register<SampleMessage>(this, m => { });
Messenger.Register<SampleMessage>(this, (r, m) => { });
}
protected override void OnDeactivated()

Просмотреть файл

@ -35,7 +35,7 @@ namespace UnitTests.Mvvm
Assert.AreSame(args.Item1, command);
Assert.AreSame(args.Item2, EventArgs.Empty);
command.Execute("Hello");
command.Execute((object)"Hello");
Assert.AreEqual(text, "Hello");
@ -57,7 +57,7 @@ namespace UnitTests.Mvvm
Assert.ThrowsException<InvalidCastException>(() => command.CanExecute(new object()));
command.Execute("Hello");
command.Execute((object)"Hello");
Assert.AreEqual(text, "Hello");
@ -65,5 +65,21 @@ namespace UnitTests.Mvvm
Assert.AreEqual(text, "Hello");
}
[TestCategory("Mvvm")]
[TestMethod]
public void Test_RelayCommand_NullWithValueType()
{
int n = 0;
var command = new RelayCommand<int>(i => n = i);
// Special case for null value types
Assert.IsTrue(command.CanExecute(null));
command = new RelayCommand<int>(i => n = i, i => i > 0);
Assert.ThrowsException<NullReferenceException>(() => command.CanExecute(null));
}
}
}

Просмотреть файл

@ -28,6 +28,7 @@
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_ObservableValidator.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_ObservableObject.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_AsyncRelayCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_AsyncRelayCommand{T}.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_RelayCommand.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_RelayCommand{T}.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Mvvm\Test_ObservableRecipient.cs" />

Просмотреть файл

@ -0,0 +1,65 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Foundation;
using Microsoft.Toolkit.Uwp.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Extensions
{
[TestClass]
public class Test_PointExtensions
{
[TestCategory("PointExtensions")]
[TestMethod]
[DataRow(0d, 0d, 0, 0)]
[DataRow(0d, 0d, 22, 6.89d)]
[DataRow(3.14d, 6.55f, 3838d, 3.24724928d)]
[DataRow(double.MinValue, double.Epsilon, 22, 0.3248d)]
public static void Test_PointExtensions_ToRect_FromWidthHeight(double width, double height, int x, int y)
{
Point p = new Point(x, y);
Rect
a = p.ToRect(width, height),
b = new Rect(x, y, width, height);
Assert.AreEqual(a, b);
}
[TestCategory("SizeExtensions")]
[TestMethod]
[DataRow(0d, 0d, 0, 0)]
[DataRow(0d, 0d, 22, 6.89d)]
[DataRow(3.14d, 6.55f, 3838d, 3.24724928d)]
[DataRow(double.MinValue, double.Epsilon, 22, 0.3248d)]
public static void Test_PointExtensions_ToRect_FromPoint(double width, double height, int x, int y)
{
Point
p1 = new Point(x, y),
p2 = new Point(x + width, y + height);
Rect
a = p1.ToRect(p2),
b = new Rect(p1, p2);
Assert.AreEqual(a, b);
}
[TestCategory("SizeExtensions")]
[TestMethod]
[DataRow(0d, 0d, 0, 0)]
[DataRow(0d, 0d, 22, 6.89d)]
[DataRow(3.14d, 6.55f, 3838d, 3.24724928d)]
[DataRow(double.MinValue, double.Epsilon, 22, 0.3248d)]
public static void Test_PointExtensions_ToRect_FromSize(double width, double height, int x, int y)
{
Point p = new Point(x, y);
Size s = new Size(width, height);
Rect
a = p.ToRect(s),
b = new Rect(p, s);
Assert.AreEqual(a, b);
}
}
}

Просмотреть файл

@ -0,0 +1,62 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Foundation;
using Microsoft.Toolkit.Uwp.Extensions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace UnitTests.Extensions
{
[TestClass]
public class Test_SizeExtensions
{
[TestCategory("SizeExtensions")]
[TestMethod]
[DataRow(0d, 0d)]
[DataRow(3.14d, 6.55f)]
[DataRow(double.MinValue, double.Epsilon)]
public static void Test_SizeExtensions_ToRect_FromSize(double width, double height)
{
Size s = new Size(width, height);
Rect
a = s.ToRect(),
b = new Rect(0, 0, s.Width, s.Height);
Assert.AreEqual(a, b);
}
[TestCategory("SizeExtensions")]
[TestMethod]
[DataRow(0d, 0d, 0, 0)]
[DataRow(0d, 0d, 22, 6.89d)]
[DataRow(3.14d, 6.55f, 3838d, 3.24724928d)]
[DataRow(double.MinValue, double.Epsilon, 22, 0.3248d)]
public static void Test_SizeExtensions_ToRect_FromSizeAndPosition(double width, double height, int x, int y)
{
Size s = new Size(width, height);
Rect
a = s.ToRect(x, y),
b = new Rect(x, y, s.Width, s.Height);
Assert.AreEqual(a, b);
}
[TestCategory("SizeExtensions")]
[TestMethod]
[DataRow(0d, 0d, 0, 0)]
[DataRow(0d, 0d, 22, 6.89d)]
[DataRow(3.14d, 6.55f, 3838d, 3.24724928d)]
[DataRow(double.MinValue, double.Epsilon, 22, 0.3248d)]
public static void Test_SizeExtensions_ToRect_FromSizeAndPoint(double width, double height, int x, int y)
{
Point p = new Point(x, y);
Size s = new Size(width, height);
Rect
a = s.ToRect(p),
b = new Rect(p, s);
Assert.AreEqual(a, b);
}
}
}

Просмотреть файл

@ -138,6 +138,8 @@
<Compile Include="Converters\Test_StringFormatConverter.cs" />
<Compile Include="Converters\Test_TypeToObjectConverter.cs" />
<Compile Include="Extensions\Helpers\ObjectWithNullableBoolProperty.cs" />
<Compile Include="Extensions\Test_PointExtensions.cs" />
<Compile Include="Extensions\Test_SizeExtensions.cs" />
<Compile Include="Extensions\Test_BitmapIconExtensionMarkupExtension.cs" />
<Compile Include="Extensions\Test_FontIconSourceExtensionMarkupExtension.cs" />
<Compile Include="Extensions\Test_FontIconExtensionMarkupExtension.cs" />