Skip to content

Commit 704e507

Browse files
committed
Enable implementing OnActivated/OnDeactivated with [ObservableRecipient]
1 parent 35d403b commit 704e507

File tree

5 files changed

+122
-22
lines changed

5 files changed

+122
-22
lines changed

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/Models/ObservableRecipientInfo.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,13 @@ namespace CommunityToolkit.Mvvm.SourceGenerators.ComponentModel.Models;
1212
/// <param name="IsAbstract">Whether or not the target type is abstract.</param>
1313
/// <param name="IsObservableValidator">Whether or not the target type inherits from <c>ObservableValidator</c>.</param>
1414
/// <param name="IsRequiresUnreferencedCodeAttributeAvailable">Whether or not the <c>RequiresUnreferencedCodeAttribute</c> type is available.</param>
15+
/// <param name="HasOnActivatedMethod">Whether the target type has a custom <c>OnActivated()</c> method.</param>
16+
/// <param name="HasOnDeactivatedMethod">Whether the target type has a custom <c>OnDeactivated()</c> method.</param>
1517
public sealed record ObservableRecipientInfo(
1618
string TypeName,
1719
bool HasExplicitConstructors,
1820
bool IsAbstract,
1921
bool IsObservableValidator,
20-
bool IsRequiresUnreferencedCodeAttributeAvailable);
22+
bool IsRequiresUnreferencedCodeAttributeAvailable,
23+
bool HasOnActivatedMethod,
24+
bool HasOnDeactivatedMethod);

CommunityToolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,17 @@ static ObservableRecipientInfo GetInfo(INamedTypeSymbol typeSymbol, AttributeDat
4040
bool hasExplicitConstructors = !(typeSymbol.InstanceConstructors.Length == 1 && typeSymbol.InstanceConstructors[0] is { Parameters.IsEmpty: true, IsImplicitlyDeclared: true });
4141
bool isAbstract = typeSymbol.IsAbstract;
4242
bool isObservableValidator = typeSymbol.InheritsFrom("global::CommunityToolkit.Mvvm.ComponentModel.ObservableValidator");
43+
bool hasOnActivatedMethod = typeSymbol.GetMembers().Any(m => m is IMethodSymbol { Parameters.IsEmpty: true, Name: "OnActivated" });
44+
bool hasOnDeactivatedMethod = typeSymbol.GetMembers().Any(m => m is IMethodSymbol { Parameters.IsEmpty: true, Name: "OnDeactivated" });
4345

4446
return new(
4547
typeName,
4648
hasExplicitConstructors,
4749
isAbstract,
4850
isObservableValidator,
49-
isRequiresUnreferencedCodeAttributeAvailable);
51+
isRequiresUnreferencedCodeAttributeAvailable,
52+
hasOnActivatedMethod,
53+
hasOnDeactivatedMethod);
5054
}
5155

5256
// Check whether [RequiresUnreferencedCode] is available
@@ -119,8 +123,38 @@ protected override ImmutableArray<MemberDeclarationSyntax> FilterDeclaredMembers
119123
}
120124
}
121125

