.NET-Community-Toolkit/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs

443 lines
26 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using CommunityToolkit.Mvvm.Messaging.Internals;
namespace CommunityToolkit.Mvvm.Messaging;
/// <summary>
/// Extensions for the <see cref="IMessenger"/> type.
/// </summary>
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="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>
private static class MethodInfos
{
/// <summary>
/// The <see cref="MethodInfo"/> instance associated with <see cref="Register{TMessage,TToken}(IMessenger,IRecipient{TMessage},TToken)"/>.
/// </summary>
public static readonly MethodInfo RegisterIRecipient = new Action<IMessenger, IRecipient<object>, Unit>(Register).Method.GetGenericMethodDefinition();
}
/// <summary>
/// A non-generic version of <see cref="DiscoveredRecipients{TToken}"/>.
/// </summary>
private static class DiscoveredRecipients
{
/// <summary>
/// The <see cref="ConditionalWeakTable{TKey,TValue}"/> instance used to track the preloaded registration action for each recipient.
/// </summary>
public static readonly ConditionalWeakTable<Type, Action<IMessenger, object>?> RegistrationMethods = new();
}
/// <summary>
/// A class that acts as a static container to associate a <see cref="ConditionalWeakTable{TKey,TValue}"/> instance to each
/// <typeparamref name="TToken"/> type in use. This is done because we can only use a single type as key, but we need to track
/// associations of each recipient type also across different communication channels, each identified by a token.
/// Since the token is actually a compile-time parameter, we can use a wrapping class to let the runtime handle a different
/// instance for each generic type instantiation. This lets us only worry about the recipient type being inspected.
/// </summary>
/// <typeparam name="TToken">The token indicating what channel to use.</typeparam>
private static class DiscoveredRecipients<TToken>
where TToken : IEquatable<TToken>
{
/// <summary>
/// The <see cref="ConditionalWeakTable{TKey,TValue}"/> instance used to track the preloaded registration action for each recipient.
/// </summary>
public static readonly ConditionalWeakTable<Type, Action<IMessenger, object, TToken>> RegistrationMethods = new();
}
/// <summary>
/// Checks whether or not a given recipient has already been registered for a message.
/// </summary>
/// <typeparam name="TMessage">The type of message to check for the given recipient.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to check the registration.</param>
/// <param name="recipient">The target recipient to check the registration for.</param>
/// <returns>Whether or not <paramref name="recipient"/> has already been registered for the specified message.</returns>
/// <remarks>This method will use the default channel to check for the requested registration.</remarks>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/> or <paramref name="recipient"/> are <see langword="null"/>.</exception>
public static bool IsRegistered<TMessage>(this IMessenger messenger, object recipient)
where TMessage : class
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.ThrowIfNull(recipient);
return messenger.IsRegistered<TMessage, Unit>(recipient, default);
}
/// <summary>
/// Registers all declared message handlers for a given recipient, using the default channel.
/// </summary>
/// <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>
/// <remarks>See notes for <see cref="RegisterAll{TToken}(IMessenger,object,TToken)"/> for more info.</remarks>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/> or <paramref name="recipient"/> are <see langword="null"/>.</exception>
[RequiresUnreferencedCode(
"This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " +
"If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " +
"path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type.")]
public static void RegisterAll(this IMessenger messenger, object recipient)
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.ThrowIfNull(recipient);
// We use this method as a callback for the conditional weak table, which will handle
// thread-safety for us. This first callback will try to find a generated method for the
// target recipient type, and just invoke it to get the delegate to cache and use later.
[RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
static Action<IMessenger, object>? LoadRegistrationMethodsForType(Type recipientType)
{
if (recipientType.Assembly.GetType("CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType &&
extensionsType.GetMethod("CreateAllMessagesRegistrator", new[] { recipientType }) is MethodInfo methodInfo)
{
return (Action<IMessenger, object>)methodInfo.Invoke(null, new object?[] { null })!;
}
return null;
}
// Try to get the cached delegate, if the generator has run correctly
Action<IMessenger, object>? registrationAction = DiscoveredRecipients.RegistrationMethods.GetValue(
recipient.GetType(),
[RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] static (t) => LoadRegistrationMethodsForType(t));
if (registrationAction is not null)
{
registrationAction(messenger, recipient);
}
else
{
messenger.RegisterAll(recipient, default(Unit));
}
}
/// <summary>
/// Registers all declared message handlers for a given recipient.
/// </summary>
/// <typeparam name="TToken">The type of token to identify what channel to use to receive messages.</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">The token indicating what channel to use.</param>
/// <remarks>
/// This method will register all messages corresponding to the <see cref="IRecipient{TMessage}"/> interfaces
/// being implemented by <paramref name="recipient"/>. If none are present, this method will do nothing.
/// Note that unlike all other extensions, this method will use reflection to find the handlers to register.
/// Once the registration is complete though, the performance will be exactly the same as with handlers
/// registered directly through any of the other generic extensions for the <see cref="IMessenger"/> interface.
/// </remarks>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/>, <paramref name="recipient"/> or <paramref name="token"/> are <see langword="null"/>.</exception>
[RequiresUnreferencedCode(
"This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " +
"If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " +
"path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type.")]
public static void RegisterAll<TToken>(this IMessenger messenger, object recipient, TToken token)
where TToken : IEquatable<TToken>
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);
// We use this method as a callback for the conditional weak table, which will handle
// thread-safety for us. This first callback will try to find a generated method for the
// target recipient type, and just invoke it to get the delegate to cache and use later.
// In this case we also need to create a generic instantiation of the target method first.
[RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")]
static Action<IMessenger, object, TToken> LoadRegistrationMethodsForType(Type recipientType)
{
if (recipientType.Assembly.GetType("CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType &&
extensionsType.GetMethod("CreateAllMessagesRegistratorWithToken", new[] { recipientType }) is MethodInfo methodInfo)
{
MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(TToken));
return (Action<IMessenger, object, TToken>)genericMethodInfo.Invoke(null, new object?[] { null })!;
}
return LoadRegistrationMethodsForTypeFallback(recipientType);
}
// Fallback method when a generated method is not found.
// This method is only invoked once per recipient type and token type, so we're not
// worried about making it super efficient, and we can use the LINQ code for clarity.
// The LINQ codegen bloat is not really important for the same reason.
static Action<IMessenger, object, TToken> LoadRegistrationMethodsForTypeFallback(Type recipientType)
{
// Get the collection of validation methods
MethodInfo[] registrationMethods = (
from interfaceType in recipientType.GetInterfaces()
where interfaceType.IsGenericType &&
interfaceType.GetGenericTypeDefinition() == typeof(IRecipient<>)
let messageType = interfaceType.GenericTypeArguments[0]
select MethodInfos.RegisterIRecipient.MakeGenericMethod(messageType, typeof(TToken))).ToArray();
// Short path if there are no message handlers to register
if (registrationMethods.Length == 0)
{
return static (_, _, _) => { };
}
// Input parameters (IMessenger instance, non-generic recipient, token)
ParameterExpression arg0 = Expression.Parameter(typeof(IMessenger));
ParameterExpression arg1 = Expression.Parameter(typeof(object));
ParameterExpression arg2 = Expression.Parameter(typeof(TToken));
// Declare a local resulting from the (RecipientType)recipient cast
UnaryExpression inst1 = Expression.Convert(arg1, recipientType);
// We want a single compiled LINQ expression that executes the registration for all
// the declared message types in the input type. To do so, we create a block with the
// unrolled invocations for the individual message registration (for each IRecipient<T>).
// The code below will generate the following block expression:
// ===============================================================================
// {
// var inst1 = (RecipientType)arg1;
// IMessengerExtensions.Register<T0, TToken>(arg0, inst1, arg2);
// IMessengerExtensions.Register<T1, TToken>(arg0, inst1, arg2);
// ...
// IMessengerExtensions.Register<TN, TToken>(arg0, inst1, arg2);
// }
// ===============================================================================
// We also add an explicit object conversion to cast the input recipient type to
// the actual specific type, so that the exposed message handlers are accessible.
BlockExpression body = Expression.Block(
from registrationMethod in registrationMethods
select Expression.Call(registrationMethod, new Expression[]
{
arg0,
inst1,
arg2
}));
return Expression.Lambda<Action<IMessenger, object, TToken>>(body, arg0, arg1, arg2).Compile();
}
// Get or compute the registration method for the current recipient type.
// As in CommunityToolkit.Diagnostics.TypeExtensions.ToTypeString, we use a lambda
// expression instead of a method group expression to leverage the statically initialized
// delegate and avoid repeated allocations for each invocation of this method.
// For more info on this, see the related issue at https://github.com/dotnet/roslyn/issues/5835.
Action<IMessenger, object, TToken> registrationAction = DiscoveredRecipients<TToken>.RegistrationMethods.GetValue(
recipient.GetType(),
[RequiresUnreferencedCode("The type of the current instance cannot be statically discovered.")] static (t) => LoadRegistrationMethodsForType(t));
// Invoke the cached delegate to actually execute the message registration
registrationAction(messenger, recipient, token);
}
/// <summary>
/// Registers a recipient for a given type of message.
/// </summary>
/// <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>
/// <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>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/> or <paramref name="recipient"/> are <see langword="null"/>.</exception>
public static void Register<TMessage>(this IMessenger messenger, IRecipient<TMessage> recipient)
where TMessage : class
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.ThrowIfNull(recipient);
if (messenger is WeakReferenceMessenger weakReferenceMessenger)
{
weakReferenceMessenger.Register<TMessage, Unit>(recipient, default);
}
else
{
messenger.Register<IRecipient<TMessage>, TMessage, Unit>(recipient, default, static (r, m) => r.Receive(m));
}
}
/// <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 identify what channel to use to receive messages.</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">The token indicating what channel to use.</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>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/>, <paramref name="recipient"/> or <paramref name="token"/> are <see langword="null"/>.</exception>
public static void Register<TMessage, TToken>(this IMessenger messenger, IRecipient<TMessage> recipient, TToken token)
where TMessage : class
where TToken : IEquatable<TToken>
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);
if (messenger is WeakReferenceMessenger weakReferenceMessenger)
{
weakReferenceMessenger.Register(recipient, token);
}
else if (messenger is StrongReferenceMessenger strongReferenceMessenger)
{
strongReferenceMessenger.Register(recipient, token);
}
else
{
messenger.Register<IRecipient<TMessage>, TMessage, TToken>(recipient, token, static (r, m) => r.Receive(m));
}
}
/// <summary>
/// Registers a recipient for a given type of message.
/// </summary>
/// <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>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/>, <paramref name="recipient"/> or <paramref name="handler"/> are <see langword="null"/>.</exception>
public static void Register<TMessage>(this IMessenger messenger, object recipient, MessageHandler<object, TMessage> handler)
where TMessage : class
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.ThrowIfNull(handler);
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>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/>, <paramref name="recipient"/> or <paramref name="handler"/> are <see langword="null"/>.</exception>
public static void Register<TRecipient, TMessage>(this IMessenger messenger, TRecipient recipient, MessageHandler<TRecipient, TMessage> handler)
where TRecipient : class
where TMessage : class
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.ThrowIfNull(handler);
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>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/>, <paramref name="recipient"/> or <paramref name="handler"/> are <see langword="null"/>.</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>
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For<TToken>.ThrowIfNull(token);
ArgumentNullException.ThrowIfNull(handler);
messenger.Register(recipient, token, handler);
}
/// <summary>
/// Unregisters a recipient from messages of a given type.
/// </summary>
/// <typeparam name="TMessage">The type of message to stop receiving.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to unregister the recipient.</param>
/// <param name="recipient">The recipient to unregister.</param>
/// <remarks>
/// This method will unregister the target recipient only from the default channel.
/// If the recipient has no registered handler, this method does nothing.
/// </remarks>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/> or <paramref name="recipient"/> are <see langword="null"/>.</exception>
public static void Unregister<TMessage>(this IMessenger messenger, object recipient)
where TMessage : class
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.ThrowIfNull(recipient);
messenger.Unregister<TMessage, Unit>(recipient, default);
}
/// <summary>
/// Sends a message of the specified type to all registered recipients.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send the message.</param>
/// <returns>The message that has been sent.</returns>
/// <remarks>
/// This method is a shorthand for <see cref="Send{TMessage}(IMessenger,TMessage)"/> when the
/// message type exposes a parameterless constructor: it will automatically create
/// a new <typeparamref name="TMessage"/> instance and send that to its recipients.
/// </remarks>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/> is <see langword="null"/>.</exception>
public static TMessage Send<TMessage>(this IMessenger messenger)
where TMessage : class, new()
{
ArgumentNullException.ThrowIfNull(messenger);
return messenger.Send(new TMessage(), default(Unit));
}
/// <summary>
/// Sends a message of the specified type to all registered recipients.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send the message.</param>
/// <param name="message">The message to send.</param>
/// <returns>The message that was sent (ie. <paramref name="message"/>).</returns>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/> or <paramref name="message"/> are <see langword="null"/>.</exception>
public static TMessage Send<TMessage>(this IMessenger messenger, TMessage message)
where TMessage : class
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.ThrowIfNull(message);
return messenger.Send(message, default(Unit));
}
/// <summary>
/// Sends a message of the specified type to all registered recipients.
/// </summary>
/// <typeparam name="TMessage">The type of message to send.</typeparam>
/// <typeparam name="TToken">The type of token to identify what channel to use to send the message.</typeparam>
/// <param name="messenger">The <see cref="IMessenger"/> instance to use to send the message.</param>
/// <param name="token">The token indicating what channel to use.</param>
/// <returns>The message that has been sen.</returns>
/// <remarks>
/// This method will automatically create a new <typeparamref name="TMessage"/> instance
/// just like <see cref="Send{TMessage}(IMessenger)"/>, and then send it to the right recipients.
/// </remarks>
/// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/> or <paramref name="token"/> are <see langword="null"/>.</exception>
public static TMessage Send<TMessage, TToken>(this IMessenger messenger, TToken token)
where TMessage : class, new()
where TToken : IEquatable<TToken>
{
ArgumentNullException.ThrowIfNull(messenger);
ArgumentNullException.For<TToken>.ThrowIfNull(token);
return messenger.Send(new TMessage(), token);
}
}