mirror of
https://github.com/chylex/.NET-Community-Toolkit.git
synced 2025-06-02 05:34:09 +02:00

- Remove unused namespaces - Remove unused attributes - Remove unnecessary #nullable enable directives - Sort using directives - Simplify names
360 lines
15 KiB
C#
360 lines
15 KiB
C#
// 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.Collections.Generic;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Threading;
|
|
using Microsoft.Collections.Extensions;
|
|
using CommunityToolkit.Mvvm.Messaging.Internals;
|
|
#if NETSTANDARD2_0
|
|
using RecipientsTable = CommunityToolkit.Mvvm.Messaging.Internals.ConditionalWeakTable2<object, Microsoft.Collections.Extensions.IDictionarySlim>;
|
|
#else
|
|
using RecipientsTable = System.Runtime.CompilerServices.ConditionalWeakTable<object, Microsoft.Collections.Extensions.IDictionarySlim>;
|
|
#endif
|
|
|
|
namespace CommunityToolkit.Mvvm.Messaging;
|
|
|
|
/// <summary>
|
|
/// A class providing a reference implementation for the <see cref="IMessenger"/> interface.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// 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.
|
|
/// </para>
|
|
/// <para>
|
|
/// The <see cref="WeakReferenceMessenger"/> type will automatically perform internal trimming when
|
|
/// full GC collections are invoked, so calling <see cref="Cleanup"/> manually is not necessary to
|
|
/// ensure that on average the internal data structures are as trimmed and compact as possible.
|
|
/// </para>
|
|
/// </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();
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="WeakReferenceMessenger"/> class.
|
|
/// </summary>
|
|
public WeakReferenceMessenger()
|
|
{
|
|
// Proxy function for the GC callback. This needs to be static and to take the target instance as
|
|
// an input parameter in order to avoid rooting it from the Gen2GcCallback object invoking it.
|
|
static void Gen2GcCallbackProxy(object target)
|
|
{
|
|
((WeakReferenceMessenger)target).CleanupWithNonBlockingLock();
|
|
}
|
|
|
|
// Register an automatic GC callback to trigger a non-blocking cleanup. This will ensure that the
|
|
// current messenger instance is trimmed and without leftover recipient maps that are no longer used.
|
|
// This is necessary (as in, some form of cleanup, either explicit or automatic like in this case)
|
|
// because the ConditionalWeakTable<TKey, TValue> instances will just remove key-value pairs on their
|
|
// own as soon as a key (ie. a recipient) is collected, causing their own keys (ie. the Type2 instances
|
|
// mapping to each conditional table for a pair of message and token types) to potentially remain in the
|
|
// root mapping structure but without any remaining recipients actually registered there, which just
|
|
// adds unnecessary overhead when trying to enumerate recipients during broadcasting operations later on.
|
|
Gen2GcCallback.Register(Gen2GcCallbackProxy, this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the default <see cref="WeakReferenceMessenger"/> instance.
|
|
/// </summary>
|
|
public static WeakReferenceMessenger Default { get; } = new();
|
|
|
|
/// <inheritdoc/>
|
|
public bool IsRegistered<TMessage, TToken>(object recipient, TToken token)
|
|
where TMessage : class
|
|
where TToken : IEquatable<TToken>
|
|
{
|
|
lock (this.recipientsMap)
|
|
{
|
|
Type2 type2 = new(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(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
|
|
DictionarySlim<TToken, object>? map = Unsafe.As<DictionarySlim<TToken, object>>(mapping.GetValue(recipient, static _ => new DictionarySlim<TToken, object>()));
|
|
|
|
// Add the new registration entry
|
|
ref object? registeredHandler = ref map.GetOrAddValueRef(token);
|
|
|
|
if (registeredHandler is not null)
|
|
{
|
|
ThrowInvalidOperationExceptionForDuplicateRegistration();
|
|
}
|
|
|
|
// Store the input handler
|
|
registeredHandler = handler;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void UnregisterAll(object recipient)
|
|
{
|
|
lock (this.recipientsMap)
|
|
{
|
|
DictionarySlim<Type2, RecipientsTable>.Enumerator 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 responsibility 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)
|
|
{
|
|
DictionarySlim<Type2, RecipientsTable>.Enumerator 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void Unregister<TMessage, TToken>(object recipient, TToken token)
|
|
where TMessage : class
|
|
where TToken : IEquatable<TToken>
|
|
{
|
|
lock (this.recipientsMap)
|
|
{
|
|
Type2 type2 = new(typeof(TMessage), typeof(TToken));
|
|
|
|
// Get the target mapping table for the combination of message and token types,
|
|
// and remove the handler with a matching token (the entire map), if present.
|
|
if (this.recipientsMap.TryGetValue(type2, out RecipientsTable? value) &&
|
|
value.TryGetValue(recipient, out IDictionarySlim? mapping))
|
|
{
|
|
_ = Unsafe.As<DictionarySlim<TToken, object>>(mapping).TryRemove(token);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public TMessage Send<TMessage, TToken>(TMessage message, TToken token)
|
|
where TMessage : class
|
|
where TToken : IEquatable<TToken>
|
|
{
|
|
ArrayPoolBufferWriter<object> bufferWriter;
|
|
int i = 0;
|
|
|
|
lock (this.recipientsMap)
|
|
{
|
|
Type2 type2 = new(typeof(TMessage), typeof(TToken));
|
|
|
|
// Try to get the target table
|
|
if (!this.recipientsMap.TryGetValue(type2, out RecipientsTable? table))
|
|
{
|
|
return message;
|
|
}
|
|
|
|
bufferWriter = 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)
|
|
{
|
|
DictionarySlim<TToken, object>? map = Unsafe.As<DictionarySlim<TToken, object>>(pair.Value);
|
|
|
|
if (map.TryGetValue(token, out object? handler))
|
|
{
|
|
bufferWriter.Add(handler);
|
|
bufferWriter.Add(pair.Key);
|
|
i++;
|
|
}
|
|
}
|
|
}
|
|
|
|
try
|
|
{
|
|
ReadOnlySpan<object> pairs = bufferWriter.Span;
|
|
|
|
for (int j = 0; j < i; j++)
|
|
{
|
|
// 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>>(pairs[2 * j])(pairs[(2 * j) + 1], message);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
bufferWriter.Dispose();
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void Cleanup()
|
|
{
|
|
lock (this.recipientsMap)
|
|
{
|
|
CleanupWithoutLock();
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
public void Reset()
|
|
{
|
|
lock (this.recipientsMap)
|
|
{
|
|
this.recipientsMap.Clear();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes a cleanup without locking the current instance. This method has to be
|
|
/// invoked when a lock on <see cref="recipientsMap"/> has already been acquired.
|
|
/// </summary>
|
|
private void CleanupWithNonBlockingLock()
|
|
{
|
|
object lockObject = this.recipientsMap;
|
|
bool lockTaken = false;
|
|
|
|
try
|
|
{
|
|
Monitor.TryEnter(lockObject, ref lockTaken);
|
|
|
|
if (lockTaken)
|
|
{
|
|
CleanupWithoutLock();
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (lockTaken)
|
|
{
|
|
Monitor.Exit(lockObject);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes a cleanup without locking the current instance. This method has to be
|
|
/// invoked when a lock on <see cref="recipientsMap"/> has already been acquired.
|
|
/// </summary>
|
|
private void CleanupWithoutLock()
|
|
{
|
|
using ArrayPoolBufferWriter<Type2> type2s = ArrayPoolBufferWriter<Type2>.Create();
|
|
using ArrayPoolBufferWriter<object> emptyRecipients = ArrayPoolBufferWriter<object>.Create();
|
|
|
|
DictionarySlim<Type2, RecipientsTable>.Enumerator 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);
|
|
}
|
|
}
|
|
|
|
/// <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");
|
|
}
|
|
}
|