122-
MemberDeclarationSyntax RemoveRequiresUnreferencedCodeAttributeIfNeeded(MemberDeclarationSyntax member)
126+
MemberDeclarationSyntax FixupFilteredMemberDeclaration(MemberDeclarationSyntax member)
123127
{
128+
// Make OnActivated partial if the type already has the method
129+
if (info.HasOnActivatedMethod &&
130+
member is MethodDeclarationSyntax { Identifier.ValueText: "OnActivated" } onActivatdMethod)
131+
{
132+
SyntaxNode attributeNode =
133+
member
134+
.DescendantNodes()
135+
.OfType<AttributeListSyntax>()
136+
.First(node => node.Attributes[0].Name is QualifiedNameSyntax { Right: IdentifierNameSyntax { Identifier.ValueText: "RequiresUnreferencedCode" } });
137+
138+
return
139+
onActivatdMethod
140+
.RemoveNode(attributeNode, SyntaxRemoveOptions.KeepExteriorTrivia)!
141+
.AddModifiers(Token(SyntaxKind.PartialKeyword))
142+
.WithBody(null)
143+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
144+
}
145+
146+
// Make OnDeactivated partial if the type already has the method
147+
if (info.HasOnDeactivatedMethod &&
148+
member is MethodDeclarationSyntax { Identifier.ValueText: "OnDeactivated" } onDeactivatedMethod)
149+
{
150+
return
151+
onDeactivatedMethod
152+
.AddModifiers(Token(SyntaxKind.PartialKeyword))
153+
.WithBody(null)
154+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken));
155+
}
156+
157+
// Remove [RequiresUnreferencedCode] if the attribute is not available
124158
if (!info.IsRequiresUnreferencedCodeAttributeAvailable &&
125159
member is PropertyDeclarationSyntax { Identifier.ValueText: "IsActive" } or MethodDeclarationSyntax { Identifier.ValueText: "OnActivated" })
126160
{
@@ -143,7 +177,7 @@ MemberDeclarationSyntax RemoveRequiresUnreferencedCodeAttributeIfNeeded(MemberDe
143177
{
144178
if (member is not MethodDeclarationSyntax { Identifier.ValueText: "SetProperty" })
145179
{
146-
builder.Add(RemoveRequiresUnreferencedCodeAttributeIfNeeded(member));
180+
builder.Add(FixupFilteredMemberDeclaration(member));
147181
}
148182
}
149183

@@ -153,7 +187,7 @@ MemberDeclarationSyntax RemoveRequiresUnreferencedCodeAttributeIfNeeded(MemberDe
153187
// If the target type has at least one custom constructor, only generate methods
154188
foreach (MemberDeclarationSyntax member in memberDeclarations.Where(static member => member is not ConstructorDeclarationSyntax))
155189
{
156-
builder.Add(RemoveRequiresUnreferencedCodeAttributeIfNeeded(member));
190+
builder.Add(FixupFilteredMemberDeclaration(member));
157191
}
158192

159193
return builder.ToImmutable();

CommunityToolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,16 +66,9 @@ public bool IsActive
6666
}
6767

6868
/// <summary>
69-
/// Raised whenever the <see cref="IsActive"/> property is set to <see langword="true"/>.
69+
/// Invoked whenever the <see cref="IsActive"/> property is set to <see langword="true"/>.
7070
/// Use this method to register to messages and do other initialization for this instance.
7171
/// </summary>
72-
/// <remarks>
73-
/// The base implementation registers all messages for this recipients that have been declared
74-
/// explicitly through the <see cref="global::CommunityToolkit.Mvvm.Messaging.IRecipient{TMessage}"/> interface, using the default channel.
75-
/// For more details on how this works, see the <see cref="global::CommunityToolkit.Mvvm.Messaging.IMessengerExtensions.RegisterAll"/> method.
76-
/// If you need more fine tuned control, want to register messages individually or just prefer
77-
/// the lambda-style syntax for message registration, override this method and register manually.
78-
/// </remarks>
7972
[global::System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(
8073
"This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " +
8174
"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 " +
@@ -87,15 +80,9 @@ protected virtual void OnActivated()
8780
}
8881

8982
/// <summary>
90-
/// Raised whenever the <see cref="IsActive"/> property is set to <see langword="false"/>.
83+
/// Invoked whenever the <see cref="IsActive"/> property is set to <see langword="false"/>.
9184
/// Use this method to unregister from messages and do general cleanup for this instance.
9285
/// </summary>
93-
/// <remarks>
94-
/// The base implementation unregisters all messages for this recipient. It does so by
95-
/// invoking <see cref="global::CommunityToolkit.Mvvm.Messaging.IMessenger.UnregisterAll"/>, which removes all registered
96-
/// handlers for a given subscriber, regardless of what token was used to register them.
97-
/// That is, all registered handlers across all subscription channels will be removed.
98-
/// </remarks>
9986
protected virtual void OnDeactivated()
10087
{
10188
Messenger.UnregisterAll(this);

CommunityToolkit.Mvvm/ComponentModel/ObservableRecipient.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public bool IsActive
8181
}
8282

8383
/// <summary>
84-
/// Raised whenever the <see cref="IsActive"/> property is set to <see langword="true"/>.
84+
/// Invoked whenever the <see cref="IsActive"/> property is set to <see langword="true"/>.
8585
/// Use this method to register to messages and do other initialization for this instance.
8686
/// </summary>
8787
/// <remarks>
@@ -102,7 +102,7 @@ protected virtual void OnActivated()
102102
}
103103

104104
/// <summary>
105-
/// Raised whenever the <see cref="IsActive"/> property is set to <see langword="false"/>.
105+
/// Invoked whenever the <see cref="IsActive"/> property is set to <see langword="false"/>.
106106
/// Use this method to unregister from messages and do general cleanup for this instance.
107107
/// </summary>
108108
/// <remarks>

tests/CommunityToolkit.Mvvm.UnitTests/Test_ObservableRecipientAttribute.cs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,79 @@ public void Test_ObservableRecipientAttribute_TrimmingAnnoations_OnActivated()
153153
public abstract partial class TestRecipient : ObservableObject
154154
{
155155
}
156+
157+
[TestMethod]
158+
public void Test_ObservableRecipientAttribute_PersonWithCustomOnActivated()
159+
{
160+
PersonWithCustomOnActivated model = new();
161+
162+
model.IsActive = true;
163+
164+
Assert.IsTrue(model.Result);
165+
}
166+
167+
[ObservableRecipient]
168+
public partial class PersonWithCustomOnActivated : ObservableObject
169+
{
170+
public bool Result { get; private set; }
171+
172+
protected virtual partial void OnActivated()
173+
{
174+
Result = true;
175+
}
176+
}
177+
178+
[TestMethod]
179+
public void Test_ObservableRecipientAttribute_PersonWithCustomOnDeactivated()
180+
{
181+
PersonWithCustomOnDeactivated model = new();
182+
183+
model.IsActive = true;
184+
model.IsActive = false;
185+
186+
Assert.IsTrue(model.Result);
187+
}
188+
189+
[ObservableRecipient]
190+
public partial class PersonWithCustomOnDeactivated : ObservableObject
191+
{
192+
public bool Result { get; private set; }
193+
194+
protected virtual partial void OnDeactivated()
195+
{
196+
Result = true;
197+
}
198+
}
199+
200+
[TestMethod]
201+
public void Test_ObservableRecipientAttribute_PersonWithCustomOnActivatedAndOnDeactivated()
202+
{
203+
PersonWithCustomOnActivatedAndOnDeactivated model = new();
204+
205+
model.IsActive = true;
206+
207+
Assert.IsTrue(model.OnActivatedResult);
208+
209+
model.IsActive = false;
210+
211+
Assert.IsTrue(model.OnDeactivatedResult);
212+
}
213+
214+
[ObservableRecipient]
215+
public partial class PersonWithCustomOnActivatedAndOnDeactivated : ObservableObject
216+
{
217+
public bool OnActivatedResult { get; private set; }
218+
219+
public bool OnDeactivatedResult { get; private set; }
220+
221+
protected virtual partial void OnActivated()
222+
{
223+
OnActivatedResult = true;
224+
}
225+
226+
protected virtual partial void OnDeactivated()
227+
{
228+
OnDeactivatedResult = true;
229+
}
230+
}
156231
}

0 commit comments

Comments
 (0)