1
0
mirror of https://github.com/chylex/.NET-Community-Toolkit.git synced 2025-06-02 05:34:09 +02:00
.NET-Community-Toolkit/CommunityToolkit.Mvvm/Messaging/WeakReferenceMessenger.cs
Sergio Pedri 51c7a67c20 More code style improvements and tweaks
- Remove unused namespaces
- Remove unused attributes
- Remove unnecessary #nullable enable directives
- Sort using directives
- Simplify names
2021-11-01 22:29:46 +01:00

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");
}
}