Skip to content

WeakReferenceMessenger automatic cleanup #4050

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
29 changes: 29 additions & 0 deletions Microsoft.Toolkit.Mvvm/Attributes/NotNullWhenAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// 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.

#if NETSTANDARD2_0

namespace System.Diagnostics.CodeAnalysis
{
/// <summary>
/// Specifies that when a method returns <see cref="ReturnValue"/>, the parameter will not be null even if the corresponding type allows it.
/// </summary>
/// <remarks>Internal copy from the BCL attribute.</remarks>
[AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
internal sealed class NotNullWhenAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="NotNullWhenAttribute"/> class.
/// </summary>
/// <param name="returnValue">The return value condition. If the method returns this value, the associated parameter will not be null.</param>
public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;

/// <summary>
/// Gets a value indicating whether the annotated variable is not <see langword="null"/>.
/// </summary>
public bool ReturnValue { get; }
}
}

#endif
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ protected virtual void Broadcast<T>(T oldValue, T newValue, string? propertyName
{
PropertyChangedMessage<T> message = new(this, propertyName, oldValue, newValue);

Messenger.Send(message);
_ = Messenger.Send(message);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ static Dictionary<string, string> GetDisplayNames(Type type)

// This method replicates the logic of DisplayName and GetDisplayName from the
// ValidationContext class. See the original source in the BCL for more details.
DisplayNamesMap.GetValue(GetType(), static t => GetDisplayNames(t)).TryGetValue(propertyName, out string? displayName);
_ = DisplayNamesMap.GetValue(GetType(), static t => GetDisplayNames(t)).TryGetValue(propertyName, out string? displayName);

return displayName ?? propertyName;
}
Expand Down
2 changes: 1 addition & 1 deletion Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public bool CanExecute(object? parameter)
/// <inheritdoc/>
public void Execute(object? parameter)
{
ExecuteAsync(parameter);
_ = ExecuteAsync(parameter);
}

/// <inheritdoc/>
Expand Down
4 changes: 2 additions & 2 deletions Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,13 +144,13 @@ public bool CanExecute(object? parameter)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Execute(T? parameter)
{
ExecuteAsync(parameter);
_ = ExecuteAsync(parameter);
}

/// <inheritdoc/>
public void Execute(object? parameter)
{
ExecuteAsync((T?)parameter);
_ = ExecuteAsync((T?)parameter);
}

/// <inheritdoc/>
Expand Down
107 changes: 107 additions & 0 deletions Microsoft.Toolkit.Mvvm/Messaging/Internals/ArrayPoolBufferWriter{T}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// 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.Diagnostics.Contracts;
using System.Runtime.CompilerServices;

namespace Microsoft.Toolkit.Mvvm.Messaging.Internals
{
/// <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>
internal 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>
/// <returns>A new <see cref="ArrayPoolBufferWriter{T}"/> instance.</returns>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ArrayPoolBufferWriter<T> Create()
{
return new() { 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// 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.

#if NETSTANDARD2_0

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;

namespace Microsoft.Toolkit.Mvvm.Messaging.Internals
{
/// <summary>
/// A wrapper for <see cref="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 ConditionalWeakTable2<TKey, TValue>
where TKey : class
where TValue : class?
{
/// <summary>
/// The underlying <see cref="ConditionalWeakTable{TKey,TValue}"/> instance.
/// </summary>
private readonly ConditionalWeakTable<TKey, TValue> table = new();

/// <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 = new();

/// <inheritdoc cref="ConditionalWeakTable{TKey,TValue}.TryGetValue"/>
public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value)
{
return this.table.TryGetValue(key, out value);
}

/// <inheritdoc cref="ConditionalWeakTable{TKey,TValue}.GetValue"/>
public TValue GetValue(TKey key, 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="ConditionalWeakTable{TKey,TValue}.Remove"/>
public bool Remove(TKey key)
{
return this.table.Remove(key);
}

/// <inheritdoc cref="IEnumerable{T}.GetEnumerator"/>
[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator GetEnumerator() => new(this);

/// <summary>
/// A custom enumerator that traverses items in a <see cref="ConditionalWeakTable{TKey, TValue}"/> instance.
/// </summary>
public ref struct Enumerator
{
/// <summary>
/// The owner <see cref="ConditionalWeakTable2{TKey, TValue}"/> instance for the enumerator.
/// </summary>
private readonly ConditionalWeakTable2<TKey, TValue> owner;

/// <summary>
/// The current <see cref="LinkedListNode{T}"/>, if any.
/// </summary>
private LinkedListNode<WeakReference<TKey>>? node;

/// <summary>
/// The current <see cref="KeyValuePair{TKey, TValue}"/> to return.
/// </summary>
private KeyValuePair<TKey, TValue> current;

/// <summary>
/// Indicates whether or not <see cref="MoveNext"/> has been called at least once.
/// </summary>
private bool isFirstMoveNextPending;

/// <summary>
/// Initializes a new instance of the <see cref="Enumerator"/> struct.
/// </summary>
/// <param name="owner">The owner <see cref="ConditionalWeakTable2{TKey, TValue}"/> instance for the enumerator.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Enumerator(ConditionalWeakTable2<TKey, TValue> owner)
{
this.owner = owner;
this.node = null;
this.current = default;
this.isFirstMoveNextPending = true;
}

/// <inheritdoc cref="System.Collections.IEnumerator.MoveNext"/>
public bool MoveNext()
{
LinkedListNode<WeakReference<TKey>>? node;

if (!isFirstMoveNextPending)
{
node = this.node!.Next;
}
else
{
node = this.owner.keys.First;

this.isFirstMoveNextPending = false;
}

while (node is not null)
{
LinkedListNode<WeakReference<TKey>>? nextNode = node.Next;

// Get the key and value for the current node
if (node.Value.TryGetTarget(out TKey? target) &&
this.owner.table.TryGetValue(target!, out TValue? value))
{
this.node = node;
this.current = new KeyValuePair<TKey, TValue>(target, value);

return true;
}
else
{
// If the current key has been collected, trim the list
this.owner.keys.Remove(node);
}

node = nextNode;
}

return false;
}

/// <inheritdoc cref="System.Collections.IEnumerator.MoveNext"/>
public readonly KeyValuePair<TKey, TValue> Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => this.current;
}
}
}
}

#endif
Loading