From 1ea5c2e779edf8c518007ae1f1cca5b037a4812d Mon Sep 17 00:00:00 2001 From: Umoxfo Date: Tue, 2 Jun 2026 17:43:25 +0900 Subject: [PATCH] Reworked the generated `RegisterAll` extension ... to ensure it is AOT-safe 1. `IMessengerRegisterAllGenerator.Execute.cs` * Change the IMessenger RegisterAll source-generation to emit public extension methods in the `CommunityToolkit.Mvvm.Messaging` namespace (`RegisterAll(this IMessenger, TRecipient)` and `RegisterAll(this IMessenger, TRecipient, TToken)`) instead of producing factory delegates in an `CommunityToolkit.Mvvm.Messaging.__Internals` namespace. * Enable nullable instead of null check. * Update comments. 2. `IMessengerExtensions` class * Simplify the runtime fallback/reflection path. * Update `RequiresDynamicCode`/`RequiresUnreferencedCode` annotetion message and reference URLs. * Update documentation comments. 3. `ObservableRecipient` class * Update `RequiresDynamicCode`/`RequiresUnreferencedCode` annotetion message and reference URLs. * Update documentation comments. 4. Unit Tests * Add new generator test, update expectations, and call sites. --- .../IMessengerRegisterAllGenerator.Execute.cs | 182 ++++++------------ .../ComponentModel/ObservableRecipient.cs | 35 ++-- .../Messaging/IMessengerExtensions.cs | 105 ++++------ ....Mvvm.SourceGenerators.UnitTests.projitems | 1 + ...GeneratorsCodegen.IMessengerRegisterAll.cs | 74 +++++++ .../Test_ArgumentNullException.Messaging.cs | 4 +- .../Test_IRecipientGenerator.cs | 12 +- .../Test_ObservableRecipient.cs | 4 +- 8 files changed, 198 insertions(+), 219 deletions(-) create mode 100644 tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.IMessengerRegisterAll.cs diff --git a/src/CommunityToolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.Execute.cs b/src/CommunityToolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.Execute.cs index 8b36b73af..9c19e073e 100644 --- a/src/CommunityToolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.Execute.cs +++ b/src/CommunityToolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.Execute.cs @@ -115,11 +115,13 @@ public static CompilationUnitSyntax GetSyntax(bool isDynamicallyAccessedMembersA AttributeArgument(ParseExpression("global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods")))))); } + // Create it in the standard Messaging namespace + // so that it resolves naturally from the user's code. // This code produces a compilation unit as follows: // // // // #pragma warning disable - // namespace CommunityToolkit.Mvvm.Messaging.__Internals + // namespace CommunityToolkit.Mvvm.Messaging // { // // internal static partial class __IMessengerExtensions @@ -128,7 +130,7 @@ public static CompilationUnitSyntax GetSyntax(bool isDynamicallyAccessedMembersA // } return CompilationUnit().AddMembers( - NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.Messaging.__Internals")).WithLeadingTrivia(TriviaList( + NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.Messaging")).WithLeadingTrivia(TriviaList( Comment("// "), Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers( ClassDeclaration("__IMessengerExtensions").AddModifiers( @@ -146,151 +148,86 @@ public static CompilationUnitSyntax GetSyntax(bool isDynamicallyAccessedMembersA /// The generated instance for . public static CompilationUnitSyntax GetSyntax(RecipientInfo recipientInfo) { - // Create a static factory method to register all messages for a given recipient type. - // This follows the same pattern used in ObservableValidatorValidateAllPropertiesGenerator, - // with the same advantages mentioned there (type safety, more AOT-friendly, etc.). + // Create a static method to register all messages for a given recipient type. + // This pattern is used so that the library doesn't have to + // use GetType(...) and GetMethod(...) at runtime. + // This pattern eliminates the need for reflection entirely + // because type resolution occurs at "compile time" rather than at runtime. // This is the first overload being generated: a non-generic method doing the registration // with no tokens, which is the most common scenario and will help particularly with AOT. // This code will produce a syntax tree as follows: // // /// - // /// Creates a message registration stub for objects. + // /// Registers all declared message handlers for a given recipient, using the default channel. // /// - // /// Dummy parameter, only used to disambiguate the method signature. - // /// A message registration stub for objects. - // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - // [global::System.Obsolete("This method is not intended to be called directly by user code")] - // public static global::System.Action CreateAllMessagesRegistrator( _) + // /// + // public static void RegisterAll(this global::CommunityToolkit.Mvvm.Messaging.IMessenger messenger, recipient) // { - // static void RegisterAll(global::CommunityToolkit.Mvvm.Messaging.IMessenger messenger, object obj) - // { - // var recipient = ()obj; - // - // } - // - // return RegisterAll; + // // } MethodDeclarationSyntax defaultChannelMethodDeclaration = MethodDeclaration( - GenericName("global::System.Action").AddTypeArgumentListArguments( - IdentifierName("global::CommunityToolkit.Mvvm.Messaging.IMessenger"), - PredefinedType(Token(SyntaxKind.ObjectKeyword))), - Identifier("CreateAllMessagesRegistrator")).AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( - AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))) - .WithOpenBracketToken(Token(TriviaList( - Comment("/// "), - Comment($"/// Creates a message registration stub for objects."), - Comment("/// "), - Comment("/// Dummy parameter, only used to disambiguate the method signature."), - Comment($"/// A message registration stub for objects.")), SyntaxKind.OpenBracketToken, TriviaList())), - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( - AttributeArgument(LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal("This method is not intended to be called directly by user code"))))))).AddModifiers( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + Identifier("RegisterAll")) + .AddModifiers( Token(SyntaxKind.PublicKeyword), - Token(SyntaxKind.StaticKeyword)).AddParameterListParameters( - Parameter(Identifier("_")).WithType(IdentifierName(recipientInfo.TypeName))) - .WithBody(Block( - LocalFunctionStatement( - PredefinedType(Token(SyntaxKind.VoidKeyword)), - Identifier("RegisterAll")) - .AddModifiers(Token(SyntaxKind.StaticKeyword)) - .AddParameterListParameters( - Parameter(Identifier("messenger")).WithType(IdentifierName("global::CommunityToolkit.Mvvm.Messaging.IMessenger")), - Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword)))) - .WithBody(Block( - LocalDeclarationStatement( - VariableDeclaration(IdentifierName("var")) - .AddVariables( - VariableDeclarator(Identifier("recipient")) - .WithInitializer(EqualsValueClause( - CastExpression( - IdentifierName(recipientInfo.TypeName), - IdentifierName("obj"))))))) - .AddStatements(EnumerateRegistrationStatements(recipientInfo).ToArray())), - ReturnStatement(IdentifierName("RegisterAll")))); + Token(SyntaxKind.StaticKeyword)) + .WithLeadingTrivia(TriviaList( + Comment("/// "), + Comment($"/// Registers all declared message handlers for a given recipient, using the default channel."), + Comment("/// "), + Comment("/// "))) + .AddParameterListParameters( + Parameter(Identifier("messenger")) + .AddModifiers(Token(SyntaxKind.ThisKeyword)) + .WithType(IdentifierName("global::CommunityToolkit.Mvvm.Messaging.IMessenger")), + Parameter(Identifier("recipient")).WithType(IdentifierName(recipientInfo.TypeName))) + .WithBody(Block(EnumerateRegistrationStatements(recipientInfo).ToArray())); // Create a generic version that will support all other cases with custom tokens. - // Note: the generic overload has a different name to simplify the lookup with reflection. // This code will produce a syntax tree as follows: // // /// - // /// Creates a message registration stub for objects, with an input token. + // /// Registers all declared message handlers for a given recipient. // /// - // /// The type of tokens that will be used to register messages. - // /// Dummy parameter, only used to disambiguate the method signature. - // /// A message registration stub for objects. - // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - // [global::System.Obsolete("This method is not intended to be called directly by user code")] - // public static global::System.Action CreateAllMessagesRegistratorWithToken( _) - // where TToken : global::System.IEquatable + // /// + // public static void RegisterAll(global::CommunityToolkit.Mvvm.Messaging.IMessenger messenger, recipient, TToken token) + // where TToken : notnull, global::System.IEquatable // { - // static void RegisterAll(global::CommunityToolkit.Mvvm.Messaging.IMessenger messenger, object obj, TToken token) - // { - // var recipient = ()obj; - // - // } - // - // return RegisterAll; + // // } MethodDeclarationSyntax customChannelMethodDeclaration = MethodDeclaration( - GenericName("global::System.Action").AddTypeArgumentListArguments( - IdentifierName("global::CommunityToolkit.Mvvm.Messaging.IMessenger"), - PredefinedType(Token(SyntaxKind.ObjectKeyword)), - IdentifierName("TToken")), - Identifier("CreateAllMessagesRegistratorWithToken")).AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( - AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))) - .WithOpenBracketToken(Token(TriviaList( - Comment("/// "), - Comment($"/// Creates a message registration stub for objects."), - Comment("/// "), - Comment("/// The type of tokens that will be used to register messages."), - Comment("/// Dummy parameter, only used to disambiguate the method signature."), - Comment($"/// A message registration stub for objects.")), SyntaxKind.OpenBracketToken, TriviaList())), - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( - AttributeArgument(LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal("This method is not intended to be called directly by user code"))))))).AddModifiers( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + Identifier("RegisterAll")) + .AddModifiers( Token(SyntaxKind.PublicKeyword), - Token(SyntaxKind.StaticKeyword)).AddParameterListParameters( - Parameter(Identifier("_")).WithType(IdentifierName(recipientInfo.TypeName))) - .AddTypeParameterListParameters(TypeParameter("TToken")) - .AddConstraintClauses( - TypeParameterConstraintClause("TToken") - .AddConstraints(TypeConstraint(GenericName("global::System.IEquatable").AddTypeArgumentListArguments(IdentifierName("TToken"))))) - .WithBody(Block( - LocalFunctionStatement( - PredefinedType(Token(SyntaxKind.VoidKeyword)), - Identifier("RegisterAll")) - .AddModifiers(Token(SyntaxKind.StaticKeyword)) - .AddParameterListParameters( - Parameter(Identifier("messenger")).WithType(IdentifierName("global::CommunityToolkit.Mvvm.Messaging.IMessenger")), - Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword))), - Parameter(Identifier("token")).WithType(IdentifierName("TToken"))) - .WithBody(Block( - LocalDeclarationStatement( - VariableDeclaration(IdentifierName("var")) - .AddVariables( - VariableDeclarator(Identifier("recipient")) - .WithInitializer(EqualsValueClause( - CastExpression( - IdentifierName(recipientInfo.TypeName), - IdentifierName("obj"))))))) - .AddStatements(EnumerateRegistrationStatementsWithTokens(recipientInfo).ToArray())), - ReturnStatement(IdentifierName("RegisterAll")))); + Token(SyntaxKind.StaticKeyword)) + .WithLeadingTrivia(TriviaList( + Comment("/// "), + Comment($"/// Registers all declared message handlers for a given recipient."), + Comment("/// "), + Comment("/// "))) + .AddTypeParameterListParameters(TypeParameter("TToken")) + .AddConstraintClauses( + TypeParameterConstraintClause("TToken") + .AddConstraints( + TypeConstraint(IdentifierName("notnull")), + TypeConstraint(GenericName("global::System.IEquatable").AddTypeArgumentListArguments(IdentifierName("TToken"))))) + .AddParameterListParameters( + Parameter(Identifier("messenger")) + .AddModifiers(Token(SyntaxKind.ThisKeyword)) + .WithType(IdentifierName("global::CommunityToolkit.Mvvm.Messaging.IMessenger")), + Parameter(Identifier("recipient")).WithType(IdentifierName(recipientInfo.TypeName)), + Parameter(Identifier("token")).WithType(IdentifierName("TToken"))) + .WithBody(Block(EnumerateRegistrationStatementsWithTokens(recipientInfo).ToArray())); // This code produces a compilation unit as follows: // // // // #pragma warning disable - // namespace CommunityToolkit.Mvvm.Messaging.__Internals + // #nullable enable + // namespace CommunityToolkit.Mvvm.Messaging // { // /// // partial class __IMessengerExtensions @@ -300,9 +237,10 @@ public static CompilationUnitSyntax GetSyntax(RecipientInfo recipientInfo) // } return CompilationUnit().AddMembers( - NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.Messaging.__Internals")).WithLeadingTrivia(TriviaList( + NamespaceDeclaration(IdentifierName("CommunityToolkit.Mvvm.Messaging")).WithLeadingTrivia(TriviaList( Comment("// "), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers( + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)), + Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)))).AddMembers( ClassDeclaration("__IMessengerExtensions").AddModifiers( Token(TriviaList(Comment("/// ")), SyntaxKind.PartialKeyword, TriviaList())) .AddMembers(defaultChannelMethodDeclaration, customChannelMethodDeclaration))) diff --git a/src/CommunityToolkit.Mvvm/ComponentModel/ObservableRecipient.cs b/src/CommunityToolkit.Mvvm/ComponentModel/ObservableRecipient.cs index 5cd9433e6..fd58c22f6 100644 --- a/src/CommunityToolkit.Mvvm/ComponentModel/ObservableRecipient.cs +++ b/src/CommunityToolkit.Mvvm/ComponentModel/ObservableRecipient.cs @@ -54,22 +54,18 @@ protected ObservableRecipient(IMessenger messenger) /// /// Gets or sets a value indicating whether the current view model is currently active. /// + /// + /// When this property is set to , the OnActivated() method will be invoked, + /// which will register all necessary message handlers for this recipient. + /// public bool IsActive { get => this.isActive; - [RequiresUnreferencedCode( - "When this property is set to true, the OnActivated() method will be invoked, which will register all necessary message handlers for this recipient. " + - "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. " + - "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")] [RequiresDynamicCode( - "When this property is set to true, the OnActivated() method will be invoked, which will register all necessary message handlers for this recipient. " + - "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " + - "If that is present, the method is AOT safe, as the only methods being invoked to register the messages will be the ones produced by the source generator. " + - "If it isn't, this method will need to dynamically create the generic methods to register messages, which might not be available at runtime. " + - "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")] + "\nEnsure that the calling view model implements the `IRecipient` interface(s), " + + "or that `OnActivated()` has been manually overridden to register each required message for this recipient individually. " + + "If either of these conditions is met, you can suppress this warning by using the `UnconditionalSuppressMessage` attribute.")] set { if (SetProperty(ref this.isActive, value, true)) @@ -91,22 +87,21 @@ public bool IsActive /// Use this method to register to messages and do other initialization for this instance. /// /// + /// /// The base implementation registers all messages for this recipients that have been declared /// explicitly through the interface, using the default channel. /// For more details on how this works, see the 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. + /// /// - [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. " + - "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")] [RequiresDynamicCode( - "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " + - "If that is present, the method is AOT safe, as the only methods being invoked to register the messages will be the ones produced by the source generator. " + - "If it isn't, this method will need to dynamically create the generic methods to register messages, which might not be available at runtime. " + - "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")] + "Ensure that the view model implements the `IRecipient` interface(s), " + + "Alternatively, `OnActivated()` can be manually overwritten, and registration can be done individually for each required message for this recipient. " + + "If either of these conditions is met, you can suppress this warning by using the `UnconditionalSuppressMessage` attribute.", + Url = "https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/observablerecipient#how-it-works")] protected virtual void OnActivated() { Messenger.RegisterAll(this); diff --git a/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs b/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs index 532639c8f..83a611311 100644 --- a/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs +++ b/src/CommunityToolkit.Mvvm/Messaging/IMessengerExtensions.cs @@ -86,47 +86,18 @@ public static bool IsRegistered(this IMessenger messenger, object reci /// The recipient that will receive the messages. /// See notes for for more info. /// Thrown if or are . - [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.")] [RequiresDynamicCode( - "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " + - "If that is present, the method is AOT safe, as the only methods being invoked to register the messages will be the ones produced by the source generator. " + - "If it isn't, this method will need to dynamically create the generic methods to register messages, which might not be available at runtime.")] + "\nThis fallback method uses `MethodInfo.MakeGenericMethod(Type[])` and " + + "to dynamically construct delegates. " + + "This will fail in Native AOT if the specific generic instantiations were not generated. " + + "Ensure the exact recipient type implements the `CommunityToolkit.Mvvm.Messaging.IRecipient` interface(s) and " + + "is statically known at compile time to use the source-generated, AOT-safe fast path.")] 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? 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)methodInfo.Invoke(null, new object?[] { null })!; - } - - return null; - } - - // Try to get the cached delegate, if the generator has run correctly - Action? registrationAction = DiscoveredRecipients.RegistrationMethods.GetValue( - recipient.GetType(), - LoadRegistrationMethodsForType); - - if (registrationAction is not null) - { - registrationAction(messenger, recipient); - } - else - { - messenger.RegisterAll(recipient, default(Unit)); - } + messenger.RegisterAll(recipient, default(Unit)); } /// @@ -137,18 +108,35 @@ public static void RegisterAll(this IMessenger messenger, object recipient) /// The recipient that will receive the messages. /// The token indicating what channel to use. /// - /// This method will register all messages corresponding to the interfaces - /// being implemented by . 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 interface. + /// + /// This method will register all messages corresponding to + /// the interfaces being implemented + /// by . If none are present, this method will do nothing. + /// + /// + /// This method serves as a runtime fallback registration path. + /// Normally, the MVVM Toolkit source generator automatically produces strongly-typed, + /// AOT-friendly overloads of this method for each discovered recipient type. + /// The C# compiler will prefer those generated methods during overload resolution + /// if the exact type is known. + /// + /// + /// This specific extension method (taking an ) is only invoked + /// if the recipient was cast to , created dynamically, + /// or missed by the source generator. + /// It relies on reflection and compiled LINQ expressions to discover and + /// register interfaces. + /// This incurs a performance overhead on the first invocation for any given type and + /// is not safe for Trimming or Native AOT deployment. + /// /// /// Thrown if , or are . - [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.")] - [RequiresDynamicCode("The generic methods to register messages might not be available at runtime.")] + [RequiresDynamicCode( + "\nThis fallback method uses `MethodInfo.MakeGenericMethod(Type[])` " + + "to dynamically construct delegates. " + + "This will fail in Native AOT if the specific generic instantiations were not generated. " + + "Ensure the exact recipient type implements the `CommunityToolkit.Mvvm.Messaging.IRecipient` interface(s) and " + + "is statically known at compile time to use the source-generated, AOT-safe fast path.")] public static void RegisterAll(this IMessenger messenger, object recipient, TToken token) where TToken : IEquatable { @@ -157,30 +145,15 @@ public static void RegisterAll(this IMessenger messenger, object recipie ArgumentNullException.For.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.")] - [RequiresDynamicCode("The generic methods to register messages might not be available at runtime.")] - static Action 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)genericMethodInfo.Invoke(null, new object?[] { null })!; - } - - return LoadRegistrationMethodsForTypeFallback(recipientType); - } - - // Fallback method when a generated method is not found. + // thread-safety for us. // 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. - [RequiresDynamicCode("The generic methods to register messages might not be available at runtime.")] - static Action LoadRegistrationMethodsForTypeFallback(Type recipientType) + [RequiresDynamicCode( + "The generated strongly-typed method to register messages might not be available at runtime. " + + "Calls `System.Reflection.MethodInfo.MakeGenericMethod(params Type[])`", + Url = "https://learn.microsoft.com/dotnet/core/deploying/native-aot/intrinsic-requiresdynamiccode-apis#methodinfomakegenericmethodtype-method-net-9")] + static Action LoadRegistrationMethodsForType(Type recipientType) { // Get the collection of validation methods MethodInfo[] registrationMethods = ( diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems index 8f141adb6..214ef14bb 100644 --- a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests.projitems @@ -12,6 +12,7 @@ + \ No newline at end of file diff --git a/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.IMessengerRegisterAll.cs b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.IMessengerRegisterAll.cs new file mode 100644 index 000000000..e4a539dff --- /dev/null +++ b/tests/CommunityToolkit.Mvvm.SourceGenerators.UnitTests/Test_SourceGeneratorsCodegen.IMessengerRegisterAll.cs @@ -0,0 +1,74 @@ +// 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 Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CommunityToolkit.Mvvm.SourceGenerators.UnitTests; + +partial class Test_SourceGeneratorsCodegen +{ + private const string TestMessage = /* lang=c#-test */ """ +public class TestMessage +{ + public string Content { get; set; } = string.Empty; +} +"""; + + [TestMethod] + public void IMessengerRegisterAll_GenerateCode() + { + string source = /* lang=c# */ $$""" + using CommunityToolkit.Mvvm.Messaging; + + namespace MyApp; + + {{TestMessage}} + + public partial class MyViewModel : IRecipient + { + public bool IsMessageReceived { get; private set; } + public string ReceivedContent { get; private set; } = string.Empty; + + public void Receive(TestMessage message) + { + IsMessageReceived = true; + ReceivedContent = message.Content; + } + } + """; + + string result = /* lang=c# */ """ + // + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Mvvm.Messaging + { + /// + partial class __IMessengerExtensions + { + /// + /// Registers all declared message handlers for a given recipient, using the default channel. + /// + /// + public static void RegisterAll(this global::CommunityToolkit.Mvvm.Messaging.IMessenger messenger, global::MyApp.MyViewModel recipient) + { + messenger.Register(recipient); + } + + /// + /// Registers all declared message handlers for a given recipient. + /// + /// + public static void RegisterAll(this global::CommunityToolkit.Mvvm.Messaging.IMessenger messenger, global::MyApp.MyViewModel recipient, TToken token) + where TToken : notnull, global::System.IEquatable + { + messenger.Register(recipient, token); + } + } + } + """; + + VerifyGenerateSources(source, new[] { new IMessengerRegisterAllGenerator() }, ("MyApp.MyViewModel.g.cs", result)); + } +} diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ArgumentNullException.Messaging.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ArgumentNullException.Messaging.cs index eed2d8846..32a18793c 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ArgumentNullException.Messaging.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ArgumentNullException.Messaging.cs @@ -50,10 +50,10 @@ public void Test_ArgumentNullException_MessengerExtensions(Type type) Assert(() => messenger.IsRegistered(recipient: null!), "recipient"); Assert(() => ((IMessenger)null!).RegisterAll(new object()), "messenger"); - Assert(() => messenger.RegisterAll(recipient: null!), "recipient"); + Assert(() => messenger.RegisterAll(recipient: (object)null!), "recipient"); Assert(() => ((IMessenger)null!).RegisterAll(new object(), ""), "messenger"); - Assert(() => messenger.RegisterAll(recipient: null!, ""), "recipient"); + Assert(() => messenger.RegisterAll(recipient: (object)null!, ""), "recipient"); Assert(() => messenger.RegisterAll(new object(), token: null!), "token"); Assert(() => ((IMessenger)null!).Register(new Recipient()), "messenger"); diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_IRecipientGenerator.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_IRecipientGenerator.cs index 782e41450..8c7b4e119 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_IRecipientGenerator.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_IRecipientGenerator.cs @@ -24,9 +24,7 @@ public void Test_IRecipientGenerator_GeneratedRegistration() MessageA messageA = new(); MessageB messageB = new(); - Action registrator = Messaging.__Internals.__IMessengerExtensions.CreateAllMessagesRegistratorWithToken(recipient); - - registrator(messenger, recipient, 42); + messenger.RegisterAll(recipient, 42); Assert.IsTrue(messenger.IsRegistered(recipient, 42)); Assert.IsTrue(messenger.IsRegistered(recipient, 42)); @@ -51,16 +49,16 @@ public void Test_IRecipientGenerator_TypeWithMultipleClassDeclarations() RecipientWithMultipleClassDeclarations recipient = new(); // This test really just needs to verify this compiles and executes normally - _ = Messaging.__Internals.__IMessengerExtensions.CreateAllMessagesRegistratorWithToken(recipient); + WeakReferenceMessenger.Default.RegisterAll(recipient, 5); } [TestMethod] public void Test_IRecipientGenerator_AbstractTypesDoNotTriggerCodeGeneration() { - MethodInfo? createAllPropertiesValidatorMethod = typeof(Messaging.__Internals.__IMessengerExtensions) + MethodInfo? createAllPropertiesValidatorMethod = typeof(Messaging.__IMessengerExtensions) .GetMethods(BindingFlags.Static | BindingFlags.Public) - .Where(static m => m.Name == "CreateAllMessagesRegistratorWithToken") - .Where(static m => m.GetParameters() is { Length: 1 } parameters && parameters[0].ParameterType == typeof(AbstractModelWithValidatablePropertyIRecipientInterfaces)) + .Where(static m => m.Name == "RegisterAll") + .Where(static m => m.GetParameters() is { Length: 3 } parameters && parameters[1].ParameterType == typeof(AbstractModelWithValidatablePropertyIRecipientInterfaces)) .FirstOrDefault(); // We need to validate that no methods are generated for abstract types, so we just check this method doesn't exist diff --git a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableRecipient.cs b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableRecipient.cs index 28135603c..7d46c3774 100644 --- a/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableRecipient.cs +++ b/tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableRecipient.cs @@ -97,13 +97,13 @@ public void Test_IRecipient_VerifyTrimmingAnnotation() { #if NET6_0_OR_GREATER System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute? attribute = - typeof(Messaging.__Internals.__IMessengerExtensions) + typeof(Messaging.__IMessengerExtensions) .GetCustomAttribute(); Assert.IsNotNull(attribute); Assert.AreEqual(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods, attribute.MemberTypes); #else - IEnumerable attributes = typeof(Messaging.__Internals.__IMessengerExtensions).GetCustomAttributes(); + IEnumerable attributes = typeof(Messaging.__IMessengerExtensions).GetCustomAttributes(); Assert.IsFalse(attributes.Any(static a => a.GetType().Name is "DynamicallyAccessedMembersAttribute")); #endif