From 66a62ae16f32b2c7c71fc4656f8fbc75dbb4adcf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 21 Mar 2021 19:18:49 +0100 Subject: [PATCH 01/89] Added initial draft of SG project and ObservableObjectAttribute --- ...osoft.Toolkit.Mvvm.SourceGenerators.csproj | 24 ++++ .../ObservableObjectGenerator.cs | 121 ++++++++++++++++++ .../Attributes/ObservableObjectAttribute.cs | 32 +++++ Windows Community Toolkit.sln | 22 ++++ 4 files changed, 199 insertions(+) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs create mode 100644 Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj new file mode 100644 index 00000000000..f2e0bbe242f --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -0,0 +1,24 @@ + + + + netstandard2.0 + 9.0 + enable + + + + + + + + + + PreserveNewest + + + + + + + + diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs new file mode 100644 index 00000000000..10eac458ec4 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + /// A source generator for the type. + /// + [Generator] + public class ObservableObjectGenerator : ISourceGenerator + { + /// + public void Initialize(GeneratorInitializationContext context) + { + } + + /// + public void Execute(GeneratorExecutionContext context) + { + // Find all the [ObservableObject] usages + IEnumerable attributes = + from syntaxTree in context.Compilation.SyntaxTrees + let semanticModel = context.Compilation.GetSemanticModel(syntaxTree) + from attribute in syntaxTree.GetRoot().DescendantNodes().OfType() + let typeInfo = semanticModel.GetTypeInfo(attribute) + where typeInfo.Type is { Name: nameof(ObservableObjectAttribute) } + select attribute; + + SyntaxTree? observableObjectSyntaxTree = null; + + foreach (AttributeSyntax attribute in attributes) + { + // Load the ObservableObject syntax tree if needed + if (observableObjectSyntaxTree is null) + { + string filename = "Microsoft.Toolkit.Mvvm.SourceGenerators.Resources.ObservableObject.cs"; + + Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename); + StreamReader reader = new(stream); + + string observableObjectSource = reader.ReadToEnd(); + + observableObjectSyntaxTree = CSharpSyntaxTree.ParseText(observableObjectSource); + } + + ClassDeclarationSyntax classDeclaration = attribute.FirstAncestorOrSelf()!; + SemanticModel semanticModel = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree); + INamedTypeSymbol classDeclarationSymbol = semanticModel.GetDeclaredSymbol(classDeclaration)!; + + OnExecute(context, classDeclaration, classDeclarationSymbol, observableObjectSyntaxTree); + } + } + + /// + /// Processes a given target type. + /// + /// The input instance to use. + /// The node to process. + /// The for . + /// The for the parsed source. + private static void OnExecute( + GeneratorExecutionContext context, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + SyntaxTree observableObjectSyntaxTree) + { + ClassDeclarationSyntax observableObjectDeclaration = observableObjectSyntaxTree.GetRoot().DescendantNodes().OfType().First(); + UsingDirectiveSyntax[] usingDirectives = observableObjectSyntaxTree.GetRoot().DescendantNodes().OfType().ToArray(); + + // Create the class declaration for the user type. This will produce a tree as follows: + // + // : INotifyPropertyChanged, INotifyPropertyChanging + // { + // + // } + var classDeclarationSyntax = + ClassDeclaration(classDeclaration.Identifier.Text) + .WithModifiers(classDeclaration.Modifiers) + .WithBaseList(observableObjectDeclaration.BaseList) + .WithMembers(observableObjectDeclaration.Members); + + TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; + + // Add all parent types in ascending order, if any + foreach (var parentType in classDeclaration.Ancestors().OfType()) + { + typeDeclarationSyntax = parentType + .WithMembers(SingletonList(typeDeclarationSyntax)) + .WithConstraintClauses(List()) + .WithBaseList(null) + .WithAttributeLists(List()) + .WithoutTrivia(); + } + + // Create the compilation unit with the namespace and target member. + // From this, we can finally generate the source code to output. + var namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)); + + var source = + CompilationUnit() + .AddUsings(usingDirectives) + .AddMembers(NamespaceDeclaration(IdentifierName(namespaceName)) + .AddMembers(typeDeclarationSyntax)) + .NormalizeWhitespace() + .ToFullString(); + + // Add the partial type + context.AddSource($"[{nameof(ObservableObjectAttribute)}]_[{classDeclaration.Identifier.Text}].cs", SourceText.From(source, Encoding.UTF8)); + } + } +} diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs new file mode 100644 index 00000000000..7a66443b7cb --- /dev/null +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs @@ -0,0 +1,32 @@ +// 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.ComponentModel; + +namespace Microsoft.Toolkit.Mvvm.ComponentModel +{ + /// + /// An attribute that indicates that a given type should have all the members from \ + /// generated into it, as well as the and + /// interfaces. This can be useful when you want the same functionality from into a class + /// that already inherits from another one (since C# doesn't support multiple inheritance). This attribute will trigger + /// the source generator to just create the same APIs directly into the decorated class. + /// + /// This attribute can be used as follows: + /// + /// [ObservableObject] + /// partial class MyViewModel : SomeOtherClass + /// { + /// // Other members here... + /// } + /// + /// + /// And with this, the same APIs from will be available on this type as well. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public sealed class ObservableObjectAttribute : Attribute + { + } +} diff --git a/Windows Community Toolkit.sln b/Windows Community Toolkit.sln index 991177c199b..bd397f3b7ae 100644 --- a/Windows Community Toolkit.sln +++ b/Windows Community Toolkit.sln @@ -157,6 +157,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Toolkit.Uwp.UI.Co EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Toolkit.Uwp.UI.Controls", "Microsoft.Toolkit.Uwp.UI.Controls\Microsoft.Toolkit.Uwp.UI.Controls.csproj", "{099B60FD-DAD6-4648-9DE2-8DBF9DCD9557}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Toolkit.Mvvm.SourceGenerators", "Microsoft.Toolkit.Mvvm.SourceGenerators\Microsoft.Toolkit.Mvvm.SourceGenerators.csproj", "{E24D1146-5AD8-498F-A518-4890D8BF4937}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution UITests\UITests.Tests.Shared\UITests.Tests.Shared.projitems*{05c83067-fa46-45e2-bec4-edee84ad18d0}*SharedItemsImports = 4 @@ -1110,6 +1112,26 @@ Global {099B60FD-DAD6-4648-9DE2-8DBF9DCD9557}.Release|x64.Build.0 = Release|Any CPU {099B60FD-DAD6-4648-9DE2-8DBF9DCD9557}.Release|x86.ActiveCfg = Release|Any CPU {099B60FD-DAD6-4648-9DE2-8DBF9DCD9557}.Release|x86.Build.0 = Release|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Debug|ARM.ActiveCfg = Debug|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Debug|ARM.Build.0 = Debug|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Debug|ARM64.Build.0 = Debug|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Debug|x64.ActiveCfg = Debug|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Debug|x64.Build.0 = Debug|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Debug|x86.ActiveCfg = Debug|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Debug|x86.Build.0 = Debug|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|Any CPU.Build.0 = Release|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|ARM.ActiveCfg = Release|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|ARM.Build.0 = Release|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|ARM64.ActiveCfg = Release|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|ARM64.Build.0 = Release|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|x64.ActiveCfg = Release|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|x64.Build.0 = Release|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|x86.ActiveCfg = Release|Any CPU + {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 8e7a4a28715f4e0e1dae281792b93fd6104f9d1c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 21 Mar 2021 19:27:27 +0100 Subject: [PATCH 02/89] Added unit test file and reference --- .../Mvvm/Test_ObservableObjectAttribute.cs | 65 +++++++++++++++++++ .../UnitTests.NetCore.csproj | 2 + 2 files changed, 67 insertions(+) create mode 100644 UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs new file mode 100644 index 00000000000..2c638cbe54c --- /dev/null +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs @@ -0,0 +1,65 @@ +// 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.ComponentModel; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.Mvvm +{ + [TestClass] + public partial class Test_ObservableObjectAttribute + { + [TestCategory("Mvvm")] + [TestMethod] + public void Test_ObservableObject_Events() + { + var model = new SampleModel(); + + (PropertyChangingEventArgs, int) changing = default; + (PropertyChangedEventArgs, int) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Data); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Data); + }; + + model.Data = 42; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Data)); + Assert.AreEqual(changing.Item2, 0); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Data)); + Assert.AreEqual(changed.Item2, 42); + } + + [ObservableObject] + public partial class SampleModel + { + private int data; + + public int Data + { + get => data; + set => SetProperty(ref data, value); + } + } + } +} diff --git a/UnitTests/UnitTests.NetCore/UnitTests.NetCore.csproj b/UnitTests/UnitTests.NetCore/UnitTests.NetCore.csproj index 32b82e9bf13..3f7353455ff 100644 --- a/UnitTests/UnitTests.NetCore/UnitTests.NetCore.csproj +++ b/UnitTests/UnitTests.NetCore/UnitTests.NetCore.csproj @@ -2,12 +2,14 @@ netcoreapp2.1;netcoreapp3.1;net5.0 + 9.0 + From d53262c7d6c0cc0c4d8ef0d572afa7f2d6db2be3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 21 Mar 2021 20:13:10 +0100 Subject: [PATCH 03/89] Added solution filter for .NET projects --- Windows Community Toolkit (NET).slnf | 1 + 1 file changed, 1 insertion(+) diff --git a/Windows Community Toolkit (NET).slnf b/Windows Community Toolkit (NET).slnf index 82ef7943535..a91a7b512b1 100644 --- a/Windows Community Toolkit (NET).slnf +++ b/Windows Community Toolkit (NET).slnf @@ -4,6 +4,7 @@ "projects": [ "Microsoft.Toolkit.Diagnostics\\Microsoft.Toolkit.Diagnostics.csproj", "Microsoft.Toolkit.HighPerformance\\Microsoft.Toolkit.HighPerformance.csproj", + "Microsoft.Toolkit.Mvvm.SourceGenerators\\Microsoft.Toolkit.Mvvm.SourceGenerators.csproj", "Microsoft.Toolkit.Mvvm\\Microsoft.Toolkit.Mvvm.csproj", "Microsoft.Toolkit\\Microsoft.Toolkit.csproj", "UnitTests\\UnitTests.HighPerformance.NetCore\\UnitTests.HighPerformance.NetCore.csproj", From fa4e2dc1ae2ca6eb335bf9ad3f3d5c80e80c39ab Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 21 Mar 2021 21:54:31 +0100 Subject: [PATCH 04/89] Fixed header generation --- .../ObservableObjectGenerator.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs index 10eac458ec4..cc4034982fc 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs @@ -108,10 +108,23 @@ private static void OnExecute( var source = CompilationUnit() - .AddUsings(usingDirectives) .AddMembers(NamespaceDeclaration(IdentifierName(namespaceName)) .AddMembers(typeDeclarationSyntax)) .NormalizeWhitespace() + .AddUsings(usingDirectives.First().WithLeadingTrivia(TriviaList( + Comment("// Licensed to the .NET Foundation under one or more agreements."), + CarriageReturnLineFeed, + Comment("// The .NET Foundation licenses this file to you under the MIT license."), + CarriageReturnLineFeed, + Comment("// See the LICENSE file in the project root for more information."), + CarriageReturnLineFeed, + CarriageReturnLineFeed, + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true) + .WithPragmaKeyword(Token(TriviaList(), SyntaxKind.PragmaKeyword, TriviaList(Space))) + .WithWarningKeyword(Token(TriviaList(), SyntaxKind.WarningKeyword, TriviaList(Space))) + .WithEndOfDirectiveToken(Token(TriviaList(), SyntaxKind.EndOfDirectiveToken, TriviaList(CarriageReturnLineFeed)))), + CarriageReturnLineFeed))) + .AddUsings(usingDirectives.Skip(1).ToArray()) .ToFullString(); // Add the partial type From 36ce871ad770e1e396714b825ab9ae650b2c4fdd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 21 Mar 2021 22:26:34 +0100 Subject: [PATCH 05/89] Added INotifyPropertyChangedAttribute and NotifyPropertyChangedObject --- .../NotifyPropertyChangedObject.cs | 494 ++++++++++++++++++ .../INotifyPropertyChangedAttribute.cs | 30 ++ .../Attributes/ObservableObjectAttribute.cs | 2 +- 3 files changed, 525 insertions(+), 1 deletion(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm/NotifyPropertyChangedObject.cs create mode 100644 Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm/NotifyPropertyChangedObject.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm/NotifyPropertyChangedObject.cs new file mode 100644 index 00000000000..e32dc07672e --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm/NotifyPropertyChangedObject.cs @@ -0,0 +1,494 @@ +// 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.ComponentModel; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Microsoft.Toolkit.Mvvm.ComponentModel +{ + /// + /// A base class for objects implementing . + /// + public abstract class NotifyPropertyChangedObject : INotifyPropertyChanged + { + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Raises the event. + /// + /// The input instance. + protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + + /// + /// Raises the event. + /// + /// (optional) The name of the property that changed. + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); + } + + /// + /// Compares the current and new values for a given property. If the value has changed, updates + /// the property with the new value, then raises the event. + /// + /// The type of the property that changed. + /// The field storing the property's value. + /// The property's value after the change occurred. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The event is not raised if the current and new value for the target property are the same. + /// + protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, newValue)) + { + return false; + } + + field = newValue; + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given property. If the value has changed, updates + /// the property with the new value, then raises the event. + /// See additional notes about this overload in . + /// + /// The type of the property that changed. + /// The field storing the property's value. + /// The property's value after the change occurred. + /// The instance to use to compare the input values. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null) + { + if (comparer.Equals(field, newValue)) + { + return false; + } + + field = newValue; + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given property. If the value has changed, updates + /// the property with the new value, then raises the event. + /// This overload is much less efficient than and it + /// should only be used when the former is not viable (eg. when the target property being + /// updated does not directly expose a backing field that can be passed by reference). + /// For performance reasons, it is recommended to use a stateful callback if possible through + /// the whenever possible + /// instead of this overload, as that will allow the C# compiler to cache the input callback and + /// reduce the memory allocations. More info on that overload are available in the related XML + /// docs. This overload is here for completeness and in cases where that is not applicable. + /// + /// The type of the property that changed. + /// The current property value. + /// The property's value after the change occurred. + /// A callback to invoke to update the property value. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The event is not raised if the current and new value for the target property are the same. + /// + protected bool SetProperty(T oldValue, T newValue, Action callback, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(oldValue, newValue)) + { + return false; + } + + callback(newValue); + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given property. If the value has changed, updates + /// the property with the new value, then raises the event. + /// See additional notes about this overload in . + /// + /// The type of the property that changed. + /// The current property value. + /// The property's value after the change occurred. + /// The instance to use to compare the input values. + /// A callback to invoke to update the property value. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, [CallerMemberName] string? propertyName = null) + { + if (comparer.Equals(oldValue, newValue)) + { + return false; + } + + callback(newValue); + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given nested property. If the value has changed, + /// updates the property and then raises the event. + /// The behavior mirrors that of , + /// with the difference being that this method is used to relay properties from a wrapped model in the + /// current instance. This type is useful when creating wrapping, bindable objects that operate over + /// models that lack support for notification (eg. for CRUD operations). + /// Suppose we have this model (eg. for a database row in a table): + /// + /// public class Person + /// { + /// public string Name { get; set; } + /// } + /// + /// We can then use a property to wrap instances of this type into our observable model (which supports + /// notifications), injecting the notification to the properties of that model, like so: + /// + /// [INotifyPropertyChanged] + /// public partial class BindablePerson + /// { + /// public Model { get; } + /// + /// public BindablePerson(Person model) + /// { + /// Model = model; + /// } + /// + /// public string Name + /// { + /// get => Model.Name; + /// set => Set(Model.Name, value, Model, (model, name) => model.Name = name); + /// } + /// } + /// + /// This way we can then use the wrapping object in our application, and all those "proxy" properties will + /// also raise notifications when changed. Note that this method is not meant to be a replacement for + /// , and it should only be used when relaying properties to a model that + /// doesn't support notifications, and only if you can't implement notifications to that model directly (eg. by having + /// it inherit from ). The syntax relies on passing the target model and a stateless callback + /// to allow the C# compiler to cache the function, which results in much better performance and no memory usage. + /// + /// The type of model whose property (or field) to set. + /// The type of property (or field) to set. + /// The current property value. + /// The property's value after the change occurred. + /// The model containing the property being updated. + /// The callback to invoke to set the target property value, if a change has occurred. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The event is not raised if the current and new value for the target property are the same. + /// + protected bool SetProperty(T oldValue, T newValue, TModel model, Action callback, [CallerMemberName] string? propertyName = null) + where TModel : class + { + if (EqualityComparer.Default.Equals(oldValue, newValue)) + { + return false; + } + + callback(model, newValue); + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given nested property. If the value has changed, + /// updates the property and then raises the event. + /// The behavior mirrors that of , + /// with the difference being that this method is used to relay properties from a wrapped model in the + /// current instance. See additional notes about this overload in . + /// + /// The type of model whose property (or field) to set. + /// The type of property (or field) to set. + /// The current property value. + /// The property's value after the change occurred. + /// The instance to use to compare the input values. + /// The model containing the property being updated. + /// The callback to invoke to set the target property value, if a change has occurred. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, [CallerMemberName] string? propertyName = null) + where TModel : class + { + if (comparer.Equals(oldValue, newValue)) + { + return false; + } + + callback(model, newValue); + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given field (which should be the backing field for a property). + /// If the value has changed, updates the field and then raises the event. + /// The behavior mirrors that of , with the difference being that + /// this method will also monitor the new value of the property (a generic ) and will also + /// raise the again for the target property when it completes. + /// This can be used to update bindings observing that or any of its properties. + /// This method and its overload specifically rely on the type, which needs + /// to be used in the backing field for the target property. The field doesn't need to be + /// initialized, as this method will take care of doing that automatically. The + /// type also includes an implicit operator, so it can be assigned to any instance directly. + /// Here is a sample property declaration using this method: + /// + /// private TaskNotifier myTask; + /// + /// public Task MyTask + /// { + /// get => myTask; + /// private set => SetAndNotifyOnCompletion(ref myTask, value); + /// } + /// + /// + /// The field notifier to modify. + /// The property's value after the change occurred. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The event is not raised if the current and new value for the target property are + /// the same. The return value being only indicates that the new value being assigned to + /// is different than the previous one, and it does not mean the new + /// instance passed as argument is in any particular state. + /// + protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) + { + return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); + } + + /// + /// Compares the current and new values for a given field (which should be the backing field for a property). + /// If the value has changed, updates the field and then raises the event. + /// This method is just like , + /// with the difference being an extra parameter with a callback being invoked + /// either immediately, if the new task has already completed or is , or upon completion. + /// + /// The field notifier to modify. + /// The property's value after the change occurred. + /// A callback to invoke to update the property value. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The event is not raised if the current and new value for the target property are the same. + /// + protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null) + { + return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); + } + + /// + /// Compares the current and new values for a given field (which should be the backing field for a property). + /// If the value has changed, updates the field and then raises the event. + /// The behavior mirrors that of , with the difference being that + /// this method will also monitor the new value of the property (a generic ) and will also + /// raise the again for the target property when it completes. + /// This can be used to update bindings observing that or any of its properties. + /// This method and its overload specifically rely on the type, which needs + /// to be used in the backing field for the target property. The field doesn't need to be + /// initialized, as this method will take care of doing that automatically. The + /// type also includes an implicit operator, so it can be assigned to any instance directly. + /// Here is a sample property declaration using this method: + /// + /// private TaskNotifier<int> myTask; + /// + /// public Task<int> MyTask + /// { + /// get => myTask; + /// private set => SetAndNotifyOnCompletion(ref myTask, value); + /// } + /// + /// + /// The type of result for the to set and monitor. + /// The field notifier to modify. + /// The property's value after the change occurred. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The event is not raised if the current and new value for the target property are + /// the same. The return value being only indicates that the new value being assigned to + /// is different than the previous one, and it does not mean the new + /// instance passed as argument is in any particular state. + /// + protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) + { + return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); + } + + /// + /// Compares the current and new values for a given field (which should be the backing field for a property). + /// If the value has changed, updates the field and then raises the event. + /// This method is just like , + /// with the difference being an extra parameter with a callback being invoked + /// either immediately, if the new task has already completed or is , or upon completion. + /// + /// The type of result for the to set and monitor. + /// The field notifier to modify. + /// The property's value after the change occurred. + /// A callback to invoke to update the property value. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The event is not raised if the current and new value for the target property are the same. + /// + protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null) + { + return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); + } + + /// + /// Implements the notification logic for the related methods. + /// + /// The type of to set and monitor. + /// The field notifier. + /// The property's value after the change occurred. + /// A callback to invoke to update the property value. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + private bool SetPropertyAndNotifyOnCompletion(ITaskNotifier taskNotifier, TTask? newValue, Action callback, [CallerMemberName] string? propertyName = null) + where TTask : Task + { + if (ReferenceEquals(taskNotifier.Task, newValue)) + { + return false; + } + + bool isAlreadyCompletedOrNull = newValue?.IsCompleted ?? true; + + taskNotifier.Task = newValue; + + OnPropertyChanged(propertyName); + + if (isAlreadyCompletedOrNull) + { + callback(newValue); + + return true; + } + + async void MonitorTask() + { + try + { + await newValue!; + } + catch + { + } + + if (ReferenceEquals(taskNotifier.Task, newValue)) + { + OnPropertyChanged(propertyName); + } + + callback(newValue); + } + + MonitorTask(); + + return true; + } + + /// + /// An interface for task notifiers of a specified type. + /// + /// The type of value to store. + private interface ITaskNotifier + where TTask : Task + { + /// + /// Gets or sets the wrapped value. + /// + TTask? Task { get; set; } + } + + /// + /// A wrapping class that can hold a value. + /// + protected sealed class TaskNotifier : ITaskNotifier + { + /// + /// Initializes a new instance of the class. + /// + internal TaskNotifier() + { + } + + private Task? task; + + /// + Task? ITaskNotifier.Task + { + get => this.task; + set => this.task = value; + } + + /// + /// Unwraps the value stored in the current instance. + /// + /// The input instance. + public static implicit operator Task?(TaskNotifier? notifier) + { + return notifier?.task; + } + } + + /// + /// A wrapping class that can hold a value. + /// + /// The type of value for the wrapped instance. + protected sealed class TaskNotifier : ITaskNotifier> + { + /// + /// Initializes a new instance of the class. + /// + internal TaskNotifier() + { + } + + private Task? task; + + /// + Task? ITaskNotifier>.Task + { + get => this.task; + set => this.task = value; + } + + /// + /// Unwraps the value stored in the current instance. + /// + /// The input instance. + public static implicit operator Task?(TaskNotifier? notifier) + { + return notifier?.task; + } + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs new file mode 100644 index 00000000000..c34fc1ed945 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs @@ -0,0 +1,30 @@ +// 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.ComponentModel; + +namespace Microsoft.Toolkit.Mvvm.ComponentModel +{ + /// + /// An attribute that indicates that a given type should implement the interface and + /// have minimal built-in functionality to support it. This includes exposing the necessary event and having two methods + /// to raise it that mirror and + /// . For more extensive support, use . + /// + /// This attribute can be used as follows: + /// + /// [INotifyPropertyChanged] + /// partial class MyViewModel : SomeOtherClass + /// { + /// // Other members here... + /// } + /// + /// + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public sealed class INotifyPropertyChangedAttribute : Attribute + { + } +} diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs index 7a66443b7cb..372de49473e 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs @@ -8,7 +8,7 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel { /// - /// An attribute that indicates that a given type should have all the members from \ + /// An attribute that indicates that a given type should have all the members from /// generated into it, as well as the and /// interfaces. This can be useful when you want the same functionality from into a class /// that already inherits from another one (since C# doesn't support multiple inheritance). This attribute will trigger From a045fd67f05a962c6af3bcce858cdd65c26d9bfd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 21 Mar 2021 22:40:34 +0100 Subject: [PATCH 06/89] Code refactoring, improved generator modularization --- .../ObservableObjectGenerator.cs | 125 +--------------- .../TransitiveMembersGenerator.cs | 136 ++++++++++++++++++ 2 files changed, 138 insertions(+), 123 deletions(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs index cc4034982fc..fa0bddea026 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs @@ -1,15 +1,5 @@ -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; +using Microsoft.CodeAnalysis; using Microsoft.Toolkit.Mvvm.ComponentModel; -using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; -using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -17,118 +7,7 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators /// A source generator for the type. /// [Generator] - public class ObservableObjectGenerator : ISourceGenerator + public class ObservableObjectGenerator : TransitiveMembersGenerator { - /// - public void Initialize(GeneratorInitializationContext context) - { - } - - /// - public void Execute(GeneratorExecutionContext context) - { - // Find all the [ObservableObject] usages - IEnumerable attributes = - from syntaxTree in context.Compilation.SyntaxTrees - let semanticModel = context.Compilation.GetSemanticModel(syntaxTree) - from attribute in syntaxTree.GetRoot().DescendantNodes().OfType() - let typeInfo = semanticModel.GetTypeInfo(attribute) - where typeInfo.Type is { Name: nameof(ObservableObjectAttribute) } - select attribute; - - SyntaxTree? observableObjectSyntaxTree = null; - - foreach (AttributeSyntax attribute in attributes) - { - // Load the ObservableObject syntax tree if needed - if (observableObjectSyntaxTree is null) - { - string filename = "Microsoft.Toolkit.Mvvm.SourceGenerators.Resources.ObservableObject.cs"; - - Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename); - StreamReader reader = new(stream); - - string observableObjectSource = reader.ReadToEnd(); - - observableObjectSyntaxTree = CSharpSyntaxTree.ParseText(observableObjectSource); - } - - ClassDeclarationSyntax classDeclaration = attribute.FirstAncestorOrSelf()!; - SemanticModel semanticModel = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree); - INamedTypeSymbol classDeclarationSymbol = semanticModel.GetDeclaredSymbol(classDeclaration)!; - - OnExecute(context, classDeclaration, classDeclarationSymbol, observableObjectSyntaxTree); - } - } - - /// - /// Processes a given target type. - /// - /// The input instance to use. - /// The node to process. - /// The for . - /// The for the parsed source. - private static void OnExecute( - GeneratorExecutionContext context, - ClassDeclarationSyntax classDeclaration, - INamedTypeSymbol classDeclarationSymbol, - SyntaxTree observableObjectSyntaxTree) - { - ClassDeclarationSyntax observableObjectDeclaration = observableObjectSyntaxTree.GetRoot().DescendantNodes().OfType().First(); - UsingDirectiveSyntax[] usingDirectives = observableObjectSyntaxTree.GetRoot().DescendantNodes().OfType().ToArray(); - - // Create the class declaration for the user type. This will produce a tree as follows: - // - // : INotifyPropertyChanged, INotifyPropertyChanging - // { - // - // } - var classDeclarationSyntax = - ClassDeclaration(classDeclaration.Identifier.Text) - .WithModifiers(classDeclaration.Modifiers) - .WithBaseList(observableObjectDeclaration.BaseList) - .WithMembers(observableObjectDeclaration.Members); - - TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; - - // Add all parent types in ascending order, if any - foreach (var parentType in classDeclaration.Ancestors().OfType()) - { - typeDeclarationSyntax = parentType - .WithMembers(SingletonList(typeDeclarationSyntax)) - .WithConstraintClauses(List()) - .WithBaseList(null) - .WithAttributeLists(List()) - .WithoutTrivia(); - } - - // Create the compilation unit with the namespace and target member. - // From this, we can finally generate the source code to output. - var namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)); - - var source = - CompilationUnit() - .AddMembers(NamespaceDeclaration(IdentifierName(namespaceName)) - .AddMembers(typeDeclarationSyntax)) - .NormalizeWhitespace() - .AddUsings(usingDirectives.First().WithLeadingTrivia(TriviaList( - Comment("// Licensed to the .NET Foundation under one or more agreements."), - CarriageReturnLineFeed, - Comment("// The .NET Foundation licenses this file to you under the MIT license."), - CarriageReturnLineFeed, - Comment("// See the LICENSE file in the project root for more information."), - CarriageReturnLineFeed, - CarriageReturnLineFeed, - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true) - .WithPragmaKeyword(Token(TriviaList(), SyntaxKind.PragmaKeyword, TriviaList(Space))) - .WithWarningKeyword(Token(TriviaList(), SyntaxKind.WarningKeyword, TriviaList(Space))) - .WithEndOfDirectiveToken(Token(TriviaList(), SyntaxKind.EndOfDirectiveToken, TriviaList(CarriageReturnLineFeed)))), - CarriageReturnLineFeed))) - .AddUsings(usingDirectives.Skip(1).ToArray()) - .ToFullString(); - - // Add the partial type - context.AddSource($"[{nameof(ObservableObjectAttribute)}]_[{classDeclaration.Identifier.Text}].cs", SourceText.From(source, Encoding.UTF8)); - } } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs new file mode 100644 index 00000000000..8b0b23f4918 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + /// A source generator for the type. + /// + /// The type of the source attribute to look for. + public abstract class TransitiveMembersGenerator : ISourceGenerator + where TAttribute : Attribute + { + /// + public void Initialize(GeneratorInitializationContext context) + { + } + + /// + public void Execute(GeneratorExecutionContext context) + { + // Find all the target attribute usages + IEnumerable attributes = + from syntaxTree in context.Compilation.SyntaxTrees + let semanticModel = context.Compilation.GetSemanticModel(syntaxTree) + from attribute in syntaxTree.GetRoot().DescendantNodes().OfType() + let typeInfo = semanticModel.GetTypeInfo(attribute) + where typeInfo.Type?.Name == typeof(TAttribute).Name + select attribute; + + SyntaxTree? sourceSyntaxTree = null; + + foreach (AttributeSyntax attribute in attributes) + { + // Load the source syntax tree if needed + if (sourceSyntaxTree is null) + { + string filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.Resources.{typeof(TAttribute).Name}.cs"; + + Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename); + StreamReader reader = new(stream); + + string observableObjectSource = reader.ReadToEnd(); + + sourceSyntaxTree = CSharpSyntaxTree.ParseText(observableObjectSource); + } + + ClassDeclarationSyntax classDeclaration = attribute.FirstAncestorOrSelf()!; + SemanticModel semanticModel = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree); + INamedTypeSymbol classDeclarationSymbol = semanticModel.GetDeclaredSymbol(classDeclaration)!; + + OnExecute(context, classDeclaration, classDeclarationSymbol, sourceSyntaxTree); + } + } + + /// + /// Processes a given target type. + /// + /// The input instance to use. + /// The node to process. + /// The for . + /// The for the target parsed source. + private static void OnExecute( + GeneratorExecutionContext context, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + SyntaxTree sourceSyntaxTree) + { + ClassDeclarationSyntax sourceDeclaration = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().First(); + UsingDirectiveSyntax[] usingDirectives = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().ToArray(); + + // Create the class declaration for the user type. This will produce a tree as follows: + // + // : + // { + // + // } + var classDeclarationSyntax = + ClassDeclaration(classDeclaration.Identifier.Text) + .WithModifiers(classDeclaration.Modifiers) + .WithBaseList(sourceDeclaration.BaseList) + .WithMembers(sourceDeclaration.Members); + + TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; + + // Add all parent types in ascending order, if any + foreach (var parentType in classDeclaration.Ancestors().OfType()) + { + typeDeclarationSyntax = parentType + .WithMembers(SingletonList(typeDeclarationSyntax)) + .WithConstraintClauses(List()) + .WithBaseList(null) + .WithAttributeLists(List()) + .WithoutTrivia(); + } + + // Create the compilation unit with the namespace and target member. + // From this, we can finally generate the source code to output. + var namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)); + + var source = + CompilationUnit() + .AddMembers(NamespaceDeclaration(IdentifierName(namespaceName)) + .AddMembers(typeDeclarationSyntax)) + .NormalizeWhitespace() + .AddUsings(usingDirectives.First().WithLeadingTrivia(TriviaList( + Comment("// Licensed to the .NET Foundation under one or more agreements."), + CarriageReturnLineFeed, + Comment("// The .NET Foundation licenses this file to you under the MIT license."), + CarriageReturnLineFeed, + Comment("// See the LICENSE file in the project root for more information."), + CarriageReturnLineFeed, + CarriageReturnLineFeed, + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true) + .WithPragmaKeyword(Token(TriviaList(), SyntaxKind.PragmaKeyword, TriviaList(Space))) + .WithWarningKeyword(Token(TriviaList(), SyntaxKind.WarningKeyword, TriviaList(Space))) + .WithEndOfDirectiveToken(Token(TriviaList(), SyntaxKind.EndOfDirectiveToken, TriviaList(CarriageReturnLineFeed)))), + CarriageReturnLineFeed))) + .AddUsings(usingDirectives.Skip(1).ToArray()) + .ToFullString(); + + // Add the partial type + context.AddSource($"[{typeof(TAttribute).Name}]_[{classDeclaration.Identifier.Text}].cs", SourceText.From(source, Encoding.UTF8)); + } + } +} From a1358f433a5344e57d7f427d50db96efaa71c96f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 21 Mar 2021 22:45:59 +0100 Subject: [PATCH 07/89] Added generator for [INotifyPropertyChanged] --- .../INotifyPropertyChangedGenerator.cs | 13 ++++ ...osoft.Toolkit.Mvvm.SourceGenerators.csproj | 4 ++ .../INotifyPropertyChanged.cs} | 4 +- .../Test_INotifyPropertyChangedAttribute.cs | 65 +++++++++++++++++++ 4 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/INotifyPropertyChangedGenerator.cs rename Microsoft.Toolkit.Mvvm.SourceGenerators/{Microsoft.Toolkit.Mvvm/NotifyPropertyChangedObject.cs => Resources/INotifyPropertyChanged.cs} (99%) create mode 100644 UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/INotifyPropertyChangedGenerator.cs new file mode 100644 index 00000000000..28242d1c0d6 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/INotifyPropertyChangedGenerator.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; +using Microsoft.Toolkit.Mvvm.ComponentModel; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + /// A source generator for the type. + /// + [Generator] + public class INotifyPropertyChangedGenerator : TransitiveMembersGenerator + { + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index f2e0bbe242f..ecf854918e5 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -8,6 +8,7 @@ + @@ -15,6 +16,9 @@ PreserveNewest + + PreserveNewest + diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm/NotifyPropertyChangedObject.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Resources/INotifyPropertyChanged.cs similarity index 99% rename from Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm/NotifyPropertyChangedObject.cs rename to Microsoft.Toolkit.Mvvm.SourceGenerators/Resources/INotifyPropertyChanged.cs index e32dc07672e..2551afc9302 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm/NotifyPropertyChangedObject.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Resources/INotifyPropertyChanged.cs @@ -8,12 +8,14 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; +#pragma warning disable SA1300, SA1649 + namespace Microsoft.Toolkit.Mvvm.ComponentModel { /// /// A base class for objects implementing . /// - public abstract class NotifyPropertyChangedObject : INotifyPropertyChanged + public abstract class __NotifyPropertyChanged : INotifyPropertyChanged { /// public event PropertyChangedEventHandler? PropertyChanged; diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs new file mode 100644 index 00000000000..1579687130b --- /dev/null +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs @@ -0,0 +1,65 @@ +// 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.ComponentModel; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.Mvvm +{ + [TestClass] + public partial class Test_INotifyPropertyChangedAttribute + { + [TestCategory("Mvvm")] + [TestMethod] + public void Test_INotifyPropertyChanged_Events() + { + var model = new SampleModel(); + + (PropertyChangingEventArgs, int) changing = default; + (PropertyChangedEventArgs, int) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Data); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Data); + }; + + model.Data = 42; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Data)); + Assert.AreEqual(changing.Item2, 0); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Data)); + Assert.AreEqual(changed.Item2, 42); + } + + [INotifyPropertyChanged] + public partial class SampleModel + { + private int data; + + public int Data + { + get => data; + set => SetProperty(ref data, value); + } + } + } +} From d27556f1f259df1801ea1361d31dc781046c31d1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 21 Mar 2021 22:52:36 +0100 Subject: [PATCH 08/89] Minor fixes to code generators and unit tests --- .../TransitiveMembersGenerator.cs | 2 +- .../ComponentModel/ObservableObject.cs | 7 +++++++ .../Mvvm/Test_INotifyPropertyChangedAttribute.cs | 14 -------------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs index 8b0b23f4918..fb748534d79 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs @@ -45,7 +45,7 @@ from attribute in syntaxTree.GetRoot().DescendantNodes().OfType // Load the source syntax tree if needed if (sourceSyntaxTree is null) { - string filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.Resources.{typeof(TAttribute).Name}.cs"; + string filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.Resources.{typeof(TAttribute).Name.Replace("Attribute", string.Empty)}.cs"; Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename); StreamReader reader = new(stream); diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs index c7e4f053306..1e992221eb8 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs @@ -7,6 +7,13 @@ // This file is inspired from the MvvmLight library (lbugnion/MvvmLight), // more info in ThirdPartyNotices.txt in the root of the project. +// ================================== NOTE ================================== +// This file is mirrored in the trimmed-down INotifyPropertyChanged file in +// the source generator project, to be used with the [INotifyPropertyChanged] +// attribute. If any changes are made to this file, they should also be +// appropriately ported to that file as well to keep the behavior consistent. +// ========================================================================== + using System; using System.Collections.Generic; using System.ComponentModel; diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs index 1579687130b..5eec7a6c6f6 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs @@ -17,20 +17,8 @@ public void Test_INotifyPropertyChanged_Events() { var model = new SampleModel(); - (PropertyChangingEventArgs, int) changing = default; (PropertyChangedEventArgs, int) changed = default; - model.PropertyChanging += (s, e) => - { - Assert.IsNull(changing.Item1); - Assert.IsNull(changed.Item1); - Assert.AreSame(model, s); - Assert.IsNotNull(s); - Assert.IsNotNull(e); - - changing = (e, model.Data); - }; - model.PropertyChanged += (s, e) => { Assert.IsNotNull(changing.Item1); @@ -44,8 +32,6 @@ public void Test_INotifyPropertyChanged_Events() model.Data = 42; - Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Data)); - Assert.AreEqual(changing.Item2, 0); Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Data)); Assert.AreEqual(changed.Item2, 42); } From 684755e4299770ff4b24003bc46081fb64350e92 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 21 Mar 2021 23:11:00 +0100 Subject: [PATCH 09/89] Added support for custom members filtering --- .../TransitiveMembersGenerator.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs index fb748534d79..87780a21e24 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs @@ -58,8 +58,9 @@ from attribute in syntaxTree.GetRoot().DescendantNodes().OfType ClassDeclarationSyntax classDeclaration = attribute.FirstAncestorOrSelf()!; SemanticModel semanticModel = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree); INamedTypeSymbol classDeclarationSymbol = semanticModel.GetDeclaredSymbol(classDeclaration)!; + AttributeData attributeData = classDeclarationSymbol.GetAttributes().First(a => a.ApplicationSyntaxReference?.GetSyntax() == attribute); - OnExecute(context, classDeclaration, classDeclarationSymbol, sourceSyntaxTree); + OnExecute(context, attributeData, classDeclaration, classDeclarationSymbol, sourceSyntaxTree); } } @@ -67,11 +68,13 @@ from attribute in syntaxTree.GetRoot().DescendantNodes().OfType /// Processes a given target type. /// /// The input instance to use. + /// The for the current attribute being processed. /// The node to process. /// The for . /// The for the target parsed source. - private static void OnExecute( + private void OnExecute( GeneratorExecutionContext context, + AttributeData attributeData, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, SyntaxTree sourceSyntaxTree) @@ -89,7 +92,7 @@ private static void OnExecute( ClassDeclaration(classDeclaration.Identifier.Text) .WithModifiers(classDeclaration.Modifiers) .WithBaseList(sourceDeclaration.BaseList) - .WithMembers(sourceDeclaration.Members); + .AddMembers(FilterDeclaredMembers(attributeData, sourceDeclaration).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -132,5 +135,16 @@ private static void OnExecute( // Add the partial type context.AddSource($"[{typeof(TAttribute).Name}]_[{classDeclaration.Identifier.Text}].cs", SourceText.From(source, Encoding.UTF8)); } + + /// + /// Filters the nodes to generate from the input parsed tree. + /// + /// The for the current attribute being processed. + /// The parsed instance with the source nodes. + /// A sequence of nodes to emit in the generated file. + protected virtual IEnumerable FilterDeclaredMembers(AttributeData attributeData, ClassDeclarationSyntax sourceDeclaration) + { + return sourceDeclaration.Members; + } } } From d1d32f0f6125792ab83cce0a779bf9d9320de347 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 21 Mar 2021 23:30:33 +0100 Subject: [PATCH 10/89] Added INotifyPropertyChangedAttribute.IncludeAdditionalHelperMethods --- .../INotifyPropertyChangedGenerator.cs | 25 ++++++++++++++++- .../INotifyPropertyChangedAttribute.cs | 8 ++++++ .../Test_INotifyPropertyChangedAttribute.cs | 28 ++++++++++++++++++- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/INotifyPropertyChangedGenerator.cs index 28242d1c0d6..cd31b99dd8a 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/INotifyPropertyChangedGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/INotifyPropertyChangedGenerator.cs @@ -1,4 +1,7 @@ -using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Toolkit.Mvvm.ComponentModel; namespace Microsoft.Toolkit.Mvvm.SourceGenerators @@ -9,5 +12,25 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators [Generator] public class INotifyPropertyChangedGenerator : TransitiveMembersGenerator { + /// + protected override IEnumerable FilterDeclaredMembers(AttributeData attributeData, ClassDeclarationSyntax sourceDeclaration) + { + foreach (KeyValuePair properties in attributeData.NamedArguments) + { + if (properties.Key == nameof(INotifyPropertyChangedAttribute.IncludeAdditionalHelperMethods) && + properties.Value.Value is bool includeHelpers && !includeHelpers) + { + // If requested, only include the event and the basic methods to raise it, but not the additional helpers + return sourceDeclaration.Members.Where(static member => + { + return member + is EventFieldDeclarationSyntax + or MethodDeclarationSyntax { Identifier: { ValueText: "OnPropertyChanged" } }; + }); + } + } + + return sourceDeclaration.Members; + } } } diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs index c34fc1ed945..df68a1d2b9b 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs @@ -26,5 +26,13 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public sealed class INotifyPropertyChangedAttribute : Attribute { + /// + /// Gets or sets a value indicating whether or not to also generate all the additional helper methods that are found + /// in as well (eg. . + /// If set to , only the event and + /// the two overloads will be generated. + /// The default value is . + /// + public bool IncludeAdditionalHelperMethods { get; set; } = true; } } diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs index 5eec7a6c6f6..5eba0389465 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_INotifyPropertyChangedAttribute.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.ComponentModel; +using System.Reflection; using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -21,7 +22,6 @@ public void Test_INotifyPropertyChanged_Events() model.PropertyChanged += (s, e) => { - Assert.IsNotNull(changing.Item1); Assert.IsNull(changed.Item1); Assert.AreSame(model, s); Assert.IsNotNull(s); @@ -47,5 +47,31 @@ public int Data set => SetProperty(ref data, value); } } + + [TestCategory("Mvvm")] + [TestMethod] + public void Test_INotifyPropertyChanged_WithoutHelpers() + { + Assert.IsTrue(typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(SampleModelWithoutHelpers))); + Assert.IsFalse(typeof(INotifyPropertyChanging).IsAssignableFrom(typeof(SampleModelWithoutHelpers))); + + // This just needs to check that it compiles + _ = nameof(SampleModelWithoutHelpers.PropertyChanged); + + var methods = typeof(SampleModelWithoutHelpers).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly); + + Assert.AreEqual(methods.Length, 2); + Assert.AreEqual(methods[0].Name, "OnPropertyChanged"); + Assert.AreEqual(methods[1].Name, "OnPropertyChanged"); + + var types = typeof(SampleModelWithoutHelpers).GetNestedTypes(BindingFlags.NonPublic); + + Assert.AreEqual(types.Length, 0); + } + + [INotifyPropertyChanged(IncludeAdditionalHelperMethods = false)] + public partial class SampleModelWithoutHelpers + { + } } } From baa62cdb29520b4e7c827dc2f5311c2e51384a3c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 00:43:12 +0100 Subject: [PATCH 11/89] Added [ObservableRecipient] attribute, code refactoring --- ...osoft.Toolkit.Mvvm.SourceGenerators.csproj | 13 +++++- .../ObservableRecipientGenerator.cs | 13 ++++++ .../INotifyPropertyChangedAttribute.cs | 2 + .../Attributes/ObservableObjectAttribute.cs | 2 + .../ObservableRecipientAttribute.cs | 40 +++++++++++++++++++ 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableRecipientGenerator.cs create mode 100644 Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index ecf854918e5..f531ac0b98f 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -7,16 +7,25 @@ - + + + + + + + + + PreserveNewest + PreserveNewest - + PreserveNewest diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableRecipientGenerator.cs new file mode 100644 index 00000000000..68599217f86 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableRecipientGenerator.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; +using Microsoft.Toolkit.Mvvm.ComponentModel; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + /// A source generator for the type. + /// + [Generator] + public class ObservableRecipientGenerator : TransitiveMembersGenerator + { + } +} diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs index df68a1d2b9b..a92e4954430 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#pragma warning disable CS1574 + using System; using System.ComponentModel; diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs index 372de49473e..6d20df0507b 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#pragma warning disable CS1574 + using System; using System.ComponentModel; diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs new file mode 100644 index 00000000000..0cb43405528 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs @@ -0,0 +1,40 @@ +// 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. + +#pragma warning disable CS1574 + +using System; + +namespace Microsoft.Toolkit.Mvvm.ComponentModel +{ + /// + /// An attribute that indicates that a given type should have all the members from + /// generated into it. This can be useful when you want the same functionality from into + /// a class that already inherits from another one (since C# doesn't support multiple inheritance). This attribute will trigger + /// the source generator to just create the same APIs directly into the decorated class. For instance, this attribute can be + /// used to easily combine the functionality from both and , + /// by using as the base class and adding this attribute to the declared type. + /// + /// This attribute can be used as follows: + /// + /// [ObservableRecipient] + /// partial class MyViewModel : ObservableValidator + /// { + /// // Other members here... + /// } + /// + /// + /// And with this, the same APIs from will be available on this type as well. + /// + /// + /// In order to work, needs to be applied to a type that inherits from + /// (either directly or indirectly), or to one decorated with . + /// This is because the methods rely on some of the inherited members to work. + /// If this condition is not met, the code will fail to build. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public sealed class ObservableRecipientAttribute : Attribute + { + } +} From cd29cef57cdfc79f9c4e95864246033d56bc541c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 01:36:35 +0100 Subject: [PATCH 12/89] Added [ObservableRecipient] tests, minor bug fixes --- .../ObservableRecipientGenerator.cs | 10 ++- .../TransitiveMembersGenerator.cs | 11 ++- .../Mvvm/Test_ObservableObjectAttribute.cs | 2 +- .../Mvvm/Test_ObservableRecipientAttribute.cs | 72 +++++++++++++++++++ 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableRecipientGenerator.cs index 68599217f86..1bb725c6cfe 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableRecipientGenerator.cs @@ -1,4 +1,7 @@ -using Microsoft.CodeAnalysis; +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Toolkit.Mvvm.ComponentModel; namespace Microsoft.Toolkit.Mvvm.SourceGenerators @@ -9,5 +12,10 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators [Generator] public class ObservableRecipientGenerator : TransitiveMembersGenerator { + /// + protected override IEnumerable FilterDeclaredMembers(AttributeData attributeData, ClassDeclarationSyntax sourceDeclaration) + { + return sourceDeclaration.Members.Where(static member => member is not ConstructorDeclarationSyntax); + } } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs index 87780a21e24..a940ce39c94 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs @@ -81,6 +81,15 @@ private void OnExecute( { ClassDeclarationSyntax sourceDeclaration = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().First(); UsingDirectiveSyntax[] usingDirectives = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().ToArray(); + BaseListSyntax baseListSyntax = BaseList(SeparatedList( + sourceDeclaration.BaseList?.Types + .OfType() + .Select(static t => t.Type) + .OfType() + .Where(static t => t.Identifier.ValueText.StartsWith("I")) + .Select(static t => SimpleBaseType(t)) + .ToArray() + ?? Array.Empty())); // Create the class declaration for the user type. This will produce a tree as follows: // @@ -91,7 +100,7 @@ private void OnExecute( var classDeclarationSyntax = ClassDeclaration(classDeclaration.Identifier.Text) .WithModifiers(classDeclaration.Modifiers) - .WithBaseList(sourceDeclaration.BaseList) + .WithBaseList(baseListSyntax) .AddMembers(FilterDeclaredMembers(attributeData, sourceDeclaration).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs index 2c638cbe54c..dfb49ec6acc 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs @@ -13,7 +13,7 @@ public partial class Test_ObservableObjectAttribute { [TestCategory("Mvvm")] [TestMethod] - public void Test_ObservableObject_Events() + public void Test_ObservableObjectAttribute_Events() { var model = new SampleModel(); diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs new file mode 100644 index 00000000000..4bf82b58cdc --- /dev/null +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs @@ -0,0 +1,72 @@ +// 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.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Messaging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.Mvvm +{ + [TestClass] + public partial class Test_ObservableRecipientAttribute + { + [TestCategory("Mvvm")] + [TestMethod] + public void Test_ObservableRecipientAttribute_Events() + { + var model = new Person(); + var args = new List(); + + model.PropertyChanged += (s, e) => args.Add(e); + + Assert.IsFalse(model.HasErrors); + + model.Name = "No"; + + Assert.IsTrue(model.HasErrors); + Assert.AreEqual(args.Count, 2); + Assert.AreEqual(args[0].PropertyName, nameof(Person.Name)); + Assert.AreEqual(args[1].PropertyName, nameof(INotifyDataErrorInfo.HasErrors)); + + model.Name = "Valid"; + + Assert.IsFalse(model.HasErrors); + Assert.AreEqual(args.Count, 4); + Assert.AreEqual(args[2].PropertyName, nameof(Person.Name)); + Assert.AreEqual(args[3].PropertyName, nameof(INotifyDataErrorInfo.HasErrors)); + + Assert.IsNotNull(typeof(Person).GetProperty("Messenger", BindingFlags.Instance | BindingFlags.NonPublic)); + } + + [ObservableRecipient] + public partial class Person : ObservableValidator + { + public Person() + { + Messenger = WeakReferenceMessenger.Default; + } + + private string name; + + [MinLength(4)] + [MaxLength(20)] + [Required] + public string Name + { + get => this.name; + set => SetProperty(ref this.name, value, true); + } + + public void TestCompile() + { + // Validates that the method Broadcast is correctly being generated + Broadcast(0, 1, nameof(TestCompile)); + } + } + } +} From 8b415a2c23ed22f764b9870fe87f2b923e903214 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 01:51:57 +0100 Subject: [PATCH 13/89] Fixed an issue with base type list formatting --- .../TransitiveMembersGenerator.cs | 7 ++++++- .../Attributes/ObservableRecipientAttribute.cs | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs index a940ce39c94..03add8dd503 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs @@ -81,7 +81,7 @@ private void OnExecute( { ClassDeclarationSyntax sourceDeclaration = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().First(); UsingDirectiveSyntax[] usingDirectives = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().ToArray(); - BaseListSyntax baseListSyntax = BaseList(SeparatedList( + BaseListSyntax? baseListSyntax = BaseList(SeparatedList( sourceDeclaration.BaseList?.Types .OfType() .Select(static t => t.Type) @@ -91,6 +91,11 @@ private void OnExecute( .ToArray() ?? Array.Empty())); + if (baseListSyntax.Types.Count == 0) + { + baseListSyntax = null; + } + // Create the class declaration for the user type. This will produce a tree as follows: // // : diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs index 0cb43405528..40e658bf082 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs @@ -26,6 +26,10 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// /// /// And with this, the same APIs from will be available on this type as well. + /// + /// To avoid conflicts with other APIs in types where the new members are being generated, constructors are omitted. Make sure to + /// properly initialize the property from the constructors in the type being annotated. + /// /// /// /// In order to work, needs to be applied to a type that inherits from From e8cccceb55b526a907934ef13321dbafe9620114 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 01:58:38 +0100 Subject: [PATCH 14/89] Minor code refactoring --- .../INotifyPropertyChangedGenerator.cs | 0 .../ObservableObjectGenerator.cs | 0 .../ObservableRecipientGenerator.cs | 0 .../TransitiveMembersGenerator.cs | 2 +- .../INotifyPropertyChanged.cs | 0 ...rosoft.Toolkit.Mvvm.SourceGenerators.csproj | 18 +++++++++--------- 6 files changed, 10 insertions(+), 10 deletions(-) rename Microsoft.Toolkit.Mvvm.SourceGenerators/{ => ComponentModel}/INotifyPropertyChangedGenerator.cs (100%) rename Microsoft.Toolkit.Mvvm.SourceGenerators/{ => ComponentModel}/ObservableObjectGenerator.cs (100%) rename Microsoft.Toolkit.Mvvm.SourceGenerators/{ => ComponentModel}/ObservableRecipientGenerator.cs (100%) rename Microsoft.Toolkit.Mvvm.SourceGenerators/{ => ComponentModel}/TransitiveMembersGenerator.cs (98%) rename Microsoft.Toolkit.Mvvm.SourceGenerators/{Resources => EmbeddedResources}/INotifyPropertyChanged.cs (100%) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs similarity index 100% rename from Microsoft.Toolkit.Mvvm.SourceGenerators/INotifyPropertyChangedGenerator.cs rename to Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs similarity index 100% rename from Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableObjectGenerator.cs rename to Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs similarity index 100% rename from Microsoft.Toolkit.Mvvm.SourceGenerators/ObservableRecipientGenerator.cs rename to Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs similarity index 98% rename from Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs rename to Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 03add8dd503..301d29f5a07 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -45,7 +45,7 @@ from attribute in syntaxTree.GetRoot().DescendantNodes().OfType // Load the source syntax tree if needed if (sourceSyntaxTree is null) { - string filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.Resources.{typeof(TAttribute).Name.Replace("Attribute", string.Empty)}.cs"; + string filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.EmbeddedResources.{typeof(TAttribute).Name.Replace("Attribute", string.Empty)}.cs"; Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename); StreamReader reader = new(stream); diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Resources/INotifyPropertyChanged.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs similarity index 100% rename from Microsoft.Toolkit.Mvvm.SourceGenerators/Resources/INotifyPropertyChanged.cs rename to Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index f531ac0b98f..cb02cc8daa2 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -7,25 +7,25 @@ - - - + + + - - - + + + - + PreserveNewest - + PreserveNewest - + PreserveNewest From 1b621d27b2d226c8a583839a828834f3ccaee5a3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 12:27:26 +0100 Subject: [PATCH 15/89] Enabled constructor generation for [ObservableRecipient] --- .../INotifyPropertyChangedGenerator.cs | 6 ++- .../ObservableRecipientGenerator.cs | 44 ++++++++++++++++++- .../TransitiveMembersGenerator.cs | 10 ++++- .../ObservableRecipientAttribute.cs | 6 ++- .../Mvvm/Test_ObservableRecipientAttribute.cs | 32 ++++++++++++++ 5 files changed, 91 insertions(+), 7 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs index cd31b99dd8a..e9dcb26559c 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs @@ -13,7 +13,11 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators public class INotifyPropertyChangedGenerator : TransitiveMembersGenerator { /// - protected override IEnumerable FilterDeclaredMembers(AttributeData attributeData, ClassDeclarationSyntax sourceDeclaration) + protected override IEnumerable FilterDeclaredMembers( + AttributeData attributeData, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + ClassDeclarationSyntax sourceDeclaration) { foreach (KeyValuePair properties in attributeData.NamedArguments) { diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs index 1bb725c6cfe..e939884a06a 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs @@ -1,8 +1,10 @@ using System.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Toolkit.Mvvm.ComponentModel; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -13,9 +15,47 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators public class ObservableRecipientGenerator : TransitiveMembersGenerator { /// - protected override IEnumerable FilterDeclaredMembers(AttributeData attributeData, ClassDeclarationSyntax sourceDeclaration) + protected override IEnumerable FilterDeclaredMembers( + AttributeData attributeData, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + ClassDeclarationSyntax sourceDeclaration) { - return sourceDeclaration.Members.Where(static member => member is not ConstructorDeclarationSyntax); + // If the target type has no constructors, generate constructors as well + if (classDeclarationSymbol.InstanceConstructors.Length == 1 && + classDeclarationSymbol.InstanceConstructors[0] is + { + Parameters: { IsEmpty: true }, + DeclaringSyntaxReferences: { IsEmpty: true }, + IsImplicitlyDeclared: true + }) + { + foreach (ConstructorDeclarationSyntax ctor in sourceDeclaration.Members.OfType()) + { + string + text = ctor.NormalizeWhitespace().ToFullString(), + replaced = text.Replace("ObservableRecipient", classDeclarationSymbol.Name); + + ConstructorDeclarationSyntax updatedCtor = (ConstructorDeclarationSyntax)ParseMemberDeclaration(replaced)!; + + // Adjust the visibility of the constructors based on whether the target type is abstract. + // If that is not the case, the constructors have to be declared as public and not protected. + if (classDeclarationSymbol.IsAbstract) + { + yield return updatedCtor; + } + else + { + yield return updatedCtor.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))); + } + } + } + + // If the target type has at least one custom constructor, only generate methods + foreach (MemberDeclarationSyntax member in sourceDeclaration.Members.Where(static member => member is not ConstructorDeclarationSyntax)) + { + yield return member; + } } } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 301d29f5a07..51f609d3f82 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -106,7 +106,7 @@ private void OnExecute( ClassDeclaration(classDeclaration.Identifier.Text) .WithModifiers(classDeclaration.Modifiers) .WithBaseList(baseListSyntax) - .AddMembers(FilterDeclaredMembers(attributeData, sourceDeclaration).ToArray()); + .AddMembers(FilterDeclaredMembers(attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -154,9 +154,15 @@ private void OnExecute( /// Filters the nodes to generate from the input parsed tree. /// /// The for the current attribute being processed. + /// The node to process. + /// The for . /// The parsed instance with the source nodes. /// A sequence of nodes to emit in the generated file. - protected virtual IEnumerable FilterDeclaredMembers(AttributeData attributeData, ClassDeclarationSyntax sourceDeclaration) + protected virtual IEnumerable FilterDeclaredMembers( + AttributeData attributeData, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + ClassDeclarationSyntax sourceDeclaration) { return sourceDeclaration.Members; } diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs index 40e658bf082..05a8242a309 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs @@ -27,8 +27,10 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// /// And with this, the same APIs from will be available on this type as well. /// - /// To avoid conflicts with other APIs in types where the new members are being generated, constructors are omitted. Make sure to - /// properly initialize the property from the constructors in the type being annotated. + /// To avoid conflicts with other APIs in types where the new members are being generated, constructors are only generated when the annotated + /// type doesn't have any explicit constructors being declared. If that is the case, the same constructors from + /// are emitted, with the accessibility adapted to that of the annotated type. Otherwise, they are skipped, so the type being annotated has the + /// respondibility of properly initializing the property. /// /// /// diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs index 4bf82b58cdc..55d2b5106fe 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Reflection; using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Mvvm.Messaging; @@ -41,6 +42,7 @@ public void Test_ObservableRecipientAttribute_Events() Assert.AreEqual(args[3].PropertyName, nameof(INotifyDataErrorInfo.HasErrors)); Assert.IsNotNull(typeof(Person).GetProperty("Messenger", BindingFlags.Instance | BindingFlags.NonPublic)); + Assert.AreEqual(typeof(Person).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Length, 1); } [ObservableRecipient] @@ -68,5 +70,35 @@ public void TestCompile() Broadcast(0, 1, nameof(TestCompile)); } } + + [TestCategory("Mvvm")] + [TestMethod] + public void Test_ObservableRecipientAttribute_AbstractConstructors() + { + var ctors = typeof(AbstractPerson).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic); + + Assert.AreEqual(ctors.Length, 2); + Assert.IsTrue(ctors.All(static ctor => ctor.IsFamily)); + } + + [ObservableRecipient] + public abstract partial class AbstractPerson : ObservableObject + { + } + + [TestCategory("Mvvm")] + [TestMethod] + public void Test_ObservableRecipientAttribute_NonAbstractConstructors() + { + var ctors = typeof(NonAbstractPerson).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic); + + Assert.AreEqual(ctors.Length, 2); + Assert.IsTrue(ctors.All(static ctor => ctor.IsPublic)); + } + + [ObservableRecipient] + public partial class NonAbstractPerson : ObservableObject + { + } } } From c6cce1c56b307cd1eb2f732dbd076d0e3949047a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 12:46:27 +0100 Subject: [PATCH 16/89] Skipped ObservableRecipient.SetProperty overloads when conflicting --- .../ObservableRecipientGenerator.cs | 21 +++++++++++++++++++ .../ObservableRecipientAttribute.cs | 4 +++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs index e939884a06a..83cbb228e3e 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs @@ -51,6 +51,27 @@ classDeclarationSymbol.InstanceConstructors[0] is } } + INamedTypeSymbol? baseTypeSymbol = classDeclarationSymbol.BaseType; + + while (baseTypeSymbol != null) + { + // Skip the SetProperty overloads if the target type inherits from ObservableValidator, to avoid conflicts + if (baseTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator") + { + foreach (MemberDeclarationSyntax member in sourceDeclaration.Members.Where(static member => member is not ConstructorDeclarationSyntax)) + { + if (member is not MethodDeclarationSyntax { Identifier: { ValueText: "SetProperty" } }) + { + yield return member; + } + } + + yield break; + } + + baseTypeSymbol = baseTypeSymbol.BaseType; + } + // If the target type has at least one custom constructor, only generate methods foreach (MemberDeclarationSyntax member in sourceDeclaration.Members.Where(static member => member is not ConstructorDeclarationSyntax)) { diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs index 05a8242a309..36cf37d0c3d 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs @@ -30,7 +30,9 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// To avoid conflicts with other APIs in types where the new members are being generated, constructors are only generated when the annotated /// type doesn't have any explicit constructors being declared. If that is the case, the same constructors from /// are emitted, with the accessibility adapted to that of the annotated type. Otherwise, they are skipped, so the type being annotated has the - /// respondibility of properly initializing the property. + /// respondibility of properly initializing the property. Additionally, if the annotated type inherits + /// from , the overloads will be skipped + /// as well, as they would conflict with the methods. /// /// /// From e873484ce22f1c0e8be900407cc213f7f0093994 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 12:53:30 +0100 Subject: [PATCH 17/89] Fixed missing trivia from generated ObservableValidator constructors --- .../ComponentModel/ObservableRecipientGenerator.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs index 83cbb228e3e..ff0d73b5766 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs @@ -36,18 +36,14 @@ classDeclarationSymbol.InstanceConstructors[0] is text = ctor.NormalizeWhitespace().ToFullString(), replaced = text.Replace("ObservableRecipient", classDeclarationSymbol.Name); - ConstructorDeclarationSyntax updatedCtor = (ConstructorDeclarationSyntax)ParseMemberDeclaration(replaced)!; - // Adjust the visibility of the constructors based on whether the target type is abstract. // If that is not the case, the constructors have to be declared as public and not protected. - if (classDeclarationSymbol.IsAbstract) - { - yield return updatedCtor; - } - else + if (!classDeclarationSymbol.IsAbstract) { - yield return updatedCtor.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword))); + replaced = replaced.Replace("protected", "public"); } + + yield return (ConstructorDeclarationSyntax)ParseMemberDeclaration(replaced)!; } } From b86b2424b8809ca6fe8530b848c8a75612a9ce8c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 12:55:18 +0100 Subject: [PATCH 18/89] Fixed errors in unit tests --- .../Mvvm/Test_ObservableRecipientAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs index 55d2b5106fe..c2889fb4795 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableRecipientAttribute.cs @@ -42,7 +42,7 @@ public void Test_ObservableRecipientAttribute_Events() Assert.AreEqual(args[3].PropertyName, nameof(INotifyDataErrorInfo.HasErrors)); Assert.IsNotNull(typeof(Person).GetProperty("Messenger", BindingFlags.Instance | BindingFlags.NonPublic)); - Assert.AreEqual(typeof(Person).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Length, 1); + Assert.AreEqual(typeof(Person).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Length, 0); } [ObservableRecipient] @@ -90,7 +90,7 @@ public abstract partial class AbstractPerson : ObservableObject [TestMethod] public void Test_ObservableRecipientAttribute_NonAbstractConstructors() { - var ctors = typeof(NonAbstractPerson).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic); + var ctors = typeof(NonAbstractPerson).GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Assert.AreEqual(ctors.Length, 2); Assert.IsTrue(ctors.All(static ctor => ctor.IsPublic)); From 49d5e1a5a6af0473fe12688e953c2d8baba1e06d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 13:40:31 +0100 Subject: [PATCH 19/89] Added missing file headers --- .../ComponentModel/INotifyPropertyChangedGenerator.cs | 6 +++++- .../ComponentModel/ObservableObjectGenerator.cs | 6 +++++- .../ComponentModel/ObservableRecipientGenerator.cs | 6 +++++- .../ComponentModel/TransitiveMembersGenerator.cs | 6 +++++- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs index e9dcb26559c..10256f4322b 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs @@ -1,4 +1,8 @@ -using System.Collections.Generic; +// 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.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs index fa0bddea026..1b372e1e6b7 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs @@ -1,4 +1,8 @@ -using Microsoft.CodeAnalysis; +// 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.CodeAnalysis; using Microsoft.Toolkit.Mvvm.ComponentModel; namespace Microsoft.Toolkit.Mvvm.SourceGenerators diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs index ff0d73b5766..34dda1cf74e 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs @@ -1,4 +1,8 @@ -using System.Collections.Generic; +// 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.Collections.Generic; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 51f609d3f82..4ecce99d27a 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -1,4 +1,8 @@ -using System; +// 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.IO; using System.Linq; From a20d09e47505ccfd38b95565529135a69e01bc90 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 13:57:37 +0100 Subject: [PATCH 20/89] Added initial setup for custom diagnostics --- .../AnalyzerReleases.Shipped.md | 3 + .../AnalyzerReleases.Unshipped.md | 10 +++ .../Diagnostics/DiagnosticDescriptors.cs | 62 +++++++++++++++++++ .../Diagnostics/DiagnosticExtensions.cs | 47 ++++++++++++++ ...osoft.Toolkit.Mvvm.SourceGenerators.csproj | 5 ++ 5 files changed, 127 insertions(+) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticExtensions.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000000..d567f14248e --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000000..a5a7067a79d --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,10 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +MVVMTK0001 | Microsoft.Toolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0002 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0003 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 00000000000..018043531c7 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,62 @@ +// 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.CodeAnalysis; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics +{ + /// + /// A container for all instances for errors reported by analyzers in this project. + /// + internal static class DiagnosticDescriptors + { + /// + /// Gets a indicating when failed to run on a given type. + /// + /// Format: "The generator failed to execute on type {0}". + /// + /// + public static readonly DiagnosticDescriptor INotifyPropertyChangedGeneratorError = new( + id: "MVVMTK0001", + title: $"Internal error for {nameof(INotifyPropertyChangedGenerator)}", + messageFormat: $"The generator {nameof(INotifyPropertyChangedGenerator)} failed to execute on type {{0}}", + category: typeof(INotifyPropertyChangedGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"The {nameof(INotifyPropertyChangedGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when failed to run on a given type. + /// + /// Format: "The generator failed to execute on type {0}". + /// + /// + public static readonly DiagnosticDescriptor ObservableObjectGeneratorError = new( + id: "MVVMTK0002", + title: $"Internal error for {nameof(ObservableObjectGenerator)}", + messageFormat: $"The generator {nameof(ObservableObjectGenerator)} failed to execute on type {{0}}", + category: typeof(ObservableObjectGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"The {nameof(ObservableObjectGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when failed to run on a given type. + /// + /// Format: "The generator failed to execute on type {0}". + /// + /// + public static readonly DiagnosticDescriptor ObservableRecipientGeneratorError = new( + id: "MVVMTK0003", + title: $"Internal error for {nameof(ObservableRecipientGenerator)}", + messageFormat: $"The generator {nameof(ObservableRecipientGenerator)} failed to execute on type {{0}}", + category: typeof(ObservableRecipientGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"The {nameof(ObservableRecipientGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticExtensions.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticExtensions.cs new file mode 100644 index 00000000000..5e7e6830f88 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticExtensions.cs @@ -0,0 +1,47 @@ +// 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.Linq; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics +{ + /// + /// Extension methods for , specifically for reporting diagnostics. + /// + internal static class DiagnosticExtensions + { + /// + /// Adds a new diagnostics to the current compilation. + /// + /// The instance currently in use. + /// The input for the diagnostics to create. + /// The source to attach the diagnostics to. + /// The optional arguments for the formatted message to include. + public static void ReportDiagnostic( + this GeneratorExecutionContext context, + DiagnosticDescriptor descriptor, + ISymbol symbol, + params object[] args) + { + context.ReportDiagnostic(Diagnostic.Create(descriptor, symbol.Locations.FirstOrDefault(), args)); + } + + /// + /// Adds a new diagnostics to the current compilation. + /// + /// The instance currently in use. + /// The input for the diagnostics to create. + /// The source to attach the diagnostics to. + /// The optional arguments for the formatted message to include. + public static void ReportDiagnostic( + this GeneratorExecutionContext context, + DiagnosticDescriptor descriptor, + SyntaxNode node, + params object[] args) + { + context.ReportDiagnostic(Diagnostic.Create(descriptor, node.GetLocation(), args)); + } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index cb02cc8daa2..7b440b46a49 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -34,4 +34,9 @@ + + + + + From 6d0eedb0a1f26e5f4bde9dbd3d4b08988042102b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 14:02:25 +0100 Subject: [PATCH 21/89] Enabled diagnostics for failed generators --- .../INotifyPropertyChangedGenerator.cs | 4 ++++ .../ComponentModel/ObservableObjectGenerator.cs | 3 +++ .../ObservableRecipientGenerator.cs | 4 ++++ .../ComponentModel/TransitiveMembersGenerator.cs | 15 ++++++++++++++- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs index 10256f4322b..58f8b113fb0 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Toolkit.Mvvm.ComponentModel; +using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -16,6 +17,9 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators [Generator] public class INotifyPropertyChangedGenerator : TransitiveMembersGenerator { + /// + protected override DiagnosticDescriptor TargetTypeErrorDescriptor => INotifyPropertyChangedGeneratorError; + /// protected override IEnumerable FilterDeclaredMembers( AttributeData attributeData, diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs index 1b372e1e6b7..c2c822ed98e 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs @@ -4,6 +4,7 @@ using Microsoft.CodeAnalysis; using Microsoft.Toolkit.Mvvm.ComponentModel; +using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -13,5 +14,7 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators [Generator] public class ObservableObjectGenerator : TransitiveMembersGenerator { + /// + protected override DiagnosticDescriptor TargetTypeErrorDescriptor => ObservableObjectGeneratorError; } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs index 34dda1cf74e..4ea147141bc 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Toolkit.Mvvm.ComponentModel; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -18,6 +19,9 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators [Generator] public class ObservableRecipientGenerator : TransitiveMembersGenerator { + /// + protected override DiagnosticDescriptor TargetTypeErrorDescriptor => ObservableRecipientGeneratorError; + /// protected override IEnumerable FilterDeclaredMembers( AttributeData attributeData, diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 4ecce99d27a..d8b8368c98d 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -13,6 +13,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; @@ -25,6 +26,11 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators public abstract class TransitiveMembersGenerator : ISourceGenerator where TAttribute : Attribute { + /// + /// Gets a indicating when the generation failed for a given type. + /// + protected abstract DiagnosticDescriptor TargetTypeErrorDescriptor { get; } + /// public void Initialize(GeneratorInitializationContext context) { @@ -64,7 +70,14 @@ from attribute in syntaxTree.GetRoot().DescendantNodes().OfType INamedTypeSymbol classDeclarationSymbol = semanticModel.GetDeclaredSymbol(classDeclaration)!; AttributeData attributeData = classDeclarationSymbol.GetAttributes().First(a => a.ApplicationSyntaxReference?.GetSyntax() == attribute); - OnExecute(context, attributeData, classDeclaration, classDeclarationSymbol, sourceSyntaxTree); + try + { + OnExecute(context, attributeData, classDeclaration, classDeclarationSymbol, sourceSyntaxTree); + } + catch + { + context.ReportDiagnostic(TargetTypeErrorDescriptor, attribute, classDeclarationSymbol); + } } } From 956a60057b192f55816cb69085c50069b2b7b3b2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 15:30:19 +0100 Subject: [PATCH 22/89] Added support for custom target type validation --- .../Attributes/NotNullWhenAttribute.cs | 25 +++++++++++++++++++ .../INotifyPropertyChangedGenerator.cs | 11 ++++++++ .../ObservableObjectGenerator.cs | 12 +++++++++ .../ObservableRecipientGenerator.cs | 11 ++++++++ .../TransitiveMembersGenerator.cs | 22 ++++++++++++++++ 5 files changed, 81 insertions(+) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Attributes/NotNullWhenAttribute.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Attributes/NotNullWhenAttribute.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Attributes/NotNullWhenAttribute.cs new file mode 100644 index 00000000000..7e63f97aae5 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Attributes/NotNullWhenAttribute.cs @@ -0,0 +1,25 @@ +// 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. + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + internal sealed class NotNullWhenAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// + /// Gets a value indicating whether the annotated parameter will be null depending on the return value. + /// + public bool ReturnValue { get; } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs index 58f8b113fb0..0d257e81551 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -20,6 +21,16 @@ public class INotifyPropertyChangedGenerator : TransitiveMembersGenerator protected override DiagnosticDescriptor TargetTypeErrorDescriptor => INotifyPropertyChangedGeneratorError; + /// + protected override bool ValidateTargetType( + AttributeData attributeData, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) + { + throw new System.NotImplementedException(); + } + /// protected override IEnumerable FilterDeclaredMembers( AttributeData attributeData, diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs index c2c822ed98e..4f5f96582bc 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs @@ -2,7 +2,9 @@ // 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.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Toolkit.Mvvm.ComponentModel; using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; @@ -16,5 +18,15 @@ public class ObservableObjectGenerator : TransitiveMembersGenerator protected override DiagnosticDescriptor TargetTypeErrorDescriptor => ObservableObjectGeneratorError; + + /// + protected override bool ValidateTargetType( + AttributeData attributeData, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) + { + throw new System.NotImplementedException(); + } } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs index 4ea147141bc..6f8c7d93f28 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -22,6 +23,16 @@ public class ObservableRecipientGenerator : TransitiveMembersGenerator protected override DiagnosticDescriptor TargetTypeErrorDescriptor => ObservableRecipientGeneratorError; + /// + protected override bool ValidateTargetType( + AttributeData attributeData, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) + { + throw new System.NotImplementedException(); + } + /// protected override IEnumerable FilterDeclaredMembers( AttributeData attributeData, diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index d8b8368c98d..0c4749bab88 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -70,6 +71,13 @@ from attribute in syntaxTree.GetRoot().DescendantNodes().OfType INamedTypeSymbol classDeclarationSymbol = semanticModel.GetDeclaredSymbol(classDeclaration)!; AttributeData attributeData = classDeclarationSymbol.GetAttributes().First(a => a.ApplicationSyntaxReference?.GetSyntax() == attribute); + if (!ValidateTargetType(attributeData, classDeclaration, classDeclarationSymbol, out var descriptor)) + { + context.ReportDiagnostic(descriptor, attribute, classDeclarationSymbol); + + continue; + } + try { OnExecute(context, attributeData, classDeclaration, classDeclarationSymbol, sourceSyntaxTree); @@ -167,6 +175,20 @@ private void OnExecute( context.AddSource($"[{typeof(TAttribute).Name}]_[{classDeclaration.Identifier.Text}].cs", SourceText.From(source, Encoding.UTF8)); } + /// + /// Validates a target type being processed. + /// + /// The for the current attribute being processed. + /// The node to process. + /// The for . + /// The resulting to emit in case the target type isn't valid. + /// Whether or not the target type is valid and can be processed normally. + protected abstract bool ValidateTargetType( + AttributeData attributeData, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + [NotNullWhen(false)] out DiagnosticDescriptor? descriptor); + /// /// Filters the nodes to generate from the input parsed tree. /// From 58db7bd4af84d507156ce8c497bee223ecb8ed81 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 16:30:01 +0100 Subject: [PATCH 23/89] Added target type diagnostics to attributes --- .../AnalyzerReleases.Unshipped.md | 7 +- .../INotifyPropertyChangedGenerator.cs | 32 ++++--- .../ObservableObjectGenerator.cs | 22 ++++- .../ObservableRecipientGenerator.cs | 46 +++++++--- .../Diagnostics/DiagnosticDescriptors.cs | 88 ++++++++++++++++++- .../Extensions/AttributeDataExtensions.cs | 40 +++++++++ .../Extensions/INamedTypeSymbolExtensions.cs | 41 +++++++++ .../INotifyPropertyChangedAttribute.cs | 2 +- .../Attributes/ObservableObjectAttribute.cs | 2 +- .../ObservableRecipientAttribute.cs | 2 +- 10 files changed, 248 insertions(+), 34 deletions(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md index a5a7067a79d..c7377fbe147 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -7,4 +7,9 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- MVVMTK0001 | Microsoft.Toolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Error | See https://aka.ms/mvvmtoolkit MVVMTK0002 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0003 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit \ No newline at end of file +MVVMTK0003 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0004 | Microsoft.Toolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0005 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0006 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0007 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0008 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs index 0d257e81551..532bf6cb8ec 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs @@ -3,11 +3,13 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators @@ -28,7 +30,17 @@ protected override bool ValidateTargetType( INamedTypeSymbol classDeclarationSymbol, [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) { - throw new System.NotImplementedException(); + // Check if the type already implements INotifyPropertyChanged + if (classDeclarationSymbol.AllInterfaces.Any(static i => i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(INotifyPropertyChanged).FullName}")) + { + descriptor = DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError; + + return false; + } + + descriptor = null; + + return true; } /// @@ -38,19 +50,15 @@ protected override IEnumerable FilterDeclaredMembers( INamedTypeSymbol classDeclarationSymbol, ClassDeclarationSyntax sourceDeclaration) { - foreach (KeyValuePair properties in attributeData.NamedArguments) + // If requested, only include the event and the basic methods to raise it, but not the additional helpers + if (attributeData.HasNamedArgument(nameof(INotifyPropertyChangedAttribute.IncludeAdditionalHelperMethods), false)) { - if (properties.Key == nameof(INotifyPropertyChangedAttribute.IncludeAdditionalHelperMethods) && - properties.Value.Value is bool includeHelpers && !includeHelpers) + return sourceDeclaration.Members.Where(static member => { - // If requested, only include the event and the basic methods to raise it, but not the additional helpers - return sourceDeclaration.Members.Where(static member => - { - return member - is EventFieldDeclarationSyntax - or MethodDeclarationSyntax { Identifier: { ValueText: "OnPropertyChanged" } }; - }); - } + return member + is EventFieldDeclarationSyntax + or MethodDeclarationSyntax { Identifier: { ValueText: "OnPropertyChanged" } }; + }); } return sourceDeclaration.Members; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs index 4f5f96582bc..3ce23d9332c 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs @@ -2,7 +2,9 @@ // 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.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Toolkit.Mvvm.ComponentModel; @@ -26,7 +28,25 @@ protected override bool ValidateTargetType( INamedTypeSymbol classDeclarationSymbol, [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) { - throw new System.NotImplementedException(); + // Check if the type already implements INotifyPropertyChanged... + if (classDeclarationSymbol.AllInterfaces.Any(static i => i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(INotifyPropertyChanged).FullName}")) + { + descriptor = DuplicateINotifyPropertyChangedInterfaceForObservableObjectAttributeError; + + return false; + } + + // ...or INotifyPropertyChanging + if (classDeclarationSymbol.AllInterfaces.Any(static i => i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(INotifyPropertyChanging).FullName}")) + { + descriptor = DuplicateINotifyPropertyChangingInterfaceForObservableObjectAttributeError; + + return false; + } + + descriptor = null; + + return true; } } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs index 6f8c7d93f28..ac9b86cdadd 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs @@ -3,12 +3,14 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; @@ -30,7 +32,30 @@ protected override bool ValidateTargetType( INamedTypeSymbol classDeclarationSymbol, [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) { - throw new System.NotImplementedException(); + // Check if the type already inherits from ObservableRecipient + if (classDeclarationSymbol.InheritsFrom("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableRecipient")) + { + descriptor = DuplicateObservableRecipientError; + + return false; + } + + // In order to use [ObservableRecipient], the target type needs to inherit from ObservableObject, + // or be annotated with [ObservableObject] or [INotifyPropertyChanged] (with additional helpers). + if (!classDeclarationSymbol.InheritsFrom("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject") && + !classDeclarationSymbol.GetAttributes().Any(static a => a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObjectAttribute") && + !classDeclarationSymbol.GetAttributes().Any(static a => + a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute" && + !a.HasNamedArgument(nameof(INotifyPropertyChangedAttribute.IncludeAdditionalHelperMethods), false))) + { + descriptor = MissingBaseObservableObjectFunctionalityError; + + return false; + } + + descriptor = null; + + return true; } /// @@ -66,25 +91,18 @@ classDeclarationSymbol.InstanceConstructors[0] is } } - INamedTypeSymbol? baseTypeSymbol = classDeclarationSymbol.BaseType; - - while (baseTypeSymbol != null) + // Skip the SetProperty overloads if the target type inherits from ObservableValidator, to avoid conflicts + if (classDeclarationSymbol.InheritsFrom("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator")) { - // Skip the SetProperty overloads if the target type inherits from ObservableValidator, to avoid conflicts - if (baseTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator") + foreach (MemberDeclarationSyntax member in sourceDeclaration.Members.Where(static member => member is not ConstructorDeclarationSyntax)) { - foreach (MemberDeclarationSyntax member in sourceDeclaration.Members.Where(static member => member is not ConstructorDeclarationSyntax)) + if (member is not MethodDeclarationSyntax { Identifier: { ValueText: "SetProperty" } }) { - if (member is not MethodDeclarationSyntax { Identifier: { ValueText: "SetProperty" } }) - { - yield return member; - } + yield return member; } - - yield break; } - baseTypeSymbol = baseTypeSymbol.BaseType; + yield break; } // If the target type has at least one custom constructor, only generate methods diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 018043531c7..3381f7eca21 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -2,7 +2,9 @@ // 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.ComponentModel; using Microsoft.CodeAnalysis; +using Microsoft.Toolkit.Mvvm.ComponentModel; namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics { @@ -14,7 +16,7 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when failed to run on a given type. /// - /// Format: "The generator failed to execute on type {0}". + /// Format: "The generator INotifyPropertyChangedGenerator failed to execute on type {0}". /// /// public static readonly DiagnosticDescriptor INotifyPropertyChangedGeneratorError = new( @@ -30,7 +32,7 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when failed to run on a given type. /// - /// Format: "The generator failed to execute on type {0}". + /// Format: "The generator ObservableObjectGenerator failed to execute on type {0}". /// /// public static readonly DiagnosticDescriptor ObservableObjectGeneratorError = new( @@ -46,7 +48,7 @@ internal static class DiagnosticDescriptors /// /// Gets a indicating when failed to run on a given type. /// - /// Format: "The generator failed to execute on type {0}". + /// Format: "The generator ObservableRecipientGenerator failed to execute on type {0}". /// /// public static readonly DiagnosticDescriptor ObservableRecipientGeneratorError = new( @@ -58,5 +60,85 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: $"The {nameof(ObservableRecipientGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.", helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when a duplicate declaration of would happen. + /// + /// Format: "Cannot apply [INotifyPropertyChangedAttribute] to type {0}, as it already declares the INotifyPropertyChanged interface". + /// + /// + public static readonly DiagnosticDescriptor DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError = new( + id: "MVVMTK0004", + title: $"Duplicate {nameof(INotifyPropertyChanged)} definition", + messageFormat: $"Cannot apply [{nameof(INotifyPropertyChangedAttribute)}] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanged)} interface", + category: typeof(INotifyPropertyChangedGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot apply [{nameof(INotifyPropertyChangedAttribute)}] to a type that already declares the {nameof(INotifyPropertyChanged)} interface.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when a duplicate declaration of would happen. + /// + /// Format: "Cannot apply [ObservableObjectAttribute] to type {0}, as it already declares the INotifyPropertyChanged interface". + /// + /// + public static readonly DiagnosticDescriptor DuplicateINotifyPropertyChangedInterfaceForObservableObjectAttributeError = new( + id: "MVVMTK0005", + title: $"Duplicate {nameof(INotifyPropertyChanged)} definition", + messageFormat: $"Cannot apply [{nameof(ObservableObjectAttribute)}] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanged)} interface", + category: typeof(ObservableObjectGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot apply [{nameof(ObservableObjectAttribute)}] to a type that already declares the {nameof(INotifyPropertyChanged)} interface.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when a duplicate declaration of would happen. + /// + /// Format: "Cannot apply [ObservableObjectAttribute] to type {0}, as it already declares the INotifyPropertyChanging interface". + /// + /// + public static readonly DiagnosticDescriptor DuplicateINotifyPropertyChangingInterfaceForObservableObjectAttributeError = new( + id: "MVVMTK0006", + title: $"Duplicate {nameof(INotifyPropertyChanging)} definition", + messageFormat: $"Cannot apply [{nameof(ObservableObjectAttribute)}] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanging)} interface", + category: typeof(ObservableObjectGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot apply [{nameof(ObservableObjectAttribute)}] to a type that already declares the {nameof(INotifyPropertyChanging)} interface.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when a duplicate declaration of would happen. + /// + /// Format: "Cannot apply [ObservableRecipientAttribute] to type {0}, as it already inherits from the ObservableRecipient class". + /// + /// + public static readonly DiagnosticDescriptor DuplicateObservableRecipientError = new( + id: "MVVMTK0007", + title: "Duplicate ObservableRecipient definition", + messageFormat: $"Cannot apply [{nameof(ObservableRecipientAttribute)}] to type {{0}}, as it already inherits from the ObservableRecipient class", + category: typeof(ObservableRecipientGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot apply [{nameof(ObservableRecipientAttribute)}] to a type that already inherits from the ObservableRecipient class.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when there is a missing base functionality to enable . + /// + /// Format: "Cannot apply [ObservableRecipientAttribute] to type {0}, as it lacks necessary base functionality (it should either inherit from ObservableObject, or be annotated with [ObservableObjectAttribute] or [INotifyPropertyChangedAttribute])". + /// + /// + public static readonly DiagnosticDescriptor MissingBaseObservableObjectFunctionalityError = new( + id: "MVVMTK0008", + title: "Missing base ObservableObject functionality", + messageFormat: $"Cannot apply [{nameof(ObservableRecipientAttribute)}] to type {{0}}, as it lacks necessary base functionality (it should either inherit from ObservableObject, or be annotated with [{nameof(ObservableObjectAttribute)}] or [{nameof(INotifyPropertyChangedAttribute)}])", + category: typeof(ObservableRecipientGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot apply [{nameof(ObservableRecipientAttribute)}] to a type that lacks necessary base functionality (it should either inherit from ObservableObject, or be annotated with [{nameof(ObservableObjectAttribute)}] or [{nameof(INotifyPropertyChangedAttribute)}]).", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs new file mode 100644 index 00000000000..0d5c5834096 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -0,0 +1,40 @@ +// 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.Collections.Generic; +using System.Diagnostics.Contracts; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions +{ + /// + /// Extension methods for the type. + /// + internal static class AttributeDataExtensions + { + /// + /// Checks whether a given instance contains a specified named argument. + /// + /// The type of argument to check. + /// The target instance to check. + /// The name of the argument to check. + /// The expected value for the target named argument. + /// Whether or not contains an argument named with the expected value. + [Pure] + public static bool HasNamedArgument(this AttributeData attributeData, string name, T? value) + { + foreach (KeyValuePair properties in attributeData.NamedArguments) + { + if (properties.Key == name) + { + return + properties.Value.Value is T argumentValue && + EqualityComparer.Default.Equals(argumentValue, value); + } + } + + return false; + } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs new file mode 100644 index 00000000000..a31dc1c87c3 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs @@ -0,0 +1,41 @@ +// 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.Diagnostics.Contracts; +using Microsoft.CodeAnalysis; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions +{ + /// + /// Extension methods for the type. + /// + internal static class INamedTypeSymbolExtensions + { + /// + /// Checks whether or not a given inherits from a specified type. + /// + /// The target instance to check. + /// The full name of the type to check for inheritance (without global qualifier). + /// Whether or not inherits from . + [Pure] + public static bool InheritsFrom(this INamedTypeSymbol typeSymbol, string typeName) + { + typeName = "global::" + typeName; + + INamedTypeSymbol? baseType = typeSymbol.BaseType; + + while (baseType != null) + { + if (baseType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == typeName) + { + return true; + } + + baseType = baseType.BaseType; + } + + return false; + } + } +} diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs index a92e4954430..e23410bdcd3 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs @@ -25,7 +25,7 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// /// /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class INotifyPropertyChangedAttribute : Attribute { /// diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs index 6d20df0507b..ef900966900 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs @@ -27,7 +27,7 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// /// And with this, the same APIs from will be available on this type as well. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class ObservableObjectAttribute : Attribute { } diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs index 36cf37d0c3d..7d92aa45e92 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs @@ -41,7 +41,7 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// This is because the methods rely on some of the inherited members to work. /// If this condition is not met, the code will fail to build. /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] public sealed class ObservableRecipientAttribute : Attribute { } From 34651560f2f059e99777ec26fd12e95dd927f309 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 20:38:50 +0100 Subject: [PATCH 24/89] Initial draft for IMessengerRegisterAllGenerator --- .../INotifyPropertyChangedGenerator.cs | 2 +- .../ObservableObjectGenerator.cs | 2 +- .../ObservableRecipientGenerator.cs | 5 +- .../IMessengerRegisterAllGenerator.cs | 154 ++++++++++++++++++ .../Mvvm/Test_IRecipientGenerator.cs | 45 +++++ 5 files changed, 204 insertions(+), 4 deletions(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs create mode 100644 UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs index 532bf6cb8ec..b45cb1c85f5 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs @@ -18,7 +18,7 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators /// A source generator for the type. /// [Generator] - public class INotifyPropertyChangedGenerator : TransitiveMembersGenerator + public sealed class INotifyPropertyChangedGenerator : TransitiveMembersGenerator { /// protected override DiagnosticDescriptor TargetTypeErrorDescriptor => INotifyPropertyChangedGeneratorError; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs index 3ce23d9332c..f7ea5f1ce19 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs @@ -16,7 +16,7 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators /// A source generator for the type. /// [Generator] - public class ObservableObjectGenerator : TransitiveMembersGenerator + public sealed class ObservableObjectGenerator : TransitiveMembersGenerator { /// protected override DiagnosticDescriptor TargetTypeErrorDescriptor => ObservableObjectGeneratorError; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs index ac9b86cdadd..3839d98b65c 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs @@ -20,7 +20,7 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators /// A source generator for the type. /// [Generator] - public class ObservableRecipientGenerator : TransitiveMembersGenerator + public sealed class ObservableRecipientGenerator : TransitiveMembersGenerator { /// protected override DiagnosticDescriptor TargetTypeErrorDescriptor => ObservableRecipientGeneratorError; @@ -43,7 +43,8 @@ protected override bool ValidateTargetType( // In order to use [ObservableRecipient], the target type needs to inherit from ObservableObject, // or be annotated with [ObservableObject] or [INotifyPropertyChanged] (with additional helpers). if (!classDeclarationSymbol.InheritsFrom("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject") && - !classDeclarationSymbol.GetAttributes().Any(static a => a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObjectAttribute") && + !classDeclarationSymbol.GetAttributes().Any(static a => + a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObjectAttribute") && !classDeclarationSymbol.GetAttributes().Any(static a => a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute" && !a.HasNamedArgument(nameof(INotifyPropertyChangedAttribute.IncludeAdditionalHelperMethods), false))) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs new file mode 100644 index 00000000000..1462e4d5ce4 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -0,0 +1,154 @@ +// 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.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + /// A source generator for message registration without relying on compiled LINQ expressions. + /// + [Generator] + public sealed class IMessengerRegisterAllGenerator : ISourceGenerator + { + /// + public void Initialize(GeneratorInitializationContext context) + { + } + + /// + public void Execute(GeneratorExecutionContext context) + { + // Find all the class symbols with at least one IRecipient usage, that are not generic + IEnumerable classSymbols = + from syntaxTree in context.Compilation.SyntaxTrees + let semanticModel = context.Compilation.GetSemanticModel(syntaxTree) + from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType() + let classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol + where + classSymbol is { IsGenericType: false } && + classSymbol?.AllInterfaces.Any(static i => + i.IsGenericType && + i.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.Messaging.IRecipient<>") == true + select classSymbol; + + int i = 0; // TODO + + foreach (INamedTypeSymbol classSymbol in classSymbols) + { + // Create a static method to register all messages for a given recipient type. + // This code takes a class symbol and produces a compilation unit as follows: + // + // // 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.ComponentModel; + // + // namespace Microsoft.Toolkit.Mvvm.Messaging.__Internals + // { + // [EditorBrowsable(EditorBrowsableState.Never)] + // [Obsolete("This type is not intended to be used directly by user code")] + // internal static partial class HashCodeProvider + // { + // [EditorBrowsable(EditorBrowsableState.Never)] + // [Obsolete("This method is not intended to be called directly by user code")] + // public static void RegisterAll(IMessenger messenger, recipient, TToken token) + // where TToken : IEquatable + // { + // + // } + // } + // } + var source = + CompilationUnit().AddUsings( + UsingDirective(IdentifierName("System")), + UsingDirective(IdentifierName("System.ComponentModel"))).AddMembers( + NamespaceDeclaration(IdentifierName("Microsoft.Toolkit.Mvvm.Messaging.__Internals")).AddMembers( + ClassDeclaration("__IMessengerExtensions").AddModifiers( + Token(SyntaxKind.InternalKeyword), + Token(SyntaxKind.StaticKeyword), + Token(SyntaxKind.PartialKeyword)).AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("Obsolete")).AddArgumentListArguments( + AttributeArgument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal("This type is not intended to be used directly by user code"))))))).AddMembers( + MethodDeclaration( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + Identifier("RegisterAll")).AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("Obsolete")).AddArgumentListArguments( + AttributeArgument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal("This method is not intended to be called directly by user code"))))))).AddModifiers( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword)).AddParameterListParameters( + Parameter(Identifier("messenger")).WithType(IdentifierName("IMessenger")), + Parameter(Identifier("recipient")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))), + Parameter(Identifier("token")).WithType(IdentifierName("TToken"))) + .AddTypeParameterListParameters(TypeParameter("TToken")) + .AddConstraintClauses( + TypeParameterConstraintClause("TToken") + .AddConstraints(TypeConstraint(GenericName("IEquatable").AddTypeArgumentListArguments(IdentifierName("TToken"))))) + .WithBody(Block(EnumerateRegistrationStatements(classSymbol).ToArray()))))) + .NormalizeWhitespace() + .ToFullString(); + + // Add the partial type + context.AddSource($"[IRecipient{{T}}]_[{classSymbol.Name}]{i++}.cs", SourceText.From(source, Encoding.UTF8)); + } + } + + /// + /// Gets a sequence of statements to register declared message handlers. + /// + /// The input instance to process. + /// The sequence of instances to register message handleers. + [Pure] + private static IEnumerable EnumerateRegistrationStatements(INamedTypeSymbol classSymbol) + { + foreach (var interfaceSymbol in classSymbol.AllInterfaces) + { + if (!interfaceSymbol.IsGenericType || + interfaceSymbol.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) != "global::Microsoft.Toolkit.Mvvm.Messaging.IRecipient<>") + { + continue; + } + + // This enumerator produces a sequence of statements as follows: + // + // messenger.Register<, TToken>(recipient, token); + // messenger.Register<, TToken>(recipient, token); + // // ... + // messenger.Register<, TToken>(recipient, token); + yield return + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("messenger"), + GenericName(Identifier("Register")).AddTypeArgumentListArguments( + IdentifierName(interfaceSymbol.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + IdentifierName("TToken")))) + .AddArgumentListArguments(Argument(IdentifierName("recipient")), Argument(IdentifierName("token")))); + } + } + } +} diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs new file mode 100644 index 00000000000..2425a042790 --- /dev/null +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs @@ -0,0 +1,45 @@ +// 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 Microsoft.Toolkit.Mvvm.Messaging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.Mvvm +{ + [TestClass] + public partial class Test_IRecipientGenerator + { + [TestCategory("Mvvm")] + [TestMethod] + public void Test_Messenger_UnregisterRecipientWithMessageType(Type type) + { + var messenger = (IMessenger)Activator.CreateInstance(type); + var recipient = new object(); + + messenger.Unregister(recipient); + } + + public sealed class RecipientWithSomeMessages : + IRecipient, + IRecipient + { + public void Receive(MessageA message) + { + } + + public void Receive(MessageB message) + { + } + } + + public sealed class MessageA + { + } + + public sealed class MessageB + { + } + } +} From c86fab9291d832f973536f409fb2db2b58998211 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 21:34:21 +0100 Subject: [PATCH 25/89] Fixed duplicate attributes in generated type --- .../IMessengerRegisterAllGenerator.cs | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index 1462e4d5ce4..4a7597b1368 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -2,6 +2,7 @@ // 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.Diagnostics.Contracts; using System.Linq; @@ -41,7 +42,21 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType") == true select classSymbol; - int i = 0; // TODO + // Prepare the attributes to add to the first class declaration + AttributeListSyntax[] classAttributes = new[] + { + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("Obsolete")).AddArgumentListArguments( + AttributeArgument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal("This type is not intended to be used directly by user code")))))) + }; + + // Local counter to avoid filename conflicts in case of different types with the same name + int i = 0; foreach (INamedTypeSymbol classSymbol in classSymbols) { @@ -78,15 +93,7 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType(); + // Add the partial type - context.AddSource($"[IRecipient{{T}}]_[{classSymbol.Name}]{i++}.cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"[IRecipient{{T}}]_[{classSymbol.Name}]_[{i++}].cs", SourceText.From(source, Encoding.UTF8)); } } From 24625e2cacd29ed197ef500350241f40832aa45c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 22:02:33 +0100 Subject: [PATCH 26/89] Optimized the LINQ expression for message registration --- .../ComponentModel/ObservableValidator.cs | 1 + .../Messaging/IMessengerExtensions.cs | 68 ++++++++++--------- 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs index 58bf60577d1..67e2e97a6a9 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs @@ -489,6 +489,7 @@ static Action GetValidationAction(Type type) // inst0.ValidateProperty(inst0.Property0, nameof(MyViewModel.Property0)); // inst0.ValidateProperty(inst0.Property1, nameof(MyViewModel.Property1)); // ... + // inst0.ValidateProperty(inst0.PropertyN, nameof(MyViewModel.PropertyN)); // } // =============================================================================== // We also add an explicit object conversion to represent boxing, if a given property diff --git a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs index 43b9dc42add..a31ece3a7bf 100644 --- a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs +++ b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs @@ -45,9 +45,9 @@ private static class DiscoveredRecipients where TToken : IEquatable { /// - /// The instance used to track the preloaded registration actions for each recipient. + /// The instance used to track the preloaded registration action for each recipient. /// - public static readonly ConditionalWeakTable[]> RegistrationMethods = new(); + public static readonly ConditionalWeakTable> RegistrationMethods = new(); } /// @@ -97,21 +97,7 @@ public static void RegisterAll(this IMessenger messenger, object recipie // handle thread-safety for us, as well as avoiding all the LINQ codegen bloat here. // 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. - static Action[] LoadRegistrationMethodsForType(Type type) - { - return ( - from interfaceType in type.GetInterfaces() - where interfaceType.IsGenericType && - interfaceType.GetGenericTypeDefinition() == typeof(IRecipient<>) - let messageType = interfaceType.GenericTypeArguments[0] - let registrationMethod = MethodInfos.RegisterIRecipient.MakeGenericMethod(messageType, typeof(TToken)) - let registrationAction = GetRegistrationAction(type, registrationMethod) - select registrationAction).ToArray(); - } - - // Helper method to build and compile an expression tree to a message handler to use for the registration - // This is used to reduce the overhead of repeated calls to MethodInfo.Invoke (which is over 10 times slower). - static Action GetRegistrationAction(Type type, MethodInfo methodInfo) + static Action LoadRegistrationMethodsForType(Type recipientType) { // Input parameters (IMessenger instance, non-generic recipient, token) ParameterExpression @@ -119,31 +105,51 @@ static Action GetRegistrationAction(Type type, Metho arg1 = Expression.Parameter(typeof(object)), arg2 = Expression.Parameter(typeof(TToken)); - // Cast the recipient and invoke the registration method - MethodCallExpression body = Expression.Call(null, methodInfo, new Expression[] - { - arg0, - Expression.Convert(arg1, type), - arg2 - }); + // 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 indivudual message registration (for each IRecipient). + // The code below will generate the following block expression: + // =============================================================================== + // { + // var inst1 = (RecipientType)arg1; + // IMessengerExtensions.Register(arg0, inst1, arg2); + // IMessengerExtensions.Register(arg0, inst1, arg2); + // ... + // IMessengerExtensions.Register(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 interfaceType in recipientType.GetInterfaces() + where interfaceType.IsGenericType && + interfaceType.GetGenericTypeDefinition() == typeof(IRecipient<>) + let messageType = interfaceType.GenericTypeArguments[0] + let registrationMethod = MethodInfos.RegisterIRecipient.MakeGenericMethod(messageType, typeof(TToken)) + select Expression.Call(registrationMethod, new Expression[] + { + arg0, + inst1, + arg2 + })); - // Create the expression tree and compile to a target delegate return Expression.Lambda>(body, arg0, arg1, arg2).Compile(); } - // Get or compute the registration methods for the current recipient type. + // Get or compute the registration method for the current recipient type. // As in Microsoft.Toolkit.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[] registrationActions = DiscoveredRecipients.RegistrationMethods.GetValue( + Action registrationAction = DiscoveredRecipients.RegistrationMethods.GetValue( recipient.GetType(), static t => LoadRegistrationMethodsForType(t)); - foreach (Action registrationAction in registrationActions) - { - registrationAction(messenger, recipient, token); - } + // Invoke the cached delegate to actually execute the message registration + registrationAction(messenger, recipient, token); } /// From 777e0200f3f65fd4678dbd35a2eb1b59b751fe0b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 22:18:07 +0100 Subject: [PATCH 27/89] Enabled loading of generated message registration methods --- .../Messaging/IMessengerExtensions.cs | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs index a31ece3a7bf..1ad9af1ccf4 100644 --- a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs +++ b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs @@ -93,11 +93,39 @@ public static void RegisterAll(this IMessenger messenger, object recipient) public static void RegisterAll(this IMessenger messenger, object recipient, TToken token) where TToken : IEquatable { - // We use this method as a callback for the conditional weak table, which will both - // handle thread-safety for us, as well as avoiding all the LINQ codegen bloat here. + // 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 create a delegate wrapping that method if it is found. + static Action LoadRegistrationMethodsForType(Type recipientType) + { + if (recipientType.Assembly.GetType("Microsoft.Toolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType && + extensionsType.GetMethods(BindingFlags.Static | BindingFlags.Public) is MethodInfo[] { Length: > 0 } extensionMethods) + { + foreach (MethodInfo methodInfo in extensionMethods) + { + if (methodInfo.Name is "RegisterAll" && + methodInfo.GetParameters()[1].ParameterType == recipientType) + { + MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(TToken)); + Type delegateType = typeof(Action<,,>).MakeGenericType(typeof(IMessenger), recipientType, typeof(TToken)); + + // We need an unsafe cast here like we did in StrongReferenceMessenger to be able to treat the new delegate + // type as if it was covariant in its input recipient. This allows us to keep the type-specific overloads in + // the generated code while still creating non-generic delegates here. This code is technically safe since + // we have control over what types we're working with, and we know the type conversions will always be valid. + return Unsafe.As>(genericMethodInfo.CreateDelegate(delegateType)); + } + } + } + + 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. - static Action LoadRegistrationMethodsForType(Type recipientType) + // The LINQ codegen bloat is not really important for the same reason. + static Action LoadRegistrationMethodsForTypeFallback(Type recipientType) { // Input parameters (IMessenger instance, non-generic recipient, token) ParameterExpression From 7ac91feaaf22c364c56f7c73013c972cdc3e2e55 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 22:30:22 +0100 Subject: [PATCH 28/89] Fixed unit test for generated RegisterAll method --- .../Mvvm/Test_IRecipientGenerator.cs | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs index 2425a042790..dd196545f47 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs @@ -2,7 +2,8 @@ // 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; +#pragma warning disable CS0618 + using Microsoft.Toolkit.Mvvm.Messaging; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -13,24 +14,49 @@ public partial class Test_IRecipientGenerator { [TestCategory("Mvvm")] [TestMethod] - public void Test_Messenger_UnregisterRecipientWithMessageType(Type type) + public void Test_IRecipientGenerator_GeneratedRegistration() { - var messenger = (IMessenger)Activator.CreateInstance(type); - var recipient = new object(); + var messenger = new StrongReferenceMessenger(); + var recipient = new RecipientWithSomeMessages(); + + var messageA = new MessageA(); + var messageB = new MessageB(); + + Microsoft.Toolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions.RegisterAll(messenger, recipient, 42); + + Assert.IsTrue(messenger.IsRegistered(recipient, 42)); + Assert.IsTrue(messenger.IsRegistered(recipient, 42)); + + Assert.IsNull(recipient.A); + Assert.IsNull(recipient.B); - messenger.Unregister(recipient); + messenger.Send(messageA, 42); + + Assert.AreSame(recipient.A, messageA); + Assert.IsNull(recipient.B); + + messenger.Send(messageB, 42); + + Assert.AreSame(recipient.A, messageA); + Assert.AreSame(recipient.B, messageB); } public sealed class RecipientWithSomeMessages : IRecipient, IRecipient { + public MessageA A { get; private set; } + + public MessageB B { get; private set; } + public void Receive(MessageA message) { + A = message; } public void Receive(MessageB message) { + B = message; } } From bb1c29516e55a713c897e6a8fd379b1477101498 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 22:42:26 +0100 Subject: [PATCH 29/89] Fixed header generation in generated files --- .../ComponentModel/TransitiveMembersGenerator.cs | 12 ++---------- .../Messaging/IMessengerRegisterAllGenerator.cs | 8 +++++++- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 0c4749bab88..9aa4a3dea3d 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -154,21 +154,13 @@ private void OnExecute( CompilationUnit() .AddMembers(NamespaceDeclaration(IdentifierName(namespaceName)) .AddMembers(typeDeclarationSyntax)) - .NormalizeWhitespace() .AddUsings(usingDirectives.First().WithLeadingTrivia(TriviaList( Comment("// Licensed to the .NET Foundation under one or more agreements."), - CarriageReturnLineFeed, Comment("// The .NET Foundation licenses this file to you under the MIT license."), - CarriageReturnLineFeed, Comment("// See the LICENSE file in the project root for more information."), - CarriageReturnLineFeed, - CarriageReturnLineFeed, - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true) - .WithPragmaKeyword(Token(TriviaList(), SyntaxKind.PragmaKeyword, TriviaList(Space))) - .WithWarningKeyword(Token(TriviaList(), SyntaxKind.WarningKeyword, TriviaList(Space))) - .WithEndOfDirectiveToken(Token(TriviaList(), SyntaxKind.EndOfDirectiveToken, TriviaList(CarriageReturnLineFeed)))), - CarriageReturnLineFeed))) + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))) .AddUsings(usingDirectives.Skip(1).ToArray()) + .NormalizeWhitespace() .ToFullString(); // Add the partial type diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index 4a7597b1368..032fc4b8690 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -67,6 +67,8 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType Date: Mon, 22 Mar 2021 23:31:28 +0100 Subject: [PATCH 30/89] Added optimized method lookup on NS2.1 and up --- .../Messaging/IMessengerExtensions.cs | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs index 1ad9af1ccf4..7eb9ff57dee 100644 --- a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs +++ b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs @@ -98,29 +98,45 @@ public static void RegisterAll(this IMessenger messenger, object recipie // target recipient type, and just create a delegate wrapping that method if it is found. static Action LoadRegistrationMethodsForType(Type recipientType) { - if (recipientType.Assembly.GetType("Microsoft.Toolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType && - extensionsType.GetMethods(BindingFlags.Static | BindingFlags.Public) is MethodInfo[] { Length: > 0 } extensionMethods) + if (recipientType.Assembly.GetType("Microsoft.Toolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType) { - foreach (MethodInfo methodInfo in extensionMethods) +#if NETSTANDARD2_0 + // .NET Standard 2.0 doesn't have Type.MakeGenericMethodParameter, so we need to iterate manually + foreach (MethodInfo methodInfo in extensionsType.GetMethods(BindingFlags.Static | BindingFlags.Public)) { if (methodInfo.Name is "RegisterAll" && methodInfo.GetParameters()[1].ParameterType == recipientType) { - MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(TToken)); - Type delegateType = typeof(Action<,,>).MakeGenericType(typeof(IMessenger), recipientType, typeof(TToken)); - - // We need an unsafe cast here like we did in StrongReferenceMessenger to be able to treat the new delegate - // type as if it was covariant in its input recipient. This allows us to keep the type-specific overloads in - // the generated code while still creating non-generic delegates here. This code is technically safe since - // we have control over what types we're working with, and we know the type conversions will always be valid. - return Unsafe.As>(genericMethodInfo.CreateDelegate(delegateType)); + return CreateGenericDelegate(recipientType, methodInfo); } } +#else + // On .NET Standard 2.1 and up, we can directly look for the target method in one call + Type[] methodTypes = new[] { typeof(IMessenger), recipientType, Type.MakeGenericMethodParameter(0) }; + + if (extensionsType.GetMethod("RegisterAll", methodTypes) is MethodInfo methodInfo) + { + return CreateGenericDelegate(recipientType, methodInfo); + } +#endif } return LoadRegistrationMethodsForTypeFallback(recipientType); } + // A shared method to create a generic delegate from an identified method + static Action CreateGenericDelegate(Type recipientType, MethodInfo methodInfo) + { + MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(TToken)); + Type delegateType = typeof(Action<,,>).MakeGenericType(typeof(IMessenger), recipientType, typeof(TToken)); + + // We need an unsafe cast here like we did in StrongReferenceMessenger to be able to treat the new delegate + // type as if it was covariant in its input recipient. This allows us to keep the type-specific overloads in + // the generated code while still creating non-generic delegates here. This code is technically safe since + // we have control over what types we're working with, and we know the type conversions will always be valid. + return Unsafe.As>(genericMethodInfo.CreateDelegate(delegateType)); + } + // 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. From 7bffd22913056b2aba2cf10b8c749671f021bdee Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 22 Mar 2021 23:40:52 +0100 Subject: [PATCH 31/89] Added generation of non-generic RegisterAll methods --- .../IMessengerRegisterAllGenerator.cs | 64 ++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index 032fc4b8690..f42128f537d 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -61,6 +61,9 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType recipient) + // { + // + // } + // + // [EditorBrowsable(EditorBrowsableState.Never)] + // [Obsolete("This method is not intended to be called directly by user code")] // public static void RegisterAll(IMessenger messenger, recipient, TToken token) // where TToken : IEquatable // { @@ -100,6 +110,22 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType EnumerateRegistrationStatements(INam continue; } + // This enumerator produces a sequence of statements as follows: + // + // messenger.Register<, TToken>(recipient); + // messenger.Register<, TToken>(recipient); + // // ... + // messenger.Register<, TToken>(recipient); + yield return + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("messenger"), + GenericName(Identifier("Register")).AddTypeArgumentListArguments( + IdentifierName(interfaceSymbol.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))))) + .AddArgumentListArguments(Argument(IdentifierName("recipient")))); + } + } + + /// + /// Gets a sequence of statements to register declared message handlers with custom tokens. + /// + /// The input instance to process. + /// The sequence of instances to register message handleers. + [Pure] + private static IEnumerable EnumerateRegistrationStatementsWithTokens(INamedTypeSymbol classSymbol) + { + foreach (var interfaceSymbol in classSymbol.AllInterfaces) + { + if (!interfaceSymbol.IsGenericType || + interfaceSymbol.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) != "global::Microsoft.Toolkit.Mvvm.Messaging.IRecipient<>") + { + continue; + } + // This enumerator produces a sequence of statements as follows: // // messenger.Register<, TToken>(recipient, token); From 3a0966c4e4e58cf5b6a0bdc5b70bcf761ac9b462 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 23 Mar 2021 00:00:22 +0100 Subject: [PATCH 32/89] Enabled AOT-friendly path for message registration --- .../Messaging/IMessengerExtensions.cs | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs index 7eb9ff57dee..9e1a2e9f646 100644 --- a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs +++ b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs @@ -33,6 +33,17 @@ private static class MethodInfos public static readonly MethodInfo RegisterIRecipient = new Action, Unit>(Register).Method.GetGenericMethodDefinition(); } + /// + /// A non-generic version of . + /// + private static class DiscoveredRecipients + { + /// + /// The instance used to track the preloaded registration action for each recipient. + /// + public static readonly ConditionalWeakTable?> RegistrationMethods = new(); + } + /// /// A class that acts as a static container to associate a instance to each /// type in use. This is done because we can only use a single type as key, but we need to track @@ -73,7 +84,36 @@ public static bool IsRegistered(this IMessenger messenger, object reci /// See notes for for more info. public static void RegisterAll(this IMessenger messenger, object recipient) { - messenger.RegisterAll(recipient, default(Unit)); + // 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 create a delegate wrapping that method if it is found. + static Action? LoadRegistrationMethodsForType(Type recipientType) + { + if (recipientType.Assembly.GetType("Microsoft.Toolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType && + extensionsType.GetMethod("RegisterAll", new[] { typeof(IMessenger), recipientType }) is MethodInfo methodInfo) + { + Type delegateType = typeof(Action<,>).MakeGenericType(typeof(IMessenger), recipientType); + + // Create the delegate and use an unsafe cast to achieve input covariance (as detailed below) + return Unsafe.As>(methodInfo.CreateDelegate(delegateType)); + } + + return null; + } + + // Try to get the cached delegate, if the generatos has run correctly + Action? registrationAction = DiscoveredRecipients.RegistrationMethods.GetValue( + recipient.GetType(), + static t => LoadRegistrationMethodsForType(t)); + + if (registrationAction is not null) + { + registrationAction(messenger, recipient); + } + else + { + messenger.RegisterAll(recipient, default(Unit)); + } } /// From d28c75e27211828ac54480d5b89ef2c34b411b50 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 23 Mar 2021 00:50:11 +0100 Subject: [PATCH 33/89] Fixed a bug when running on .NET Standard 2.0 --- Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs index 9e1a2e9f646..999f6544deb 100644 --- a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs +++ b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs @@ -145,6 +145,7 @@ static Action LoadRegistrationMethodsForType(Type re foreach (MethodInfo methodInfo in extensionsType.GetMethods(BindingFlags.Static | BindingFlags.Public)) { if (methodInfo.Name is "RegisterAll" && + methodInfo.IsGenericMethod && methodInfo.GetParameters()[1].ParameterType == recipientType) { return CreateGenericDelegate(recipientType, methodInfo); From c514929fc1a971b61605f0c40cec8d0f876d63f5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 23 Mar 2021 16:25:11 +0100 Subject: [PATCH 34/89] Improved filenames for generated files --- .../TransitiveMembersGenerator.cs | 3 +- .../Extensions/INamedTypeSymbolExtensions.cs | 37 +++++++++++++++++++ .../IMessengerRegisterAllGenerator.cs | 12 +++--- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 9aa4a3dea3d..ea67eeafacf 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -15,6 +15,7 @@ using Microsoft.CodeAnalysis.Text; using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; @@ -164,7 +165,7 @@ private void OnExecute( .ToFullString(); // Add the partial type - context.AddSource($"[{typeof(TAttribute).Name}]_[{classDeclaration.Identifier.Text}].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"[{typeof(TAttribute).Name}]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); } /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs index a31dc1c87c3..fe1586961dd 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Diagnostics.Contracts; +using System.Text; using Microsoft.CodeAnalysis; namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions @@ -12,6 +13,42 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions /// internal static class INamedTypeSymbolExtensions { + /// + /// Gets the full metadata name for a given instance. + /// + /// The input instance. + /// The full metadata name for . + [Pure] + public static string GetFullMetadataName(this INamedTypeSymbol symbol) + { + static StringBuilder BuildFrom(ISymbol? symbol, StringBuilder builder) + { + return symbol switch + { + INamespaceSymbol ns when ns.IsGlobalNamespace => builder, + INamespaceSymbol ns when ns.ContainingNamespace is { IsGlobalNamespace: false } + => BuildFrom(ns.ContainingNamespace, builder.Insert(0, $".{ns.MetadataName}")), + ITypeSymbol ts when ts.ContainingType is ISymbol pt => BuildFrom(pt, builder.Insert(0, $"+{ts.MetadataName}")), + ITypeSymbol ts when ts.ContainingNamespace is ISymbol pn => BuildFrom(pn, builder.Insert(0, $".{ts.MetadataName}")), + ISymbol => BuildFrom(symbol.ContainingSymbol, builder.Insert(0, symbol.MetadataName)), + _ => builder + }; + } + + return BuildFrom(symbol, new StringBuilder(256)).ToString(); + } + + /// + /// Gets a valid filename for a given instance. + /// + /// The input instance. + /// The full metadata name for that is also a valid filename. + [Pure] + public static string GetFullMetadataNameForFileName(this INamedTypeSymbol symbol) + { + return symbol.GetFullMetadataName().Replace('`', '-').Replace('+', '.'); + } + /// /// Checks whether or not a given inherits from a specified type. /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index f42128f537d..e2307998034 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -11,6 +11,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Microsoft.Toolkit.Mvvm.SourceGenerators @@ -55,9 +56,6 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType(); // Add the partial type - context.AddSource($"[IRecipient{{T}}]_[{classSymbol.Name}]_[{i++}].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"[IRecipient{{T}}]_[{classSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); } } @@ -176,10 +174,10 @@ private static IEnumerable EnumerateRegistrationStatements(INam // This enumerator produces a sequence of statements as follows: // - // messenger.Register<, TToken>(recipient); - // messenger.Register<, TToken>(recipient); + // messenger.Register<>(recipient); + // messenger.Register<>(recipient); // // ... - // messenger.Register<, TToken>(recipient); + // messenger.Register<>(recipient); yield return ExpressionStatement( InvocationExpression( From c7210ae2b28b5ed1b1ae9d9a4c36eb4ba8957043 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 23 Mar 2021 20:15:22 +0100 Subject: [PATCH 35/89] Enabled source generator packing into MVVM Toolkit See https://github.com/windows-toolkit/WindowsCommunityToolkit/issues/3872#issuecomment-805023949 --- ...Microsoft.Toolkit.Mvvm.SourceGenerators.csproj | 1 + .../Microsoft.Toolkit.Mvvm.csproj | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index 7b440b46a49..a2284f23244 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -4,6 +4,7 @@ netstandard2.0 9.0 enable + false diff --git a/Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj b/Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj index 93ac0794922..77cc60298fb 100644 --- a/Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj +++ b/Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj @@ -20,6 +20,7 @@ - Ioc: a helper class to configure dependency injection service containers. MVVM;Toolkit;MVVMToolkit;INotifyPropertyChanged;Observable;IOC;DI;Dependency Injection;Object Messaging;Extensions;Helpers + $(TargetsForTfmSpecificContentInPackage);CopyAnalyzerProjectReferencesToPackage @@ -34,5 +35,19 @@ + + + + + + + + + + + analyzers\dotnet\cs + + + \ No newline at end of file From d2624c8dc187e3b1d5992bc9922eea743dbef1ad Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 23 Mar 2021 22:06:02 +0100 Subject: [PATCH 36/89] Fixed analyzer target condition --- Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj b/Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj index 77cc60298fb..efa6bad4691 100644 --- a/Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj +++ b/Microsoft.Toolkit.Mvvm/Microsoft.Toolkit.Mvvm.csproj @@ -41,8 +41,11 @@ - - + + analyzers\dotnet\cs From 4c035f8225bfb1c045e047084b5d1b8b0874b539 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 24 Mar 2021 22:32:58 +0100 Subject: [PATCH 37/89] Added ObservableValidatorValidateAllPropertiesGenerator --- ...ValidatorValidateAllPropertiesGenerator.cs | 177 ++++++++++++++++++ .../IMessengerRegisterAllGenerator.cs | 6 +- .../ComponentModel/ObservableValidator.cs | 2 +- .../__ObservableValidatorHelper.cs | 33 ++++ 4 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs create mode 100644 Microsoft.Toolkit.Mvvm/ComponentModel/__Internals/__ObservableValidatorHelper.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs new file mode 100644 index 00000000000..127c53052d2 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -0,0 +1,177 @@ +// 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.Collections.Immutable; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + /// A source generator for properties validation without relying on compiled LINQ expressions. + /// + [Generator] + public sealed class ObservableValidatorValidateAllPropertiesGenerator : ISourceGenerator + { + /// + public void Initialize(GeneratorInitializationContext context) + { + } + + /// + public void Execute(GeneratorExecutionContext context) + { + // Find all the class symbols inheriting from ObservableValidator, that are not generic + IEnumerable classSymbols = + from syntaxTree in context.Compilation.SyntaxTrees + let semanticModel = context.Compilation.GetSemanticModel(syntaxTree) + from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType() + let classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration) + where + classSymbol is { IsGenericType: false } && + classSymbol.InheritsFrom("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator") + select classSymbol; + + // Prepare the attributes to add to the first class declaration + AttributeListSyntax[] classAttributes = new[] + { + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("Obsolete")).AddArgumentListArguments( + AttributeArgument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal("This type is not intended to be used directly by user code")))))) + }; + + foreach (INamedTypeSymbol classSymbol in classSymbols) + { + // Create a static method to validate all properties in a given class. + // This code takes a class symbol and produces a compilation unit as follows: + // + // // 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. + // + // #pragma warning disable + // + // using System; + // using System.ComponentModel; + // + // namespace Microsoft.Toolkit.Mvvm.ComponentModel.__Internals + // { + // [EditorBrowsable(EditorBrowsableState.Never)] + // [Obsolete("This type is not intended to be used directly by user code")] + // internal static partial class __ObservableValidatorExtensions + // { + // [EditorBrowsable(EditorBrowsableState.Never)] + // [Obsolete("This method is not intended to be called directly by user code")] + // public static void ValidateAllProperties( instance) + // { + // + // } + // } + // } + var source = + CompilationUnit().AddUsings( + UsingDirective(IdentifierName("System")).WithLeadingTrivia(TriviaList( + Comment("// Licensed to the .NET Foundation under one or more agreements."), + Comment("// The .NET Foundation licenses this file to you under the MIT license."), + Comment("// See the LICENSE file in the project root for more information."), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))), + UsingDirective(IdentifierName("System.ComponentModel"))).AddMembers( + NamespaceDeclaration(IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals")).AddMembers( + ClassDeclaration("__ObservableValidatorExtensions").AddModifiers( + Token(SyntaxKind.InternalKeyword), + Token(SyntaxKind.StaticKeyword), + Token(SyntaxKind.PartialKeyword)).AddAttributeLists(classAttributes).AddMembers( + MethodDeclaration( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + Identifier("ValidateAllProperties")).AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("Obsolete")).AddArgumentListArguments( + AttributeArgument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal("This method is not intended to be called directly by user code"))))))).AddModifiers( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword)).AddParameterListParameters( + Parameter(Identifier("instance")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) + .WithBody(Block(EnumerateValidationStatements(classSymbol).ToArray()))))) + .NormalizeWhitespace() + .ToFullString(); + + // Reset the attributes list (so the same class doesn't get duplicate attributes) + classAttributes = Array.Empty(); + + // Add the partial type + context.AddSource($"[ObservableValidator]_[{classSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + } + } + + /// + /// Gets a sequence of statements to validate declared properties. + /// + /// The input instance to process. + /// The sequence of instances to validate declared properties. + [Pure] + private static IEnumerable EnumerateValidationStatements(INamedTypeSymbol classSymbol) + { + foreach (var propertySymbol in classSymbol.GetMembers().OfType()) + { + if (propertySymbol.IsIndexer) + { + continue; + } + + ImmutableArray attributes = propertySymbol.GetAttributes(); + + if (!attributes.Any(static a => a.AttributeClass?.InheritsFrom("System.ComponentModel.DataAnnotations.ValidationAttribute") == true)) + { + continue; + } + + // This enumerator produces a sequence of statements as follows: + // + // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.)); + // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.)); + // // ... + // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.)); + yield return + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("__ObservableValidatorHelper"), + IdentifierName("ValidateProperty"))) + .AddArgumentListArguments( + Argument(IdentifierName("instance")), + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("instance"), + IdentifierName(propertySymbol.Name))), + Argument( + InvocationExpression(IdentifierName("nameof")) + .AddArgumentListArguments(Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("instance"), + IdentifierName(propertySymbol.Name))))))); + } + } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index e2307998034..525a53c23db 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -35,12 +35,12 @@ public void Execute(GeneratorExecutionContext context) from syntaxTree in context.Compilation.SyntaxTrees let semanticModel = context.Compilation.GetSemanticModel(syntaxTree) from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType() - let classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration) as INamedTypeSymbol + let classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration) where classSymbol is { IsGenericType: false } && - classSymbol?.AllInterfaces.Any(static i => + classSymbol.AllInterfaces.Any(static i => i.IsGenericType && - i.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.Messaging.IRecipient<>") == true + i.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.Messaging.IRecipient<>") select classSymbol; // Prepare the attributes to add to the first class declaration diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs index 67e2e97a6a9..8dfc3fe89d1 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs @@ -524,7 +524,7 @@ where getter is not null /// The value to test for the specified property. /// The name of the property to validate. /// Thrown when is . - protected void ValidateProperty(object? value, [CallerMemberName] string? propertyName = null) + protected internal void ValidateProperty(object? value, [CallerMemberName] string? propertyName = null) { if (propertyName is null) { diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/__Internals/__ObservableValidatorHelper.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/__Internals/__ObservableValidatorHelper.cs new file mode 100644 index 00000000000..bd9ee7ede80 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/__Internals/__ObservableValidatorHelper.cs @@ -0,0 +1,33 @@ +// 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. + +#pragma warning disable SA1300 + +using System; +using System.ComponentModel; + +namespace Microsoft.Toolkit.Mvvm.ComponentModel.__Internals +{ + /// + /// An internal helper to support the source generator APIs related to . + /// This type is not intended to be used directly by user code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This type is not intended to be used directly by user code")] + public static class __ObservableValidatorHelper + { + /// + /// Invokes externally on a target instance. + /// + /// The target instance. + /// The value to test for the specified property. + /// The name of the property to validate. + [EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This method is not intended to be called directly by user code")] + public static void ValidateProperty(ObservableValidator instance, object? value, string propertyName) + { + instance.ValidateProperty(value, propertyName); + } + } +} \ No newline at end of file From 7c88c3dbeaa89349f3ce0f5408b145dd7ed79daa Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 24 Mar 2021 22:43:18 +0100 Subject: [PATCH 38/89] Enabled generated code for ValidateAllProperties --- .../ComponentModel/ObservableValidator.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs index 8dfc3fe89d1..dead1935df0 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs @@ -471,7 +471,23 @@ IEnumerable GetAllErrors() /// protected void ValidateAllProperties() { + // Fast path that tries to create a delegate from a generated type-specific method. This + // is used to make this method more AOT-friendly and faster, as there is no dynamic code. static Action GetValidationAction(Type type) + { + if (type.Assembly.GetType("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions") is Type extensionsType && + extensionsType.GetMethod("ValidateAllProperties", new[] { type }) is MethodInfo methodInfo) + { + Type delegateType = typeof(Action<>).MakeGenericType(type); + + return Unsafe.As>(methodInfo.CreateDelegate(delegateType)); + } + + return GetValidationActionFallback(type); + } + + // Fallback method to create the delegate with a compiled LINQ expression + static Action GetValidationActionFallback(Type type) { // MyViewModel inst0 = (MyViewModel)arg0; ParameterExpression arg0 = Expression.Parameter(typeof(object)); From e5af579e5f444732ec26b188e8b91a90412e2893 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 26 Mar 2021 01:23:57 +0100 Subject: [PATCH 39/89] Switched symbol comparisons to GetTypeByMetadataName() --- .../INotifyPropertyChangedGenerator.cs | 6 ++++- .../ObservableObjectGenerator.cs | 9 ++++++-- .../ObservableRecipientGenerator.cs | 23 +++++++++++++------ ...ValidatorValidateAllPropertiesGenerator.cs | 14 +++++++---- .../TransitiveMembersGenerator.cs | 13 ++++++++--- .../Extensions/INamedTypeSymbolExtensions.cs | 10 ++++---- .../IMessengerRegisterAllGenerator.cs | 22 +++++++++++------- 7 files changed, 66 insertions(+), 31 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs index b45cb1c85f5..e3c356d62ae 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs @@ -25,13 +25,16 @@ public sealed class INotifyPropertyChangedGenerator : TransitiveMembersGenerator /// protected override bool ValidateTargetType( + GeneratorExecutionContext context, AttributeData attributeData, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) { + INamedTypeSymbol iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanged).FullName)!; + // Check if the type already implements INotifyPropertyChanged - if (classDeclarationSymbol.AllInterfaces.Any(static i => i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(INotifyPropertyChanged).FullName}")) + if (classDeclarationSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iNotifyPropertyChangedSymbol))) { descriptor = DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError; @@ -45,6 +48,7 @@ protected override bool ValidateTargetType( /// protected override IEnumerable FilterDeclaredMembers( + GeneratorExecutionContext context, AttributeData attributeData, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs index f7ea5f1ce19..081c95fcf66 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs @@ -23,13 +23,18 @@ public sealed class ObservableObjectGenerator : TransitiveMembersGenerator protected override bool ValidateTargetType( + GeneratorExecutionContext context, AttributeData attributeData, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) { + INamedTypeSymbol + iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanged).FullName)!, + iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanging).FullName)!; + // Check if the type already implements INotifyPropertyChanged... - if (classDeclarationSymbol.AllInterfaces.Any(static i => i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(INotifyPropertyChanged).FullName}")) + if (classDeclarationSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iNotifyPropertyChangedSymbol))) { descriptor = DuplicateINotifyPropertyChangedInterfaceForObservableObjectAttributeError; @@ -37,7 +42,7 @@ protected override bool ValidateTargetType( } // ...or INotifyPropertyChanging - if (classDeclarationSymbol.AllInterfaces.Any(static i => i.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == $"global::{typeof(INotifyPropertyChanging).FullName}")) + if (classDeclarationSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iNotifyPropertyChangingSymbol))) { descriptor = DuplicateINotifyPropertyChangingInterfaceForObservableObjectAttributeError; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs index 3839d98b65c..323c08c991c 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs @@ -27,13 +27,20 @@ public sealed class ObservableRecipientGenerator : TransitiveMembersGenerator protected override bool ValidateTargetType( + GeneratorExecutionContext context, AttributeData attributeData, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) { + INamedTypeSymbol + observableRecipientSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableRecipient")!, + observableObjectSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject")!, + observableObjectAttributeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObjectAttribute")!, + iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanged).FullName)!; + // Check if the type already inherits from ObservableRecipient - if (classDeclarationSymbol.InheritsFrom("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableRecipient")) + if (classDeclarationSymbol.InheritsFrom(observableRecipientSymbol)) { descriptor = DuplicateObservableRecipientError; @@ -42,11 +49,10 @@ protected override bool ValidateTargetType( // In order to use [ObservableRecipient], the target type needs to inherit from ObservableObject, // or be annotated with [ObservableObject] or [INotifyPropertyChanged] (with additional helpers). - if (!classDeclarationSymbol.InheritsFrom("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject") && - !classDeclarationSymbol.GetAttributes().Any(static a => - a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObjectAttribute") && - !classDeclarationSymbol.GetAttributes().Any(static a => - a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute" && + if (!classDeclarationSymbol.InheritsFrom(observableObjectSymbol) && + !classDeclarationSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observableObjectAttributeSymbol)) && + !classDeclarationSymbol.GetAttributes().Any(a => + SymbolEqualityComparer.Default.Equals(a.AttributeClass, iNotifyPropertyChangedSymbol) && !a.HasNamedArgument(nameof(INotifyPropertyChangedAttribute.IncludeAdditionalHelperMethods), false))) { descriptor = MissingBaseObservableObjectFunctionalityError; @@ -61,6 +67,7 @@ protected override bool ValidateTargetType( /// protected override IEnumerable FilterDeclaredMembers( + GeneratorExecutionContext context, AttributeData attributeData, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, @@ -92,8 +99,10 @@ classDeclarationSymbol.InstanceConstructors[0] is } } + INamedTypeSymbol observableValidatorSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator")!; + // Skip the SetProperty overloads if the target type inherits from ObservableValidator, to avoid conflicts - if (classDeclarationSymbol.InheritsFrom("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator")) + if (classDeclarationSymbol.InheritsFrom(observableValidatorSymbol)) { foreach (MemberDeclarationSyntax member in sourceDeclaration.Members.Where(static member => member is not ConstructorDeclarationSyntax)) { diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs index 127c53052d2..2ea4c909c69 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -31,6 +31,11 @@ public void Initialize(GeneratorInitializationContext context) /// public void Execute(GeneratorExecutionContext context) { + // Get the symbol for the ObservableValidator type and the ValidationAttribute type + INamedTypeSymbol + validatorSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator")!, + validationSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute")!; + // Find all the class symbols inheriting from ObservableValidator, that are not generic IEnumerable classSymbols = from syntaxTree in context.Compilation.SyntaxTrees @@ -39,7 +44,7 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType /// The input instance to process. + /// The type symbol for the ValidationAttribute type. /// The sequence of instances to validate declared properties. [Pure] - private static IEnumerable EnumerateValidationStatements(INamedTypeSymbol classSymbol) + private static IEnumerable EnumerateValidationStatements(INamedTypeSymbol classSymbol, INamedTypeSymbol validationSymbol) { foreach (var propertySymbol in classSymbol.GetMembers().OfType()) { @@ -139,7 +145,7 @@ private static IEnumerable EnumerateValidationStatements(INamed ImmutableArray attributes = propertySymbol.GetAttributes(); - if (!attributes.Any(static a => a.AttributeClass?.InheritsFrom("System.ComponentModel.DataAnnotations.ValidationAttribute") == true)) + if (!attributes.Any(a => a.AttributeClass?.InheritsFrom(validationSymbol) == true)) { continue; } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index ea67eeafacf..02b3faff1bc 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -41,13 +41,16 @@ public void Initialize(GeneratorInitializationContext context) /// public void Execute(GeneratorExecutionContext context) { + // Get the symbol for the current target attribute + INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(TAttribute).FullName)!; + // Find all the target attribute usages IEnumerable attributes = from syntaxTree in context.Compilation.SyntaxTrees let semanticModel = context.Compilation.GetSemanticModel(syntaxTree) from attribute in syntaxTree.GetRoot().DescendantNodes().OfType() let typeInfo = semanticModel.GetTypeInfo(attribute) - where typeInfo.Type?.Name == typeof(TAttribute).Name + where SymbolEqualityComparer.Default.Equals(typeInfo.Type, attributeSymbol) select attribute; SyntaxTree? sourceSyntaxTree = null; @@ -72,7 +75,7 @@ from attribute in syntaxTree.GetRoot().DescendantNodes().OfType INamedTypeSymbol classDeclarationSymbol = semanticModel.GetDeclaredSymbol(classDeclaration)!; AttributeData attributeData = classDeclarationSymbol.GetAttributes().First(a => a.ApplicationSyntaxReference?.GetSyntax() == attribute); - if (!ValidateTargetType(attributeData, classDeclaration, classDeclarationSymbol, out var descriptor)) + if (!ValidateTargetType(context, attributeData, classDeclaration, classDeclarationSymbol, out var descriptor)) { context.ReportDiagnostic(descriptor, attribute, classDeclarationSymbol); @@ -132,7 +135,7 @@ private void OnExecute( ClassDeclaration(classDeclaration.Identifier.Text) .WithModifiers(classDeclaration.Modifiers) .WithBaseList(baseListSyntax) - .AddMembers(FilterDeclaredMembers(attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration).ToArray()); + .AddMembers(FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -171,12 +174,14 @@ private void OnExecute( /// /// Validates a target type being processed. /// + /// The input instance to use. /// The for the current attribute being processed. /// The node to process. /// The for . /// The resulting to emit in case the target type isn't valid. /// Whether or not the target type is valid and can be processed normally. protected abstract bool ValidateTargetType( + GeneratorExecutionContext context, AttributeData attributeData, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, @@ -185,12 +190,14 @@ protected abstract bool ValidateTargetType( /// /// Filters the nodes to generate from the input parsed tree. /// + /// The input instance to use. /// The for the current attribute being processed. /// The node to process. /// The for . /// The parsed instance with the source nodes. /// A sequence of nodes to emit in the generated file. protected virtual IEnumerable FilterDeclaredMembers( + GeneratorExecutionContext context, AttributeData attributeData, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs index fe1586961dd..e04728d019c 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs @@ -53,18 +53,16 @@ public static string GetFullMetadataNameForFileName(this INamedTypeSymbol symbol /// Checks whether or not a given inherits from a specified type. /// /// The target instance to check. - /// The full name of the type to check for inheritance (without global qualifier). - /// Whether or not inherits from . + /// The type symbol of the type to check for inheritance. + /// Whether or not inherits from . [Pure] - public static bool InheritsFrom(this INamedTypeSymbol typeSymbol, string typeName) + public static bool InheritsFrom(this INamedTypeSymbol typeSymbol, INamedTypeSymbol targetTypeSymbol) { - typeName = "global::" + typeName; - INamedTypeSymbol? baseType = typeSymbol.BaseType; while (baseType != null) { - if (baseType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == typeName) + if (SymbolEqualityComparer.Default.Equals(baseType, targetTypeSymbol)) { return true; } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index 525a53c23db..e4f7b3197fe 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.Contracts; using System.Linq; using System.Text; @@ -30,6 +31,9 @@ public void Initialize(GeneratorInitializationContext context) /// public void Execute(GeneratorExecutionContext context) { + // Get the symbol for the IRecipient interface type + INamedTypeSymbol iRecipientSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Messaging.IRecipient`1")!.ConstructUnboundGenericType(); + // Find all the class symbols with at least one IRecipient usage, that are not generic IEnumerable classSymbols = from syntaxTree in context.Compilation.SyntaxTrees @@ -38,9 +42,9 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType + classSymbol.AllInterfaces.Any(i => i.IsGenericType && - i.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::Microsoft.Toolkit.Mvvm.Messaging.IRecipient<>") + SymbolEqualityComparer.Default.Equals(i.ConstructUnboundGenericType(), iRecipientSymbol)) select classSymbol; // Prepare the attributes to add to the first class declaration @@ -123,7 +127,7 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType /// The input instance to process. + /// The type symbol for the IRecipient<T> interface. /// The sequence of instances to register message handleers. [Pure] - private static IEnumerable EnumerateRegistrationStatements(INamedTypeSymbol classSymbol) + private static IEnumerable EnumerateRegistrationStatements(INamedTypeSymbol classSymbol, INamedTypeSymbol iRecipientSymbol) { foreach (var interfaceSymbol in classSymbol.AllInterfaces) { if (!interfaceSymbol.IsGenericType || - interfaceSymbol.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) != "global::Microsoft.Toolkit.Mvvm.Messaging.IRecipient<>") + !SymbolEqualityComparer.Default.Equals(interfaceSymbol.ConstructUnboundGenericType(), iRecipientSymbol)) { continue; } @@ -194,14 +199,15 @@ private static IEnumerable EnumerateRegistrationStatements(INam /// Gets a sequence of statements to register declared message handlers with custom tokens. /// /// The input instance to process. + /// The type symbol for the IRecipient<T> interface. /// The sequence of instances to register message handleers. [Pure] - private static IEnumerable EnumerateRegistrationStatementsWithTokens(INamedTypeSymbol classSymbol) + private static IEnumerable EnumerateRegistrationStatementsWithTokens(INamedTypeSymbol classSymbol, INamedTypeSymbol iRecipientSymbol) { foreach (var interfaceSymbol in classSymbol.AllInterfaces) { if (!interfaceSymbol.IsGenericType || - interfaceSymbol.ConstructUnboundGenericType().ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) != "global::Microsoft.Toolkit.Mvvm.Messaging.IRecipient<>") + !SymbolEqualityComparer.Default.Equals(interfaceSymbol.ConstructUnboundGenericType(), iRecipientSymbol)) { continue; } From bfd21a22c2166487efc881d47bbd1fd9ef47b8f8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 26 Mar 2021 01:27:21 +0100 Subject: [PATCH 40/89] Removed unnecessary ConstructUnboundGenericType() calls --- .../Messaging/IMessengerRegisterAllGenerator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index e4f7b3197fe..0cb2d9cfbbb 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -32,7 +32,7 @@ public void Initialize(GeneratorInitializationContext context) public void Execute(GeneratorExecutionContext context) { // Get the symbol for the IRecipient interface type - INamedTypeSymbol iRecipientSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Messaging.IRecipient`1")!.ConstructUnboundGenericType(); + INamedTypeSymbol iRecipientSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Messaging.IRecipient`1")!; // Find all the class symbols with at least one IRecipient usage, that are not generic IEnumerable classSymbols = @@ -44,7 +44,7 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType i.IsGenericType && - SymbolEqualityComparer.Default.Equals(i.ConstructUnboundGenericType(), iRecipientSymbol)) + SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, iRecipientSymbol)) select classSymbol; // Prepare the attributes to add to the first class declaration @@ -172,7 +172,7 @@ private static IEnumerable EnumerateRegistrationStatements(INam foreach (var interfaceSymbol in classSymbol.AllInterfaces) { if (!interfaceSymbol.IsGenericType || - !SymbolEqualityComparer.Default.Equals(interfaceSymbol.ConstructUnboundGenericType(), iRecipientSymbol)) + !SymbolEqualityComparer.Default.Equals(interfaceSymbol.ConstructedFrom, iRecipientSymbol)) { continue; } @@ -207,7 +207,7 @@ private static IEnumerable EnumerateRegistrationStatementsWithT foreach (var interfaceSymbol in classSymbol.AllInterfaces) { if (!interfaceSymbol.IsGenericType || - !SymbolEqualityComparer.Default.Equals(interfaceSymbol.ConstructUnboundGenericType(), iRecipientSymbol)) + !SymbolEqualityComparer.Default.Equals(interfaceSymbol.ConstructedFrom, iRecipientSymbol)) { continue; } From 4de5fd42e44a1c1c8bc2a68a65f72c6dbbfc25ac Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 26 Mar 2021 10:57:32 +0100 Subject: [PATCH 41/89] Switched usage of ISymbol.ConstructedFrom to OriginalDefinition --- .../Messaging/IMessengerRegisterAllGenerator.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index 0cb2d9cfbbb..feaf37df00f 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -42,9 +42,7 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType - i.IsGenericType && - SymbolEqualityComparer.Default.Equals(i.ConstructedFrom, iRecipientSymbol)) + classSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, iRecipientSymbol)) select classSymbol; // Prepare the attributes to add to the first class declaration @@ -171,8 +169,7 @@ private static IEnumerable EnumerateRegistrationStatements(INam { foreach (var interfaceSymbol in classSymbol.AllInterfaces) { - if (!interfaceSymbol.IsGenericType || - !SymbolEqualityComparer.Default.Equals(interfaceSymbol.ConstructedFrom, iRecipientSymbol)) + if (!SymbolEqualityComparer.Default.Equals(interfaceSymbol.OriginalDefinition, iRecipientSymbol)) { continue; } @@ -206,8 +203,7 @@ private static IEnumerable EnumerateRegistrationStatementsWithT { foreach (var interfaceSymbol in classSymbol.AllInterfaces) { - if (!interfaceSymbol.IsGenericType || - !SymbolEqualityComparer.Default.Equals(interfaceSymbol.ConstructedFrom, iRecipientSymbol)) + if (!SymbolEqualityComparer.Default.Equals(interfaceSymbol.OriginalDefinition, iRecipientSymbol)) { continue; } From a2ac0735ba2f2a2008f455b23c14e3963d86dbac Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 26 Mar 2021 11:39:22 +0100 Subject: [PATCH 42/89] Added custom SyntaxReceiver to TransitiveMembersGenerator --- ...ansitiveMembersGenerator.SyntaxReceiver.cs | 46 ++++++++++++ .../TransitiveMembersGenerator.cs | 70 +++++++++---------- .../IMessengerRegisterAllGenerator.cs | 1 - 3 files changed, 81 insertions(+), 36 deletions(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs new file mode 100644 index 00000000000..ff7c6cdb415 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs @@ -0,0 +1,46 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + public abstract partial class TransitiveMembersGenerator + { + /// + /// An that selects candidate nodes to process. + /// + private sealed class SyntaxReceiver : ISyntaxContextReceiver + { + /// + /// The list of info gathered during exploration. + /// + private readonly List<(ClassDeclarationSyntax Class, AttributeSyntax Attribute, AttributeData Data)> gatheredInfo = new(); + + /// + /// Gets the collection of gathered info to process. + /// + public IReadOnlyCollection<(ClassDeclarationSyntax Class, AttributeSyntax Attribute, AttributeData Data)> GatheredInfo => this.gatheredInfo; + + /// + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is ClassDeclarationSyntax classDeclaration && + classDeclaration.AttributeLists.Count > 0 && + context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol classSymbol && + classSymbol.GetAttributes().FirstOrDefault(static a => a.AttributeClass?.ToDisplayString() == typeof(TAttribute).FullName) is AttributeData attributeData && + attributeData.ApplicationSyntaxReference is SyntaxReference syntaxReference && + syntaxReference.GetSyntax() is AttributeSyntax attributeSyntax) + { + this.gatheredInfo.Add((classDeclaration, attributeSyntax, attributeData)); + } + } + } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 02b3faff1bc..35181e6704f 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; using System.IO; using System.Linq; using System.Reflection; @@ -25,7 +26,7 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators /// A source generator for the type. /// /// The type of the source attribute to look for. - public abstract class TransitiveMembersGenerator : ISourceGenerator + public abstract partial class TransitiveMembersGenerator : ISourceGenerator where TAttribute : Attribute { /// @@ -36,63 +37,62 @@ public abstract class TransitiveMembersGenerator : ISourceGenerator /// public void Initialize(GeneratorInitializationContext context) { + context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver()); } /// public void Execute(GeneratorExecutionContext context) { - // Get the symbol for the current target attribute - INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(TAttribute).FullName)!; - - // Find all the target attribute usages - IEnumerable attributes = - from syntaxTree in context.Compilation.SyntaxTrees - let semanticModel = context.Compilation.GetSemanticModel(syntaxTree) - from attribute in syntaxTree.GetRoot().DescendantNodes().OfType() - let typeInfo = semanticModel.GetTypeInfo(attribute) - where SymbolEqualityComparer.Default.Equals(typeInfo.Type, attributeSymbol) - select attribute; - - SyntaxTree? sourceSyntaxTree = null; - - foreach (AttributeSyntax attribute in attributes) + // Get the syntax receiver with the candidate nodes + if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver || + syntaxReceiver.GatheredInfo.Count == 0) { - // Load the source syntax tree if needed - if (sourceSyntaxTree is null) - { - string filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.EmbeddedResources.{typeof(TAttribute).Name.Replace("Attribute", string.Empty)}.cs"; - - Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename); - StreamReader reader = new(stream); - - string observableObjectSource = reader.ReadToEnd(); + return; + } - sourceSyntaxTree = CSharpSyntaxTree.ParseText(observableObjectSource); - } + // Load the syntax tree with the members to generate + SyntaxTree sourceSyntaxTree = LoadSourceSyntaxTree(); - ClassDeclarationSyntax classDeclaration = attribute.FirstAncestorOrSelf()!; - SemanticModel semanticModel = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree); - INamedTypeSymbol classDeclarationSymbol = semanticModel.GetDeclaredSymbol(classDeclaration)!; - AttributeData attributeData = classDeclarationSymbol.GetAttributes().First(a => a.ApplicationSyntaxReference?.GetSyntax() == attribute); + foreach (var info in syntaxReceiver.GatheredInfo) + { + SemanticModel semanticModel = context.Compilation.GetSemanticModel(info.Class.SyntaxTree); + INamedTypeSymbol classDeclarationSymbol = semanticModel.GetDeclaredSymbol(info.Class)!; - if (!ValidateTargetType(context, attributeData, classDeclaration, classDeclarationSymbol, out var descriptor)) + if (!ValidateTargetType(context, info.Data, info.Class, classDeclarationSymbol, out var descriptor)) { - context.ReportDiagnostic(descriptor, attribute, classDeclarationSymbol); + context.ReportDiagnostic(descriptor, info.Attribute, classDeclarationSymbol); continue; } try { - OnExecute(context, attributeData, classDeclaration, classDeclarationSymbol, sourceSyntaxTree); + OnExecute(context, info.Data, info.Class, classDeclarationSymbol, sourceSyntaxTree); } catch { - context.ReportDiagnostic(TargetTypeErrorDescriptor, attribute, classDeclarationSymbol); + context.ReportDiagnostic(TargetTypeErrorDescriptor, info.Attribute, classDeclarationSymbol); } } } + /// + /// Loads the source syntax tree for the current generator. + /// + /// The syntax tree with the elements to emit in the generated code. + [Pure] + private static SyntaxTree LoadSourceSyntaxTree() + { + string filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.EmbeddedResources.{typeof(TAttribute).Name.Replace("Attribute", string.Empty)}.cs"; + + Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename); + StreamReader reader = new(stream); + + string observableObjectSource = reader.ReadToEnd(); + + return CSharpSyntaxTree.ParseText(observableObjectSource); + } + /// /// Processes a given target type. /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index feaf37df00f..50b773bf04b 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.Contracts; using System.Linq; using System.Text; From 9a543e8237bdc1845c61b199814bcb45c07b3656 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 26 Mar 2021 11:49:02 +0100 Subject: [PATCH 43/89] Minor code refactoring --- ...ansitiveMembersGenerator.SyntaxReceiver.cs | 19 ++++++++++++++++--- .../TransitiveMembersGenerator.cs | 13 +++++-------- .../IsExternalInit.cs | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 11 deletions(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/System.Runtime.CompilerServices/IsExternalInit.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs index ff7c6cdb415..988a1934c64 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs @@ -21,12 +21,12 @@ private sealed class SyntaxReceiver : ISyntaxContextReceiver /// /// The list of info gathered during exploration. /// - private readonly List<(ClassDeclarationSyntax Class, AttributeSyntax Attribute, AttributeData Data)> gatheredInfo = new(); + private readonly List gatheredInfo = new(); /// /// Gets the collection of gathered info to process. /// - public IReadOnlyCollection<(ClassDeclarationSyntax Class, AttributeSyntax Attribute, AttributeData Data)> GatheredInfo => this.gatheredInfo; + public IReadOnlyCollection GatheredInfo => this.gatheredInfo; /// public void OnVisitSyntaxNode(GeneratorSyntaxContext context) @@ -38,9 +38,22 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) attributeData.ApplicationSyntaxReference is SyntaxReference syntaxReference && syntaxReference.GetSyntax() is AttributeSyntax attributeSyntax) { - this.gatheredInfo.Add((classDeclaration, attributeSyntax, attributeData)); + this.gatheredInfo.Add(new Item(classDeclaration, classSymbol, attributeSyntax, attributeData)); } } + + /// + /// A model for a group of item representing a discovered type to process. + /// + /// The instance for the target class declaration. + /// The instance for . + /// The instance for the target attribute over . + /// The instance for . + public sealed record Item( + ClassDeclarationSyntax ClassDeclaration, + INamedTypeSymbol ClassSymbol, + AttributeSyntax AttributeSyntax, + AttributeData AttributeData); } } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 35181e6704f..bb11cd04bd6 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -53,25 +53,22 @@ public void Execute(GeneratorExecutionContext context) // Load the syntax tree with the members to generate SyntaxTree sourceSyntaxTree = LoadSourceSyntaxTree(); - foreach (var info in syntaxReceiver.GatheredInfo) + foreach (SyntaxReceiver.Item item in syntaxReceiver.GatheredInfo) { - SemanticModel semanticModel = context.Compilation.GetSemanticModel(info.Class.SyntaxTree); - INamedTypeSymbol classDeclarationSymbol = semanticModel.GetDeclaredSymbol(info.Class)!; - - if (!ValidateTargetType(context, info.Data, info.Class, classDeclarationSymbol, out var descriptor)) + if (!ValidateTargetType(context, item.AttributeData, item.ClassDeclaration, item.ClassSymbol, out var descriptor)) { - context.ReportDiagnostic(descriptor, info.Attribute, classDeclarationSymbol); + context.ReportDiagnostic(descriptor, item.AttributeSyntax, item.ClassSymbol); continue; } try { - OnExecute(context, info.Data, info.Class, classDeclarationSymbol, sourceSyntaxTree); + OnExecute(context, item.AttributeData, item.ClassDeclaration, item.ClassSymbol, sourceSyntaxTree); } catch { - context.ReportDiagnostic(TargetTypeErrorDescriptor, info.Attribute, classDeclarationSymbol); + context.ReportDiagnostic(TargetTypeErrorDescriptor, item.AttributeSyntax, item.ClassSymbol); } } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/System.Runtime.CompilerServices/IsExternalInit.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/System.Runtime.CompilerServices/IsExternalInit.cs new file mode 100644 index 00000000000..cadcf5a8570 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/System.Runtime.CompilerServices/IsExternalInit.cs @@ -0,0 +1,17 @@ +// 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.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} \ No newline at end of file From 2acbff6aa130135fb9e549a76b9cc4afb37c397b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 26 Mar 2021 12:05:14 +0100 Subject: [PATCH 44/89] Switched to SyntaxReceiver in remaining generators --- ...teAllPropertiesGenerator.SyntaxReceiver.cs | 45 +++++++++++++++++++ ...ValidatorValidateAllPropertiesGenerator.cs | 27 +++++------ ...ansitiveMembersGenerator.SyntaxReceiver.cs | 3 +- ...ngerRegisterAllGenerator.SyntaxReceiver.cs | 45 +++++++++++++++++++ .../IMessengerRegisterAllGenerator.cs | 23 +++++----- 5 files changed, 113 insertions(+), 30 deletions(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.SyntaxReceiver.cs create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.SyntaxReceiver.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.SyntaxReceiver.cs new file mode 100644 index 00000000000..dbe03c77b0b --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.SyntaxReceiver.cs @@ -0,0 +1,45 @@ +// 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.Collections.Generic; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + public sealed partial class ObservableValidatorValidateAllPropertiesGenerator + { + /// + /// An that selects candidate nodes to process. + /// + private sealed class SyntaxReceiver : ISyntaxContextReceiver + { + /// + /// The list of info gathered during exploration. + /// + private readonly List gatheredInfo = new(); + + /// + /// Gets the collection of gathered info to process. + /// + public IReadOnlyCollection GatheredInfo => this.gatheredInfo; + + /// + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is ClassDeclarationSyntax classDeclaration && + context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol classSymbol && + !classSymbol.IsGenericType && + context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator") is INamedTypeSymbol validatorSymbol && + classSymbol.InheritsFrom(validatorSymbol)) + { + this.gatheredInfo.Add(classSymbol); + } + } + } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs index 2ea4c909c69..eb2a8c5605e 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -21,31 +21,26 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators /// A source generator for properties validation without relying on compiled LINQ expressions. /// [Generator] - public sealed class ObservableValidatorValidateAllPropertiesGenerator : ISourceGenerator + public sealed partial class ObservableValidatorValidateAllPropertiesGenerator : ISourceGenerator { /// public void Initialize(GeneratorInitializationContext context) { + context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver()); } /// public void Execute(GeneratorExecutionContext context) { - // Get the symbol for the ObservableValidator type and the ValidationAttribute type - INamedTypeSymbol - validatorSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator")!, - validationSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute")!; + // Get the syntax receiver with the candidate nodes + if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver || + syntaxReceiver.GatheredInfo.Count == 0) + { + return; + } - // Find all the class symbols inheriting from ObservableValidator, that are not generic - IEnumerable classSymbols = - from syntaxTree in context.Compilation.SyntaxTrees - let semanticModel = context.Compilation.GetSemanticModel(syntaxTree) - from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType() - let classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration) - where - classSymbol is { IsGenericType: false } && - classSymbol.InheritsFrom(validatorSymbol) - select classSymbol; + // Get the symbol for the ValidationAttribute type + INamedTypeSymbol validationSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute")!; // Prepare the attributes to add to the first class declaration AttributeListSyntax[] classAttributes = new[] @@ -60,7 +55,7 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType 0 && context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol classSymbol && - classSymbol.GetAttributes().FirstOrDefault(static a => a.AttributeClass?.ToDisplayString() == typeof(TAttribute).FullName) is AttributeData attributeData && + context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(TAttribute).FullName) is INamedTypeSymbol attributeSymbol && + classSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)) is AttributeData attributeData && attributeData.ApplicationSyntaxReference is SyntaxReference syntaxReference && syntaxReference.GetSyntax() is AttributeSyntax attributeSyntax) { diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.SyntaxReceiver.cs new file mode 100644 index 00000000000..1c5c3730b4f --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.SyntaxReceiver.cs @@ -0,0 +1,45 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + public sealed partial class IMessengerRegisterAllGenerator + { + /// + /// An that selects candidate nodes to process. + /// + private sealed class SyntaxReceiver : ISyntaxContextReceiver + { + /// + /// The list of info gathered during exploration. + /// + private readonly List gatheredInfo = new(); + + /// + /// Gets the collection of gathered info to process. + /// + public IReadOnlyCollection GatheredInfo => this.gatheredInfo; + + /// + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is ClassDeclarationSyntax classDeclaration && + context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol classSymbol && + !classSymbol.IsGenericType && + context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Messaging.IRecipient`1") is INamedTypeSymbol iRecipientSymbol && + classSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, iRecipientSymbol))) + { + this.gatheredInfo.Add(classSymbol); + } + } + } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index 50b773bf04b..c4811da710c 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -20,30 +20,27 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators /// A source generator for message registration without relying on compiled LINQ expressions. /// [Generator] - public sealed class IMessengerRegisterAllGenerator : ISourceGenerator + public sealed partial class IMessengerRegisterAllGenerator : ISourceGenerator { /// public void Initialize(GeneratorInitializationContext context) { + context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver()); } /// public void Execute(GeneratorExecutionContext context) { + // Get the syntax receiver with the candidate nodes + if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver || + syntaxReceiver.GatheredInfo.Count == 0) + { + return; + } + // Get the symbol for the IRecipient interface type INamedTypeSymbol iRecipientSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Messaging.IRecipient`1")!; - // Find all the class symbols with at least one IRecipient usage, that are not generic - IEnumerable classSymbols = - from syntaxTree in context.Compilation.SyntaxTrees - let semanticModel = context.Compilation.GetSemanticModel(syntaxTree) - from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType() - let classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration) - where - classSymbol is { IsGenericType: false } && - classSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, iRecipientSymbol)) - select classSymbol; - // Prepare the attributes to add to the first class declaration AttributeListSyntax[] classAttributes = new[] { @@ -57,7 +54,7 @@ from classDeclaration in syntaxTree.GetRoot().DescendantNodes().OfType Date: Fri, 26 Mar 2021 16:23:55 +0100 Subject: [PATCH 45/89] Minor code style tweaks --- ...leValidatorValidateAllPropertiesGenerator.SyntaxReceiver.cs | 3 +-- .../TransitiveMembersGenerator.SyntaxReceiver.cs | 3 +-- .../Messaging/IMessengerRegisterAllGenerator.SyntaxReceiver.cs | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.SyntaxReceiver.cs index dbe03c77b0b..ece1653b246 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.SyntaxReceiver.cs @@ -32,8 +32,7 @@ private sealed class SyntaxReceiver : ISyntaxContextReceiver public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { if (context.Node is ClassDeclarationSyntax classDeclaration && - context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol classSymbol && - !classSymbol.IsGenericType && + context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol { IsGenericType: false } classSymbol && context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator") is INamedTypeSymbol validatorSymbol && classSymbol.InheritsFrom(validatorSymbol)) { diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs index 2b2b9bc8e46..780b4c4d0fd 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs @@ -31,8 +31,7 @@ private sealed class SyntaxReceiver : ISyntaxContextReceiver /// public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { - if (context.Node is ClassDeclarationSyntax classDeclaration && - classDeclaration.AttributeLists.Count > 0 && + if (context.Node is ClassDeclarationSyntax { AttributeLists: { Count: > 0 } } classDeclaration && context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol classSymbol && context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(TAttribute).FullName) is INamedTypeSymbol attributeSymbol && classSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)) is AttributeData attributeData && diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.SyntaxReceiver.cs index 1c5c3730b4f..979cfb8f492 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.SyntaxReceiver.cs @@ -32,8 +32,7 @@ private sealed class SyntaxReceiver : ISyntaxContextReceiver public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { if (context.Node is ClassDeclarationSyntax classDeclaration && - context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol classSymbol && - !classSymbol.IsGenericType && + context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol { IsGenericType: false } classSymbol && context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Messaging.IRecipient`1") is INamedTypeSymbol iRecipientSymbol && classSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i.OriginalDefinition, iRecipientSymbol))) { From 06595aeada611e5c171b9566618f7f02ad430bca Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 26 Mar 2021 20:53:20 +0100 Subject: [PATCH 46/89] Added debugging attributes to validator/messaging generated classes --- ...bservableValidatorValidateAllPropertiesGenerator.cs | 10 +++++++++- .../Messaging/IMessengerRegisterAllGenerator.cs | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs index eb2a8c5605e..894544d5a69 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -45,6 +45,8 @@ public void Execute(GeneratorExecutionContext context) // Prepare the attributes to add to the first class declaration AttributeListSyntax[] classAttributes = new[] { + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("DebuggerNonUserCode")))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ExcludeFromCodeCoverage")))), AttributeList(SingletonSeparatedList( Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), @@ -68,9 +70,13 @@ public void Execute(GeneratorExecutionContext context) // // using System; // using System.ComponentModel; + // using System.Diagnostics; + // using System.Diagnostics.CodeAnalysis; // // namespace Microsoft.Toolkit.Mvvm.ComponentModel.__Internals // { + // [DebuggerNonUserCode] + // [ExcludeFromCodeCoverage] // [EditorBrowsable(EditorBrowsableState.Never)] // [Obsolete("This type is not intended to be used directly by user code")] // internal static partial class __ObservableValidatorExtensions @@ -90,7 +96,9 @@ public void Execute(GeneratorExecutionContext context) Comment("// The .NET Foundation licenses this file to you under the MIT license."), Comment("// See the LICENSE file in the project root for more information."), Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))), - UsingDirective(IdentifierName("System.ComponentModel"))).AddMembers( + UsingDirective(IdentifierName("System.ComponentModel")), + UsingDirective(IdentifierName("System.Diagnostics")), + UsingDirective(IdentifierName("System.Diagnostics.CodeAnalysis"))).AddMembers( NamespaceDeclaration(IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals")).AddMembers( ClassDeclaration("__ObservableValidatorExtensions").AddModifiers( Token(SyntaxKind.InternalKeyword), diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index c4811da710c..7c8336cf23b 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -44,6 +44,8 @@ public void Execute(GeneratorExecutionContext context) // Prepare the attributes to add to the first class declaration AttributeListSyntax[] classAttributes = new[] { + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("DebuggerNonUserCode")))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ExcludeFromCodeCoverage")))), AttributeList(SingletonSeparatedList( Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), @@ -70,9 +72,13 @@ public void Execute(GeneratorExecutionContext context) // // using System; // using System.ComponentModel; + // using System.Diagnostics; + // using System.Diagnostics.CodeAnalysis; // // namespace Microsoft.Toolkit.Mvvm.Messaging.__Internals // { + // [DebuggerNonUserCode] + // [ExcludeFromCodeCoverage] // [EditorBrowsable(EditorBrowsableState.Never)] // [Obsolete("This type is not intended to be used directly by user code")] // internal static partial class __IMessengerExtensions @@ -100,7 +106,9 @@ public void Execute(GeneratorExecutionContext context) Comment("// The .NET Foundation licenses this file to you under the MIT license."), Comment("// See the LICENSE file in the project root for more information."), Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))), - UsingDirective(IdentifierName("System.ComponentModel"))).AddMembers( + UsingDirective(IdentifierName("System.ComponentModel")), + UsingDirective(IdentifierName("System.Diagnostics")), + UsingDirective(IdentifierName("System.Diagnostics.CodeAnalysis"))).AddMembers( NamespaceDeclaration(IdentifierName("Microsoft.Toolkit.Mvvm.Messaging.__Internals")).AddMembers( ClassDeclaration("__IMessengerExtensions").AddModifiers( Token(SyntaxKind.InternalKeyword), From 3e293884a3e633322fa6f50d2e9141c4c1236ab4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 27 Mar 2021 17:44:30 +0100 Subject: [PATCH 47/89] Added debugging attributes to remaining generated classes --- .../INotifyPropertyChanged.cs | 41 +- .../EmbeddedResources/ObservableObject.cs | 618 ++++++++++++++++++ .../EmbeddedResources/ObservableRecipient.cs | 329 ++++++++++ ...osoft.Toolkit.Mvvm.SourceGenerators.csproj | 4 +- .../ComponentModel/ObservableObject.cs | 7 +- .../ComponentModel/ObservableRecipient.cs | 7 + 6 files changed, 998 insertions(+), 8 deletions(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableObject.cs create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs index 2551afc9302..34f8f7b5f5f 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs @@ -2,28 +2,33 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#pragma warning disable + using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading.Tasks; -#pragma warning disable SA1300, SA1649 - namespace Microsoft.Toolkit.Mvvm.ComponentModel { /// /// A base class for objects implementing . /// - public abstract class __NotifyPropertyChanged : INotifyPropertyChanged + public abstract class NotifyPropertyChanged : INotifyPropertyChanged { /// + [ExcludeFromCodeCoverage] public event PropertyChangedEventHandler? PropertyChanged; /// /// Raises the event. /// /// The input instance. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { PropertyChanged?.Invoke(this, e); @@ -33,6 +38,8 @@ protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) /// Raises the event. /// /// (optional) The name of the property that changed. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) { OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); @@ -50,6 +57,8 @@ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) /// /// The event is not raised if the current and new value for the target property are the same. /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, newValue)) @@ -75,6 +84,8 @@ protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string /// The instance to use to compare the input values. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null) { if (comparer.Equals(field, newValue)) @@ -110,6 +121,8 @@ protected bool SetProperty(ref T field, T newValue, IEqualityComparer comp /// /// The event is not raised if the current and new value for the target property are the same. /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected bool SetProperty(T oldValue, T newValue, Action callback, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(oldValue, newValue)) @@ -136,6 +149,8 @@ protected bool SetProperty(T oldValue, T newValue, Action callback, [Calle /// A callback to invoke to update the property value. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, [CallerMemberName] string? propertyName = null) { if (comparer.Equals(oldValue, newValue)) @@ -202,6 +217,8 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer compa /// /// The event is not raised if the current and new value for the target property are the same. /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected bool SetProperty(T oldValue, T newValue, TModel model, Action callback, [CallerMemberName] string? propertyName = null) where TModel : class { @@ -233,6 +250,8 @@ protected bool SetProperty(T oldValue, T newValue, TModel model, Acti /// The callback to invoke to set the target property value, if a change has occurred. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, [CallerMemberName] string? propertyName = null) where TModel : class { @@ -280,6 +299,8 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer< /// is different than the previous one, and it does not mean the new /// instance passed as argument is in any particular state. /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); @@ -300,6 +321,8 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, /// /// The event is not raised if the current and new value for the target property are the same. /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); @@ -338,6 +361,8 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, /// is different than the previous one, and it does not mean the new /// instance passed as argument is in any particular state. /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); @@ -359,6 +384,8 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNoti /// /// The event is not raised if the current and new value for the target property are the same. /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); @@ -373,6 +400,8 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNoti /// A callback to invoke to update the property value. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] private bool SetPropertyAndNotifyOnCompletion(ITaskNotifier taskNotifier, TTask? newValue, Action callback, [CallerMemberName] string? propertyName = null) where TTask : Task { @@ -427,12 +456,16 @@ private interface ITaskNotifier /// /// Gets or sets the wrapped value. /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] TTask? Task { get; set; } } /// /// A wrapping class that can hold a value. /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected sealed class TaskNotifier : ITaskNotifier { /// @@ -465,6 +498,8 @@ internal TaskNotifier() /// A wrapping class that can hold a value. /// /// The type of value for the wrapped instance. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] protected sealed class TaskNotifier : ITaskNotifier> { /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableObject.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableObject.cs new file mode 100644 index 00000000000..526fd7f3702 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableObject.cs @@ -0,0 +1,618 @@ +// 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. + +#pragma warning disable + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Microsoft.Toolkit.Mvvm.ComponentModel +{ + /// + /// A base class for objects of which the properties must be observable. + /// + public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging + { + /// + [ExcludeFromCodeCoverage] + public event PropertyChangedEventHandler? PropertyChanged; + + /// + [ExcludeFromCodeCoverage] + public event PropertyChangingEventHandler? PropertyChanging; + + /// + /// Raises the event. + /// + /// The input instance. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) + { + PropertyChanged?.Invoke(this, e); + } + + /// + /// Raises the event. + /// + /// The input instance. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected virtual void OnPropertyChanging(PropertyChangingEventArgs e) + { + PropertyChanging?.Invoke(this, e); + } + + /// + /// Raises the event. + /// + /// (optional) The name of the property that changed. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) + { + OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); + } + + /// + /// Raises the event. + /// + /// (optional) The name of the property that changed. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected void OnPropertyChanging([CallerMemberName] string? propertyName = null) + { + OnPropertyChanging(new PropertyChangingEventArgs(propertyName)); + } + + /// + /// Compares the current and new values for a given property. If the value has changed, + /// raises the event, updates the property with the new + /// value, then raises the event. + /// + /// The type of the property that changed. + /// The field storing the property's value. + /// The property's value after the change occurred. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The and events are not raised + /// if the current and new value for the target property are the same. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) + { + // We duplicate the code here instead of calling the overload because we can't + // guarantee that the invoked SetProperty will be inlined, and we need the JIT + // to be able to see the full EqualityComparer.Default.Equals call, so that + // it'll use the intrinsics version of it and just replace the whole invocation + // with a direct comparison when possible (eg. for primitive numeric types). + // This is the fastest SetProperty overload so we particularly care about + // the codegen quality here, and the code is small and simple enough so that + // duplicating it still doesn't make the whole class harder to maintain. + if (EqualityComparer.Default.Equals(field, newValue)) + { + return false; + } + + OnPropertyChanging(propertyName); + + field = newValue; + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given property. If the value has changed, + /// raises the event, updates the property with the new + /// value, then raises the event. + /// See additional notes about this overload in . + /// + /// The type of the property that changed. + /// The field storing the property's value. + /// The property's value after the change occurred. + /// The instance to use to compare the input values. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null) + { + if (comparer.Equals(field, newValue)) + { + return false; + } + + OnPropertyChanging(propertyName); + + field = newValue; + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given property. If the value has changed, + /// raises the event, updates the property with the new + /// value, then raises the event. + /// This overload is much less efficient than and it + /// should only be used when the former is not viable (eg. when the target property being + /// updated does not directly expose a backing field that can be passed by reference). + /// For performance reasons, it is recommended to use a stateful callback if possible through + /// the whenever possible + /// instead of this overload, as that will allow the C# compiler to cache the input callback and + /// reduce the memory allocations. More info on that overload are available in the related XML + /// docs. This overload is here for completeness and in cases where that is not applicable. + /// + /// The type of the property that changed. + /// The current property value. + /// The property's value after the change occurred. + /// A callback to invoke to update the property value. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The and events are not raised + /// if the current and new value for the target property are the same. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(T oldValue, T newValue, Action callback, [CallerMemberName] string? propertyName = null) + { + // We avoid calling the overload again to ensure the comparison is inlined + if (EqualityComparer.Default.Equals(oldValue, newValue)) + { + return false; + } + + OnPropertyChanging(propertyName); + + callback(newValue); + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given property. If the value has changed, + /// raises the event, updates the property with the new + /// value, then raises the event. + /// See additional notes about this overload in . + /// + /// The type of the property that changed. + /// The current property value. + /// The property's value after the change occurred. + /// The instance to use to compare the input values. + /// A callback to invoke to update the property value. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, [CallerMemberName] string? propertyName = null) + { + if (comparer.Equals(oldValue, newValue)) + { + return false; + } + + OnPropertyChanging(propertyName); + + callback(newValue); + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given nested property. If the value has changed, + /// raises the event, updates the property and then raises the + /// event. The behavior mirrors that of , + /// with the difference being that this method is used to relay properties from a wrapped model in the + /// current instance. This type is useful when creating wrapping, bindable objects that operate over + /// models that lack support for notification (eg. for CRUD operations). + /// Suppose we have this model (eg. for a database row in a table): + /// + /// public class Person + /// { + /// public string Name { get; set; } + /// } + /// + /// We can then use a property to wrap instances of this type into our observable model (which supports + /// notifications), injecting the notification to the properties of that model, like so: + /// + /// public class BindablePerson : ObservableObject + /// { + /// public Model { get; } + /// + /// public BindablePerson(Person model) + /// { + /// Model = model; + /// } + /// + /// public string Name + /// { + /// get => Model.Name; + /// set => Set(Model.Name, value, Model, (model, name) => model.Name = name); + /// } + /// } + /// + /// This way we can then use the wrapping object in our application, and all those "proxy" properties will + /// also raise notifications when changed. Note that this method is not meant to be a replacement for + /// , and it should only be used when relaying properties to a model that + /// doesn't support notifications, and only if you can't implement notifications to that model directly (eg. by having + /// it inherit from ). The syntax relies on passing the target model and a stateless callback + /// to allow the C# compiler to cache the function, which results in much better performance and no memory usage. + /// + /// The type of model whose property (or field) to set. + /// The type of property (or field) to set. + /// The current property value. + /// The property's value after the change occurred. + /// The model containing the property being updated. + /// The callback to invoke to set the target property value, if a change has occurred. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The and events are not + /// raised if the current and new value for the target property are the same. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(T oldValue, T newValue, TModel model, Action callback, [CallerMemberName] string? propertyName = null) + where TModel : class + { + if (EqualityComparer.Default.Equals(oldValue, newValue)) + { + return false; + } + + OnPropertyChanging(propertyName); + + callback(model, newValue); + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given nested property. If the value has changed, + /// raises the event, updates the property and then raises the + /// event. The behavior mirrors that of , + /// with the difference being that this method is used to relay properties from a wrapped model in the + /// current instance. See additional notes about this overload in . + /// + /// The type of model whose property (or field) to set. + /// The type of property (or field) to set. + /// The current property value. + /// The property's value after the change occurred. + /// The instance to use to compare the input values. + /// The model containing the property being updated. + /// The callback to invoke to set the target property value, if a change has occurred. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, [CallerMemberName] string? propertyName = null) + where TModel : class + { + if (comparer.Equals(oldValue, newValue)) + { + return false; + } + + OnPropertyChanging(propertyName); + + callback(model, newValue); + + OnPropertyChanged(propertyName); + + return true; + } + + /// + /// Compares the current and new values for a given field (which should be the backing + /// field for a property). If the value has changed, raises the + /// event, updates the field and then raises the event. + /// The behavior mirrors that of , with the difference being that + /// this method will also monitor the new value of the property (a generic ) and will also + /// raise the again for the target property when it completes. + /// This can be used to update bindings observing that or any of its properties. + /// This method and its overload specifically rely on the type, which needs + /// to be used in the backing field for the target property. The field doesn't need to be + /// initialized, as this method will take care of doing that automatically. The + /// type also includes an implicit operator, so it can be assigned to any instance directly. + /// Here is a sample property declaration using this method: + /// + /// private TaskNotifier myTask; + /// + /// public Task MyTask + /// { + /// get => myTask; + /// private set => SetAndNotifyOnCompletion(ref myTask, value); + /// } + /// + /// + /// The field notifier to modify. + /// The property's value after the change occurred. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The and events are not raised if the current + /// and new value for the target property are the same. The return value being only + /// indicates that the new value being assigned to is different than the previous one, + /// and it does not mean the new instance passed as argument is in any particular state. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) + { + // We invoke the overload with a callback here to avoid code duplication, and simply pass an empty callback. + // The lambda expression here is transformed by the C# compiler into an empty closure class with a + // static singleton field containing a closure instance, and another caching the instantiated Action + // instance. This will result in no further allocations after the first time this method is called for a given + // generic type. We only pay the cost of the virtual call to the delegate, but this is not performance critical + // code and that overhead would still be much lower than the rest of the method anyway, so that's fine. + return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); + } + + /// + /// Compares the current and new values for a given field (which should be the backing + /// field for a property). If the value has changed, raises the + /// event, updates the field and then raises the event. + /// This method is just like , + /// with the difference being an extra parameter with a callback being invoked + /// either immediately, if the new task has already completed or is , or upon completion. + /// + /// The field notifier to modify. + /// The property's value after the change occurred. + /// A callback to invoke to update the property value. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The and events are not raised + /// if the current and new value for the target property are the same. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null) + { + return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); + } + + /// + /// Compares the current and new values for a given field (which should be the backing + /// field for a property). If the value has changed, raises the + /// event, updates the field and then raises the event. + /// The behavior mirrors that of , with the difference being that + /// this method will also monitor the new value of the property (a generic ) and will also + /// raise the again for the target property when it completes. + /// This can be used to update bindings observing that or any of its properties. + /// This method and its overload specifically rely on the type, which needs + /// to be used in the backing field for the target property. The field doesn't need to be + /// initialized, as this method will take care of doing that automatically. The + /// type also includes an implicit operator, so it can be assigned to any instance directly. + /// Here is a sample property declaration using this method: + /// + /// private TaskNotifier<int> myTask; + /// + /// public Task<int> MyTask + /// { + /// get => myTask; + /// private set => SetAndNotifyOnCompletion(ref myTask, value); + /// } + /// + /// + /// The type of result for the to set and monitor. + /// The field notifier to modify. + /// The property's value after the change occurred. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The and events are not raised if the current + /// and new value for the target property are the same. The return value being only + /// indicates that the new value being assigned to is different than the previous one, + /// and it does not mean the new instance passed as argument is in any particular state. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) + { + return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); + } + + /// + /// Compares the current and new values for a given field (which should be the backing + /// field for a property). If the value has changed, raises the + /// event, updates the field and then raises the event. + /// This method is just like , + /// with the difference being an extra parameter with a callback being invoked + /// either immediately, if the new task has already completed or is , or upon completion. + /// + /// The type of result for the to set and monitor. + /// The field notifier to modify. + /// The property's value after the change occurred. + /// A callback to invoke to update the property value. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// The and events are not raised + /// if the current and new value for the target property are the same. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null) + { + return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); + } + + /// + /// Implements the notification logic for the related methods. + /// + /// The type of to set and monitor. + /// The field notifier. + /// The property's value after the change occurred. + /// A callback to invoke to update the property value. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + private bool SetPropertyAndNotifyOnCompletion(ITaskNotifier taskNotifier, TTask? newValue, Action callback, [CallerMemberName] string? propertyName = null) + where TTask : Task + { + if (ReferenceEquals(taskNotifier.Task, newValue)) + { + return false; + } + + // Check the status of the new task before assigning it to the + // target field. This is so that in case the task is either + // null or already completed, we can avoid the overhead of + // scheduling the method to monitor its completion. + bool isAlreadyCompletedOrNull = newValue?.IsCompleted ?? true; + + OnPropertyChanging(propertyName); + + taskNotifier.Task = newValue; + + OnPropertyChanged(propertyName); + + // If the input task is either null or already completed, we don't need to + // execute the additional logic to monitor its completion, so we can just bypass + // the rest of the method and return that the field changed here. The return value + // does not indicate that the task itself has completed, but just that the property + // value itself has changed (ie. the referenced task instance has changed). + // This mirrors the return value of all the other synchronous Set methods as well. + if (isAlreadyCompletedOrNull) + { + callback(newValue); + + return true; + } + + // We use a local async function here so that the main method can + // remain synchronous and return a value that can be immediately + // used by the caller. This mirrors Set(ref T, T, string). + // We use an async void function instead of a Task-returning function + // so that if a binding update caused by the property change notification + // causes a crash, it is immediately reported in the application instead of + // the exception being ignored (as the returned task wouldn't be awaited), + // which would result in a confusing behavior for users. + async void MonitorTask() + { + try + { + // Await the task and ignore any exceptions + await newValue!; + } + catch + { + } + + // Only notify if the property hasn't changed + if (ReferenceEquals(taskNotifier.Task, newValue)) + { + OnPropertyChanged(propertyName); + } + + callback(newValue); + } + + MonitorTask(); + + return true; + } + + /// + /// An interface for task notifiers of a specified type. + /// + /// The type of value to store. + private interface ITaskNotifier + where TTask : Task + { + /// + /// Gets or sets the wrapped value. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + TTask? Task { get; set; } + } + + /// + /// A wrapping class that can hold a value. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected sealed class TaskNotifier : ITaskNotifier + { + /// + /// Initializes a new instance of the class. + /// + internal TaskNotifier() + { + } + + private Task? task; + + /// + Task? ITaskNotifier.Task + { + get => this.task; + set => this.task = value; + } + + /// + /// Unwraps the value stored in the current instance. + /// + /// The input instance. + public static implicit operator Task?(TaskNotifier? notifier) + { + return notifier?.task; + } + } + + /// + /// A wrapping class that can hold a value. + /// + /// The type of value for the wrapped instance. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected sealed class TaskNotifier : ITaskNotifier> + { + /// + /// Initializes a new instance of the class. + /// + internal TaskNotifier() + { + } + + private Task? task; + + /// + Task? ITaskNotifier>.Task + { + get => this.task; + set => this.task = value; + } + + /// + /// Unwraps the value stored in the current instance. + /// + /// The input instance. + public static implicit operator Task?(TaskNotifier? notifier) + { + return notifier?.task; + } + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs new file mode 100644 index 00000000000..7896e6b9828 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs @@ -0,0 +1,329 @@ +// 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. + +#pragma warning disable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Microsoft.Toolkit.Mvvm.Messaging; +using Microsoft.Toolkit.Mvvm.Messaging.Messages; + +namespace Microsoft.Toolkit.Mvvm.ComponentModel +{ + /// + /// A base class for observable objects that also acts as recipients for messages. This class is an extension of + /// which also provides built-in support to use the type. + /// + public abstract class ObservableRecipient : ObservableObject + { + /// + /// Initializes a new instance of the class. + /// + /// + /// This constructor will produce an instance that will use the instance + /// to perform requested operations. It will also be available locally through the property. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected ObservableRecipient() + : this(WeakReferenceMessenger.Default) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The instance to use to send messages. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected ObservableRecipient(IMessenger messenger) + { + Messenger = messenger; + } + + /// + /// Gets the instance in use. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected IMessenger Messenger { get; } + + private bool isActive; + + /// + /// Gets or sets a value indicating whether the current view model is currently active. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + public bool IsActive + { + get => this.isActive; + set + { + if (SetProperty(ref this.isActive, value, true)) + { + if (value) + { + OnActivated(); + } + else + { + OnDeactivated(); + } + } + } + } + + /// + /// Raised whenever the property is set to . + /// 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. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected virtual void OnActivated() + { + Messenger.RegisterAll(this); + } + + /// + /// Raised whenever the property is set to . + /// Use this method to unregister from messages and do general cleanup for this instance. + /// + /// + /// The base implementation unregisters all messages for this recipient. It does so by + /// invoking , which removes all registered + /// handlers for a given subscriber, regardless of what token was used to register them. + /// That is, all registered handlers across all subscription channels will be removed. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected virtual void OnDeactivated() + { + Messenger.UnregisterAll(this); + } + + /// + /// Broadcasts a with the specified + /// parameters, without using any particular token (so using the default channel). + /// + /// The type of the property that changed. + /// The value of the property before it changed. + /// The value of the property after it changed. + /// The name of the property that changed. + /// + /// You should override this method if you wish to customize the channel being + /// used to send the message (eg. if you need to use a specific token for the channel). + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected virtual void Broadcast(T oldValue, T newValue, string? propertyName) + { + PropertyChangedMessage message = new(this, propertyName, oldValue, newValue); + + Messenger.Send(message); + } + + /// + /// Compares the current and new values for a given property. If the value has changed, + /// raises the event, updates the property with + /// the new value, then raises the event. + /// + /// The type of the property that changed. + /// The field storing the property's value. + /// The property's value after the change occurred. + /// If , will also be invoked. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// This method is just like , just with the addition + /// of the parameter. As such, following the behavior of the base method, + /// the and events + /// are not raised if the current and new value for the target property are the same. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null) + { + T oldValue = field; + + // We duplicate the code as in the base class here to leverage + // the intrinsics support for EqualityComparer.Default.Equals. + bool propertyChanged = SetProperty(ref field, newValue, propertyName); + + if (propertyChanged && broadcast) + { + Broadcast(oldValue, newValue, propertyName); + } + + return propertyChanged; + } + + /// + /// Compares the current and new values for a given property. If the value has changed, + /// raises the event, updates the property with + /// the new value, then raises the event. + /// See additional notes about this overload in . + /// + /// The type of the property that changed. + /// The field storing the property's value. + /// The property's value after the change occurred. + /// The instance to use to compare the input values. + /// If , will also be invoked. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, bool broadcast, [CallerMemberName] string? propertyName = null) + { + T oldValue = field; + + bool propertyChanged = SetProperty(ref field, newValue, comparer, propertyName); + + if (propertyChanged && broadcast) + { + Broadcast(oldValue, newValue, propertyName); + } + + return propertyChanged; + } + + /// + /// Compares the current and new values for a given property. If the value has changed, + /// raises the event, updates the property with + /// the new value, then raises the event. Similarly to + /// the method, this overload should only be + /// used when can't be used directly. + /// + /// The type of the property that changed. + /// The current property value. + /// The property's value after the change occurred. + /// A callback to invoke to update the property value. + /// If , will also be invoked. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + /// + /// This method is just like , just with the addition + /// of the parameter. As such, following the behavior of the base method, + /// the and events + /// are not raised if the current and new value for the target property are the same. + /// + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(T oldValue, T newValue, Action callback, bool broadcast, [CallerMemberName] string? propertyName = null) + { + bool propertyChanged = SetProperty(oldValue, newValue, callback, propertyName); + + if (propertyChanged && broadcast) + { + Broadcast(oldValue, newValue, propertyName); + } + + return propertyChanged; + } + + /// + /// Compares the current and new values for a given property. If the value has changed, + /// raises the event, updates the property with + /// the new value, then raises the event. + /// See additional notes about this overload in . + /// + /// The type of the property that changed. + /// The current property value. + /// The property's value after the change occurred. + /// The instance to use to compare the input values. + /// A callback to invoke to update the property value. + /// If , will also be invoked. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, bool broadcast, [CallerMemberName] string? propertyName = null) + { + bool propertyChanged = SetProperty(oldValue, newValue, comparer, callback, propertyName); + + if (propertyChanged && broadcast) + { + Broadcast(oldValue, newValue, propertyName); + } + + return propertyChanged; + } + + /// + /// Compares the current and new values for a given nested property. If the value has changed, + /// raises the event, updates the property and then raises the + /// event. The behavior mirrors that of + /// , with the difference being that this + /// method is used to relay properties from a wrapped model in the current instance. For more info, see the docs for + /// . + /// + /// The type of model whose property (or field) to set. + /// The type of property (or field) to set. + /// The current property value. + /// The property's value after the change occurred. + /// The model + /// The callback to invoke to set the target property value, if a change has occurred. + /// If , will also be invoked. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(T oldValue, T newValue, TModel model, Action callback, bool broadcast, [CallerMemberName] string? propertyName = null) + where TModel : class + { + bool propertyChanged = SetProperty(oldValue, newValue, model, callback, propertyName); + + if (propertyChanged && broadcast) + { + Broadcast(oldValue, newValue, propertyName); + } + + return propertyChanged; + } + + /// + /// Compares the current and new values for a given nested property. If the value has changed, + /// raises the event, updates the property and then raises the + /// event. The behavior mirrors that of + /// , + /// with the difference being that this method is used to relay properties from a wrapped model in the + /// current instance. For more info, see the docs for + /// . + /// + /// The type of model whose property (or field) to set. + /// The type of property (or field) to set. + /// The current property value. + /// The property's value after the change occurred. + /// The instance to use to compare the input values. + /// The model + /// The callback to invoke to set the target property value, if a change has occurred. + /// If , will also be invoked. + /// (optional) The name of the property that changed. + /// if the property was changed, otherwise. + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, bool broadcast, [CallerMemberName] string? propertyName = null) + where TModel : class + { + bool propertyChanged = SetProperty(oldValue, newValue, comparer, model, callback, propertyName); + + if (propertyChanged && broadcast) + { + Broadcast(oldValue, newValue, propertyName); + } + + return propertyChanged; + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index a2284f23244..ada74efa73d 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -23,10 +23,10 @@ PreserveNewest - + PreserveNewest - + PreserveNewest diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs index 1e992221eb8..5685e3ed413 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs @@ -9,9 +9,10 @@ // ================================== NOTE ================================== // This file is mirrored in the trimmed-down INotifyPropertyChanged file in -// the source generator project, to be used with the [INotifyPropertyChanged] -// attribute. If any changes are made to this file, they should also be -// appropriately ported to that file as well to keep the behavior consistent. +// the source generator project, to be used with the [INotifyPropertyChanged], +// attribute, along with the ObservableObject annotated copy (for debugging info). +// If any changes are made to this file, they should also be appropriately +// ported to that file as well to keep the behavior consistent. // ========================================================================== using System; diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs index dc5d5c85690..dbf15fe1d6e 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs @@ -7,6 +7,13 @@ // This file is inspired from the MvvmLight library (lbugnion/MvvmLight), // more info in ThirdPartyNotices.txt in the root of the project. +// ================================= NOTE ================================= +// This file is mirrored in the ObservableRecipient annotated copy +// (for debugging info) in the Mvvm.SourceGenerators project. +// If any changes are made to this file, they should also be appropriately +// ported to that file as well to keep the behavior consistent. +// ======================================================================== + using System; using System.Collections.Generic; using System.Runtime.CompilerServices; From d51b0ab31203727e447e2d1b1e1b3ef759f1b3a0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 28 Mar 2021 18:25:51 +0200 Subject: [PATCH 48/89] Minor code refactoring --- .../TransitiveMembersGenerator.cs | 24 +++---------------- .../EmbeddedResources/ObservableRecipient.cs | 2 +- 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index bb11cd04bd6..2f6ae7d087f 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -107,20 +107,6 @@ private void OnExecute( { ClassDeclarationSyntax sourceDeclaration = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().First(); UsingDirectiveSyntax[] usingDirectives = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().ToArray(); - BaseListSyntax? baseListSyntax = BaseList(SeparatedList( - sourceDeclaration.BaseList?.Types - .OfType() - .Select(static t => t.Type) - .OfType() - .Where(static t => t.Identifier.ValueText.StartsWith("I")) - .Select(static t => SimpleBaseType(t)) - .ToArray() - ?? Array.Empty())); - - if (baseListSyntax.Types.Count == 0) - { - baseListSyntax = null; - } // Create the class declaration for the user type. This will produce a tree as follows: // @@ -131,7 +117,7 @@ private void OnExecute( var classDeclarationSyntax = ClassDeclaration(classDeclaration.Identifier.Text) .WithModifiers(classDeclaration.Modifiers) - .WithBaseList(baseListSyntax) + .WithBaseList(sourceDeclaration.BaseList) .AddMembers(FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -151,16 +137,12 @@ private void OnExecute( // From this, we can finally generate the source code to output. var namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)); + // Create the final compilation unit to generate (with using directives and the full type declaration) var source = CompilationUnit() .AddMembers(NamespaceDeclaration(IdentifierName(namespaceName)) .AddMembers(typeDeclarationSyntax)) - .AddUsings(usingDirectives.First().WithLeadingTrivia(TriviaList( - Comment("// Licensed to the .NET Foundation under one or more agreements."), - Comment("// The .NET Foundation licenses this file to you under the MIT license."), - Comment("// See the LICENSE file in the project root for more information."), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))) - .AddUsings(usingDirectives.Skip(1).ToArray()) + .AddUsings(usingDirectives) .NormalizeWhitespace() .ToFullString(); diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs index 7896e6b9828..7398d933118 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs @@ -18,7 +18,7 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// A base class for observable objects that also acts as recipients for messages. This class is an extension of /// which also provides built-in support to use the type. /// - public abstract class ObservableRecipient : ObservableObject + public abstract class ObservableRecipient { /// /// Initializes a new instance of the class. From 8b708f6158556d95886b895b2fac4d472cb0c1f7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 28 Mar 2021 20:11:38 +0200 Subject: [PATCH 49/89] Added initial version of ObservablePropertyAttribute --- ...ervablePropertyGenerator.SyntaxReceiver.cs | 64 ++++++ .../ObservablePropertyGenerator.cs | 200 ++++++++++++++++++ ...osoft.Toolkit.Mvvm.SourceGenerators.csproj | 1 + .../Attributes/ObservablePropertyAttribute.cs | 44 ++++ .../Mvvm/Test_ObservablePropertyAttribute.cs | 59 ++++++ 5 files changed, 368 insertions(+) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs create mode 100644 Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs create mode 100644 UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs new file mode 100644 index 00000000000..38a86ab24d0 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs @@ -0,0 +1,64 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + public sealed partial class ObservablePropertyGenerator + { + /// + /// An that selects candidate nodes to process. + /// + private sealed class SyntaxReceiver : ISyntaxContextReceiver + { + /// + /// The list of info gathered during exploration. + /// + private readonly List gatheredInfo = new(); + + /// + /// Gets the collection of gathered info to process. + /// + public IReadOnlyCollection GatheredInfo => this.gatheredInfo; + + /// + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is FieldDeclarationSyntax { AttributeLists: { Count: > 0 } } fieldDeclaration && + context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol attributeSymbol) + { + foreach (VariableDeclaratorSyntax variableDeclarator in fieldDeclaration.Declaration.Variables) + { + if (context.SemanticModel.GetDeclaredSymbol(variableDeclarator) is IFieldSymbol fieldSymbol && + fieldSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)) is AttributeData attributeData && + attributeData.ApplicationSyntaxReference is SyntaxReference syntaxReference && + syntaxReference.GetSyntax() is AttributeSyntax attributeSyntax) + { + this.gatheredInfo.Add(new Item(variableDeclarator, fieldSymbol, attributeSyntax, attributeData)); + } + } + } + } + + /// + /// A model for a group of item representing a discovered type to process. + /// + /// The instance for the target field variable declaration. + /// The instance for . + /// The instance for the target attribute over . + /// The instance for . + public sealed record Item( + VariableDeclaratorSyntax FieldDeclarator, + IFieldSymbol FieldSymbol, + AttributeSyntax AttributeSyntax, + AttributeData AttributeData); + } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs new file mode 100644 index 00000000000..7cd9f420c77 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -0,0 +1,200 @@ +// 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.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + /// A source generator for the type. + /// + [Generator] + public sealed partial class ObservablePropertyGenerator : ISourceGenerator + { + /// + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver()); + } + + /// + public void Execute(GeneratorExecutionContext context) + { + // Get the syntax receiver with the candidate nodes + if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver || + syntaxReceiver.GatheredInfo.Count == 0) + { + return; + } + + foreach (var items in syntaxReceiver.GatheredInfo.GroupBy(static item => item.FieldSymbol.ContainingType, SymbolEqualityComparer.Default)) + { + if (items.Key.DeclaringSyntaxReferences.Length > 0 && + items.Key.DeclaringSyntaxReferences.First().GetSyntax() is ClassDeclarationSyntax classDeclaration) + { + OnExecute(context, classDeclaration, items.Key, items); + } + } + } + + /// + /// Processes a given target type. + /// + /// The input instance to use. + /// The node to process. + /// The for . + /// The sequence of fields to process. + private static void OnExecute( + GeneratorExecutionContext context, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + IEnumerable items) + { + // Create the class declaration for the user type. This will produce a tree as follows: + // + // + // { + // + // } + var classDeclarationSyntax = + ClassDeclaration(classDeclarationSymbol.Name) + .WithModifiers(classDeclaration.Modifiers) + .AddMembers(items.Select(static item => CreatePropertyDeclaration(item.FieldSymbol)).ToArray()); + + TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; + + // Add all parent types in ascending order, if any + foreach (var parentType in classDeclaration.Ancestors().OfType()) + { + typeDeclarationSyntax = parentType + .WithMembers(SingletonList(typeDeclarationSyntax)) + .WithConstraintClauses(List()) + .WithBaseList(null) + .WithAttributeLists(List()) + .WithoutTrivia(); + } + + // Create the compilation unit with the namespace and target member. + // From this, we can finally generate the source code to output. + var namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)); + + // Create the final compilation unit to generate (with leading trivia) + var source = + CompilationUnit().AddUsings( + UsingDirective(IdentifierName("System.Collections.Generic")).WithLeadingTrivia(TriviaList( + Comment("// Licensed to the .NET Foundation under one or more agreements."), + Comment("// The .NET Foundation licenses this file to you under the MIT license."), + Comment("// See the LICENSE file in the project root for more information."), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))), + UsingDirective(IdentifierName("System.Diagnostics")), + UsingDirective(IdentifierName("System.Diagnostics.CodeAnalysis"))).AddMembers( + NamespaceDeclaration(IdentifierName(namespaceName)) + .AddMembers(typeDeclarationSyntax)) + .NormalizeWhitespace() + .ToFullString(); + + // Add the partial type + context.AddSource($"[{typeof(ObservablePropertyAttribute).Name}]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + } + + /// + /// Creates a instance for a specified field. + /// + /// The input instance to process. + /// A generated instance for the input field. + [Pure] + private static PropertyDeclarationSyntax CreatePropertyDeclaration(IFieldSymbol fieldSymbol) + { + // Get the field type and the target property name + string + typeName = fieldSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + propertyName = fieldSymbol.Name; + + if (propertyName.StartsWith("m_")) + { + propertyName = propertyName.Substring(2); + } + else if (propertyName.StartsWith("_")) + { + propertyName = propertyName.TrimStart('_'); + } + + propertyName = $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}"; + + BlockSyntax setter = Block(); + + // Add the OnPropertyChanging() call if necessary + setter = setter.AddStatements(ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanging")))); + + // Add the following statements: + // + // = value; + // OnPropertyChanged(); + setter = setter.AddStatements( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldSymbol.Name), + IdentifierName("value"))), + ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanged")))); + + // Construct the generated property as follows: + // + // [DebuggerNonUserCode] + // [ExcludeFromCodeCoverage] + // public + // { + // get => ; + // set + // { + // if (!EqualityComparer<>.Default.Equals(, value)) + // { + // OnPropertyChanging(); // Optional + // = value; + // OnPropertyChanged(); + // } + // } + // } + return + PropertyDeclaration(IdentifierName(typeName), Identifier(propertyName)) + .AddAttributeLists( + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("DebuggerNonUserCode")))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ExcludeFromCodeCoverage"))))) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithExpressionBody(ArrowExpressionClause(IdentifierName(fieldSymbol.Name))) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), + AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) + .AddBodyStatements( + IfStatement( + PrefixUnaryExpression( + SyntaxKind.LogicalNotExpression, + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + GenericName(Identifier("EqualityComparer")) + .AddTypeArgumentListArguments(IdentifierName(typeName)), + IdentifierName("Default")), + IdentifierName("Equals"))) + .AddArgumentListArguments( + Argument(IdentifierName(fieldSymbol.Name)), + Argument(IdentifierName("value")))), + setter))); + } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index ada74efa73d..be164dc4a3a 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -16,6 +16,7 @@ + diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs new file mode 100644 index 00000000000..2957ac312b6 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs @@ -0,0 +1,44 @@ +// 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. + +#pragma warning disable CS1574 + +using System; +using System.ComponentModel; + +namespace Microsoft.Toolkit.Mvvm.ComponentModel +{ + /// + /// An attribute that indicates that a given field should be wrapped by a generated observable property. + /// In order to use this attribute, the containing type has to implement the interface + /// and expose a method with the same signature as . If the containing + /// type also implements the interface and exposes a method with the same signature as + /// , then this method will be invoked as well by the property setter. + /// + /// This attribute can be used as follows: + /// + /// partial class MyViewModel : ObservableObject + /// { + /// [ObservableProperty] + /// private string name; + /// } + /// + /// + /// And with this, code analogous to this will be generated: + /// + /// partial class MyViewModel + /// { + /// public string Name + /// { + /// get => name; + /// set => SetProperty(ref name, value); + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + public sealed class ObservablePropertyAttribute : Attribute + { + } +} diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs new file mode 100644 index 00000000000..6165719a2ca --- /dev/null +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs @@ -0,0 +1,59 @@ +// 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.ComponentModel; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.Mvvm +{ + [TestClass] + public partial class Test_ObservablePropertyAttribute + { + [TestCategory("Mvvm")] + [TestMethod] + public void Test_ObservablePropertyAttribute_Events() + { + var model = new SampleModel(); + + (PropertyChangingEventArgs, int) changing = default; + (PropertyChangedEventArgs, int) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Data); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Data); + }; + + model.Data = 42; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModel.Data)); + Assert.AreEqual(changing.Item2, 0); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModel.Data)); + Assert.AreEqual(changed.Item2, 42); + } + + public partial class SampleModel : ObservableObject + { + [ObservableProperty] + private int data; + } + } +} From 4cd8a7c66cc33aa8115bdf93c8934c8f1d250a44 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 29 Mar 2021 13:28:11 +0200 Subject: [PATCH 50/89] Added XML docs copying from fields to generated properties --- .../ObservablePropertyGenerator.SyntaxReceiver.cs | 6 +++++- .../ComponentModel/ObservablePropertyGenerator.cs | 7 +++++-- .../Mvvm/Test_ObservablePropertyAttribute.cs | 3 +++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs index 38a86ab24d0..fda1be7d65b 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs @@ -34,6 +34,8 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) if (context.Node is FieldDeclarationSyntax { AttributeLists: { Count: > 0 } } fieldDeclaration && context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol attributeSymbol) { + SyntaxTriviaList leadingTrivia = fieldDeclaration.GetLeadingTrivia(); + foreach (VariableDeclaratorSyntax variableDeclarator in fieldDeclaration.Declaration.Variables) { if (context.SemanticModel.GetDeclaredSymbol(variableDeclarator) is IFieldSymbol fieldSymbol && @@ -41,7 +43,7 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) attributeData.ApplicationSyntaxReference is SyntaxReference syntaxReference && syntaxReference.GetSyntax() is AttributeSyntax attributeSyntax) { - this.gatheredInfo.Add(new Item(variableDeclarator, fieldSymbol, attributeSyntax, attributeData)); + this.gatheredInfo.Add(new Item(leadingTrivia, variableDeclarator, fieldSymbol, attributeSyntax, attributeData)); } } } @@ -50,11 +52,13 @@ attributeData.ApplicationSyntaxReference is SyntaxReference syntaxReference && /// /// A model for a group of item representing a discovered type to process. /// + /// The leading trivia for the field declaration. /// The instance for the target field variable declaration. /// The instance for . /// The instance for the target attribute over . /// The instance for . public sealed record Item( + SyntaxTriviaList LeadingTrivia, VariableDeclaratorSyntax FieldDeclarator, IFieldSymbol FieldSymbol, AttributeSyntax AttributeSyntax, diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 7cd9f420c77..b216d22d473 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -71,7 +71,7 @@ private static void OnExecute( var classDeclarationSyntax = ClassDeclaration(classDeclarationSymbol.Name) .WithModifiers(classDeclaration.Modifiers) - .AddMembers(items.Select(static item => CreatePropertyDeclaration(item.FieldSymbol)).ToArray()); + .AddMembers(items.Select(static item => CreatePropertyDeclaration(item.LeadingTrivia, item.FieldSymbol)).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -112,10 +112,11 @@ private static void OnExecute( /// /// Creates a instance for a specified field. /// + /// The leading trivia for the field to process. /// The input instance to process. /// A generated instance for the input field. [Pure] - private static PropertyDeclarationSyntax CreatePropertyDeclaration(IFieldSymbol fieldSymbol) + private static PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol) { // Get the field type and the target property name string @@ -152,6 +153,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(IFieldSymbol // Construct the generated property as follows: // + // // [DebuggerNonUserCode] // [ExcludeFromCodeCoverage] // public @@ -172,6 +174,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(IFieldSymbol .AddAttributeLists( AttributeList(SingletonSeparatedList(Attribute(IdentifierName("DebuggerNonUserCode")))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ExcludeFromCodeCoverage"))))) + .WithLeadingTrivia(leadingTrivia) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs index 6165719a2ca..7f9957ae8be 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs @@ -52,6 +52,9 @@ public void Test_ObservablePropertyAttribute_Events() public partial class SampleModel : ObservableObject { + /// + /// This is a sample data field within of type . + /// [ObservableProperty] private int data; } From 0a0861519d00745ae386ee1daf9c120e7dc39dba Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 29 Mar 2021 14:24:10 +0200 Subject: [PATCH 51/89] Added handling for modifiers in sealed classes --- .../TransitiveMembersGenerator.cs | 12 +++- .../MemberDeclarationSyntaxExtensions.cs | 57 +++++++++++++++++++ .../Mvvm/Test_ObservableObjectAttribute.cs | 51 +++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/MemberDeclarationSyntaxExtensions.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 2f6ae7d087f..2b7908d9534 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -108,6 +108,16 @@ private void OnExecute( ClassDeclarationSyntax sourceDeclaration = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().First(); UsingDirectiveSyntax[] usingDirectives = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().ToArray(); + IEnumerable generatedMembers = FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration); + + // If the target class is sealed, make protected members private and remove the virtual modifier + if (classDeclarationSymbol.IsSealed) + { + generatedMembers = generatedMembers.Select(static member => member + .ReplaceModifier(SyntaxKind.ProtectedKeyword, SyntaxKind.PrivateKeyword) + .RemoveModifier(SyntaxKind.VirtualKeyword)); + } + // Create the class declaration for the user type. This will produce a tree as follows: // // : @@ -118,7 +128,7 @@ private void OnExecute( ClassDeclaration(classDeclaration.Identifier.Text) .WithModifiers(classDeclaration.Modifiers) .WithBaseList(sourceDeclaration.BaseList) - .AddMembers(FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration).ToArray()); + .AddMembers(generatedMembers.ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/MemberDeclarationSyntaxExtensions.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/MemberDeclarationSyntaxExtensions.cs new file mode 100644 index 00000000000..cdc63118306 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/MemberDeclarationSyntaxExtensions.cs @@ -0,0 +1,57 @@ +// 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.Diagnostics.Contracts; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions +{ + /// + /// Extension methods for the type. + /// + internal static class MemberDeclarationSyntaxExtensions + { + /// + /// Replaces a specific modifier. + /// + /// The input instance. + /// The target modifier kind to replace. + /// The new modifier kind to add or replace. + /// A instance with the target modifier. + [Pure] + public static MemberDeclarationSyntax ReplaceModifier(this MemberDeclarationSyntax memberDeclaration, SyntaxKind oldKind, SyntaxKind newKind) + { + int index = memberDeclaration.Modifiers.IndexOf(oldKind); + + if (index != -1) + { + return memberDeclaration.WithModifiers(memberDeclaration.Modifiers.Replace(memberDeclaration.Modifiers[index], Token(newKind))); + } + + return memberDeclaration; + } + + /// + /// Removes a specific modifier. + /// + /// The input instance. + /// The modifier kind to remove. + /// A instance without the specified modifier. + [Pure] + public static MemberDeclarationSyntax RemoveModifier(this MemberDeclarationSyntax memberDeclaration, SyntaxKind kind) + { + int index = memberDeclaration.Modifiers.IndexOf(kind); + + if (index != -1) + { + return memberDeclaration.WithModifiers(memberDeclaration.Modifiers.RemoveAt(index)); + } + + return memberDeclaration; + } + } +} diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs index dfb49ec6acc..6b3cdf0dbe3 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservableObjectAttribute.cs @@ -50,6 +50,45 @@ public void Test_ObservableObjectAttribute_Events() Assert.AreEqual(changed.Item2, 42); } + [TestCategory("Mvvm")] + [TestMethod] + public void Test_ObservableObjectAttribute_OnSealedClass_Events() + { + var model = new SampleModelSealed(); + + (PropertyChangingEventArgs, int) changing = default; + (PropertyChangedEventArgs, int) changed = default; + + model.PropertyChanging += (s, e) => + { + Assert.IsNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changing = (e, model.Data); + }; + + model.PropertyChanged += (s, e) => + { + Assert.IsNotNull(changing.Item1); + Assert.IsNull(changed.Item1); + Assert.AreSame(model, s); + Assert.IsNotNull(s); + Assert.IsNotNull(e); + + changed = (e, model.Data); + }; + + model.Data = 42; + + Assert.AreEqual(changing.Item1?.PropertyName, nameof(SampleModelSealed.Data)); + Assert.AreEqual(changing.Item2, 0); + Assert.AreEqual(changed.Item1?.PropertyName, nameof(SampleModelSealed.Data)); + Assert.AreEqual(changed.Item2, 42); + } + [ObservableObject] public partial class SampleModel { @@ -61,5 +100,17 @@ public int Data set => SetProperty(ref data, value); } } + + [ObservableObject] + public sealed partial class SampleModelSealed + { + private int data; + + public int Data + { + get => data; + set => SetProperty(ref data, value); + } + } } } From 0ed3e4d4dddc9a6ef1d380d2bff8768787b64a63 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 29 Mar 2021 16:50:14 +0200 Subject: [PATCH 52/89] Added [GeneratedCode] attribute to some generators, refactoring --- .../TransitiveMembersGenerator.cs | 80 ++- .../INotifyPropertyChanged.cs | 35 - .../EmbeddedResources/ObservableObject.cs | 618 ------------------ .../EmbeddedResources/ObservableRecipient.cs | 329 ---------- ...osoft.Toolkit.Mvvm.SourceGenerators.csproj | 4 +- 5 files changed, 72 insertions(+), 994 deletions(-) delete mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableObject.cs delete mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 2b7908d9534..8d3fa1e5176 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.CodeDom.Compiler; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; @@ -107,15 +108,19 @@ private void OnExecute( { ClassDeclarationSyntax sourceDeclaration = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().First(); UsingDirectiveSyntax[] usingDirectives = sourceSyntaxTree.GetRoot().DescendantNodes().OfType().ToArray(); + BaseListSyntax? baseListSyntax = BaseList(SeparatedList( + sourceDeclaration.BaseList?.Types + .OfType() + .Select(static t => t.Type) + .OfType() + .Where(static t => t.Identifier.ValueText.StartsWith("I")) + .Select(static t => SimpleBaseType(t)) + .ToArray() + ?? Array.Empty())); - IEnumerable generatedMembers = FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration); - - // If the target class is sealed, make protected members private and remove the virtual modifier - if (classDeclarationSymbol.IsSealed) + if (baseListSyntax.Types.Count == 0) { - generatedMembers = generatedMembers.Select(static member => member - .ReplaceModifier(SyntaxKind.ProtectedKeyword, SyntaxKind.PrivateKeyword) - .RemoveModifier(SyntaxKind.VirtualKeyword)); + baseListSyntax = null; } // Create the class declaration for the user type. This will produce a tree as follows: @@ -127,8 +132,8 @@ private void OnExecute( var classDeclarationSyntax = ClassDeclaration(classDeclaration.Identifier.Text) .WithModifiers(classDeclaration.Modifiers) - .WithBaseList(sourceDeclaration.BaseList) - .AddMembers(generatedMembers.ToArray()); + .WithBaseList(baseListSyntax) + .AddMembers(OnLoadDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -152,7 +157,7 @@ private void OnExecute( CompilationUnit() .AddMembers(NamespaceDeclaration(IdentifierName(namespaceName)) .AddMembers(typeDeclarationSyntax)) - .AddUsings(usingDirectives) + .AddUsings(usingDirectives.ToArray()) .NormalizeWhitespace() .ToFullString(); @@ -160,6 +165,61 @@ private void OnExecute( context.AddSource($"[{typeof(TAttribute).Name}]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); } + /// + /// Loads the nodes to generate from the input parsed tree. + /// + /// The input instance to use. + /// The for the current attribute being processed. + /// The node to process. + /// The for . + /// The parsed instance with the source nodes. + /// A sequence of nodes to emit in the generated file. + private IEnumerable OnLoadDeclaredMembers( + GeneratorExecutionContext context, + AttributeData attributeData, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + ClassDeclarationSyntax sourceDeclaration) + { + IEnumerable generatedMembers = FilterDeclaredMembers(context, attributeData, classDeclaration, classDeclarationSymbol, sourceDeclaration); + + // Add the attributes on each member + return generatedMembers.Select(member => + { + // [GeneratedCode] is always present + member = member + .WithoutLeadingTrivia() + .AddAttributeLists(AttributeList(SingletonSeparatedList( + Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString()))))))) + .WithLeadingTrivia(member.GetLeadingTrivia()); + + // [DebuggerNonUserCode] is not supported over interfaces, events or fields + if (member.Kind() is not SyntaxKind.InterfaceDeclaration and not SyntaxKind.EventFieldDeclaration and not SyntaxKind.FieldDeclaration) + { + member = member.AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode"))))); + } + + // [ExcludeFromCodeCoverage] is not supported on interfaces and fields + if (member.Kind() is not SyntaxKind.InterfaceDeclaration and not SyntaxKind.FieldDeclaration) + { + member = member.AddAttributeLists(AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))); + } + + // If the target class is sealed, make protected members private and remove the virtual modifier + if (classDeclarationSymbol.IsSealed) + { + return member + .ReplaceModifier(SyntaxKind.ProtectedKeyword, SyntaxKind.PrivateKeyword) + .RemoveModifier(SyntaxKind.VirtualKeyword); + } + + return member; + }); + } + /// /// Validates a target type being processed. /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs index 34f8f7b5f5f..72d14de5394 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/INotifyPropertyChanged.cs @@ -7,8 +7,6 @@ using System; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading.Tasks; @@ -20,15 +18,12 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel public abstract class NotifyPropertyChanged : INotifyPropertyChanged { /// - [ExcludeFromCodeCoverage] public event PropertyChangedEventHandler? PropertyChanged; /// /// Raises the event. /// /// The input instance. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { PropertyChanged?.Invoke(this, e); @@ -38,8 +33,6 @@ protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) /// Raises the event. /// /// (optional) The name of the property that changed. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) { OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); @@ -57,8 +50,6 @@ protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) /// /// The event is not raised if the current and new value for the target property are the same. /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(field, newValue)) @@ -84,8 +75,6 @@ protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string /// The instance to use to compare the input values. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null) { if (comparer.Equals(field, newValue)) @@ -121,8 +110,6 @@ protected bool SetProperty(ref T field, T newValue, IEqualityComparer comp /// /// The event is not raised if the current and new value for the target property are the same. /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected bool SetProperty(T oldValue, T newValue, Action callback, [CallerMemberName] string? propertyName = null) { if (EqualityComparer.Default.Equals(oldValue, newValue)) @@ -149,8 +136,6 @@ protected bool SetProperty(T oldValue, T newValue, Action callback, [Calle /// A callback to invoke to update the property value. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, [CallerMemberName] string? propertyName = null) { if (comparer.Equals(oldValue, newValue)) @@ -217,8 +202,6 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer compa /// /// The event is not raised if the current and new value for the target property are the same. /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected bool SetProperty(T oldValue, T newValue, TModel model, Action callback, [CallerMemberName] string? propertyName = null) where TModel : class { @@ -250,8 +233,6 @@ protected bool SetProperty(T oldValue, T newValue, TModel model, Acti /// The callback to invoke to set the target property value, if a change has occurred. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, [CallerMemberName] string? propertyName = null) where TModel : class { @@ -299,8 +280,6 @@ protected bool SetProperty(T oldValue, T newValue, IEqualityComparer< /// is different than the previous one, and it does not mean the new /// instance passed as argument is in any particular state. /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); @@ -321,8 +300,6 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, /// /// The event is not raised if the current and new value for the target property are the same. /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); @@ -361,8 +338,6 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, /// is different than the previous one, and it does not mean the new /// instance passed as argument is in any particular state. /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); @@ -384,8 +359,6 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNoti /// /// The event is not raised if the current and new value for the target property are the same. /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null) { return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); @@ -400,8 +373,6 @@ protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNoti /// A callback to invoke to update the property value. /// (optional) The name of the property that changed. /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] private bool SetPropertyAndNotifyOnCompletion(ITaskNotifier taskNotifier, TTask? newValue, Action callback, [CallerMemberName] string? propertyName = null) where TTask : Task { @@ -456,16 +427,12 @@ private interface ITaskNotifier /// /// Gets or sets the wrapped value. /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] TTask? Task { get; set; } } /// /// A wrapping class that can hold a value. /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected sealed class TaskNotifier : ITaskNotifier { /// @@ -498,8 +465,6 @@ internal TaskNotifier() /// A wrapping class that can hold a value. /// /// The type of value for the wrapped instance. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] protected sealed class TaskNotifier : ITaskNotifier> { /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableObject.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableObject.cs deleted file mode 100644 index 526fd7f3702..00000000000 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableObject.cs +++ /dev/null @@ -1,618 +0,0 @@ -// 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. - -#pragma warning disable - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -namespace Microsoft.Toolkit.Mvvm.ComponentModel -{ - /// - /// A base class for objects of which the properties must be observable. - /// - public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging - { - /// - [ExcludeFromCodeCoverage] - public event PropertyChangedEventHandler? PropertyChanged; - - /// - [ExcludeFromCodeCoverage] - public event PropertyChangingEventHandler? PropertyChanging; - - /// - /// Raises the event. - /// - /// The input instance. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) - { - PropertyChanged?.Invoke(this, e); - } - - /// - /// Raises the event. - /// - /// The input instance. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected virtual void OnPropertyChanging(PropertyChangingEventArgs e) - { - PropertyChanging?.Invoke(this, e); - } - - /// - /// Raises the event. - /// - /// (optional) The name of the property that changed. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) - { - OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); - } - - /// - /// Raises the event. - /// - /// (optional) The name of the property that changed. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected void OnPropertyChanging([CallerMemberName] string? propertyName = null) - { - OnPropertyChanging(new PropertyChangingEventArgs(propertyName)); - } - - /// - /// Compares the current and new values for a given property. If the value has changed, - /// raises the event, updates the property with the new - /// value, then raises the event. - /// - /// The type of the property that changed. - /// The field storing the property's value. - /// The property's value after the change occurred. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - /// - /// The and events are not raised - /// if the current and new value for the target property are the same. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(ref T field, T newValue, [CallerMemberName] string? propertyName = null) - { - // We duplicate the code here instead of calling the overload because we can't - // guarantee that the invoked SetProperty will be inlined, and we need the JIT - // to be able to see the full EqualityComparer.Default.Equals call, so that - // it'll use the intrinsics version of it and just replace the whole invocation - // with a direct comparison when possible (eg. for primitive numeric types). - // This is the fastest SetProperty overload so we particularly care about - // the codegen quality here, and the code is small and simple enough so that - // duplicating it still doesn't make the whole class harder to maintain. - if (EqualityComparer.Default.Equals(field, newValue)) - { - return false; - } - - OnPropertyChanging(propertyName); - - field = newValue; - - OnPropertyChanged(propertyName); - - return true; - } - - /// - /// Compares the current and new values for a given property. If the value has changed, - /// raises the event, updates the property with the new - /// value, then raises the event. - /// See additional notes about this overload in . - /// - /// The type of the property that changed. - /// The field storing the property's value. - /// The property's value after the change occurred. - /// The instance to use to compare the input values. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, [CallerMemberName] string? propertyName = null) - { - if (comparer.Equals(field, newValue)) - { - return false; - } - - OnPropertyChanging(propertyName); - - field = newValue; - - OnPropertyChanged(propertyName); - - return true; - } - - /// - /// Compares the current and new values for a given property. If the value has changed, - /// raises the event, updates the property with the new - /// value, then raises the event. - /// This overload is much less efficient than and it - /// should only be used when the former is not viable (eg. when the target property being - /// updated does not directly expose a backing field that can be passed by reference). - /// For performance reasons, it is recommended to use a stateful callback if possible through - /// the whenever possible - /// instead of this overload, as that will allow the C# compiler to cache the input callback and - /// reduce the memory allocations. More info on that overload are available in the related XML - /// docs. This overload is here for completeness and in cases where that is not applicable. - /// - /// The type of the property that changed. - /// The current property value. - /// The property's value after the change occurred. - /// A callback to invoke to update the property value. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - /// - /// The and events are not raised - /// if the current and new value for the target property are the same. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(T oldValue, T newValue, Action callback, [CallerMemberName] string? propertyName = null) - { - // We avoid calling the overload again to ensure the comparison is inlined - if (EqualityComparer.Default.Equals(oldValue, newValue)) - { - return false; - } - - OnPropertyChanging(propertyName); - - callback(newValue); - - OnPropertyChanged(propertyName); - - return true; - } - - /// - /// Compares the current and new values for a given property. If the value has changed, - /// raises the event, updates the property with the new - /// value, then raises the event. - /// See additional notes about this overload in . - /// - /// The type of the property that changed. - /// The current property value. - /// The property's value after the change occurred. - /// The instance to use to compare the input values. - /// A callback to invoke to update the property value. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, [CallerMemberName] string? propertyName = null) - { - if (comparer.Equals(oldValue, newValue)) - { - return false; - } - - OnPropertyChanging(propertyName); - - callback(newValue); - - OnPropertyChanged(propertyName); - - return true; - } - - /// - /// Compares the current and new values for a given nested property. If the value has changed, - /// raises the event, updates the property and then raises the - /// event. The behavior mirrors that of , - /// with the difference being that this method is used to relay properties from a wrapped model in the - /// current instance. This type is useful when creating wrapping, bindable objects that operate over - /// models that lack support for notification (eg. for CRUD operations). - /// Suppose we have this model (eg. for a database row in a table): - /// - /// public class Person - /// { - /// public string Name { get; set; } - /// } - /// - /// We can then use a property to wrap instances of this type into our observable model (which supports - /// notifications), injecting the notification to the properties of that model, like so: - /// - /// public class BindablePerson : ObservableObject - /// { - /// public Model { get; } - /// - /// public BindablePerson(Person model) - /// { - /// Model = model; - /// } - /// - /// public string Name - /// { - /// get => Model.Name; - /// set => Set(Model.Name, value, Model, (model, name) => model.Name = name); - /// } - /// } - /// - /// This way we can then use the wrapping object in our application, and all those "proxy" properties will - /// also raise notifications when changed. Note that this method is not meant to be a replacement for - /// , and it should only be used when relaying properties to a model that - /// doesn't support notifications, and only if you can't implement notifications to that model directly (eg. by having - /// it inherit from ). The syntax relies on passing the target model and a stateless callback - /// to allow the C# compiler to cache the function, which results in much better performance and no memory usage. - /// - /// The type of model whose property (or field) to set. - /// The type of property (or field) to set. - /// The current property value. - /// The property's value after the change occurred. - /// The model containing the property being updated. - /// The callback to invoke to set the target property value, if a change has occurred. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - /// - /// The and events are not - /// raised if the current and new value for the target property are the same. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(T oldValue, T newValue, TModel model, Action callback, [CallerMemberName] string? propertyName = null) - where TModel : class - { - if (EqualityComparer.Default.Equals(oldValue, newValue)) - { - return false; - } - - OnPropertyChanging(propertyName); - - callback(model, newValue); - - OnPropertyChanged(propertyName); - - return true; - } - - /// - /// Compares the current and new values for a given nested property. If the value has changed, - /// raises the event, updates the property and then raises the - /// event. The behavior mirrors that of , - /// with the difference being that this method is used to relay properties from a wrapped model in the - /// current instance. See additional notes about this overload in . - /// - /// The type of model whose property (or field) to set. - /// The type of property (or field) to set. - /// The current property value. - /// The property's value after the change occurred. - /// The instance to use to compare the input values. - /// The model containing the property being updated. - /// The callback to invoke to set the target property value, if a change has occurred. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, [CallerMemberName] string? propertyName = null) - where TModel : class - { - if (comparer.Equals(oldValue, newValue)) - { - return false; - } - - OnPropertyChanging(propertyName); - - callback(model, newValue); - - OnPropertyChanged(propertyName); - - return true; - } - - /// - /// Compares the current and new values for a given field (which should be the backing - /// field for a property). If the value has changed, raises the - /// event, updates the field and then raises the event. - /// The behavior mirrors that of , with the difference being that - /// this method will also monitor the new value of the property (a generic ) and will also - /// raise the again for the target property when it completes. - /// This can be used to update bindings observing that or any of its properties. - /// This method and its overload specifically rely on the type, which needs - /// to be used in the backing field for the target property. The field doesn't need to be - /// initialized, as this method will take care of doing that automatically. The - /// type also includes an implicit operator, so it can be assigned to any instance directly. - /// Here is a sample property declaration using this method: - /// - /// private TaskNotifier myTask; - /// - /// public Task MyTask - /// { - /// get => myTask; - /// private set => SetAndNotifyOnCompletion(ref myTask, value); - /// } - /// - /// - /// The field notifier to modify. - /// The property's value after the change occurred. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - /// - /// The and events are not raised if the current - /// and new value for the target property are the same. The return value being only - /// indicates that the new value being assigned to is different than the previous one, - /// and it does not mean the new instance passed as argument is in any particular state. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) - { - // We invoke the overload with a callback here to avoid code duplication, and simply pass an empty callback. - // The lambda expression here is transformed by the C# compiler into an empty closure class with a - // static singleton field containing a closure instance, and another caching the instantiated Action - // instance. This will result in no further allocations after the first time this method is called for a given - // generic type. We only pay the cost of the virtual call to the delegate, but this is not performance critical - // code and that overhead would still be much lower than the rest of the method anyway, so that's fine. - return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); - } - - /// - /// Compares the current and new values for a given field (which should be the backing - /// field for a property). If the value has changed, raises the - /// event, updates the field and then raises the event. - /// This method is just like , - /// with the difference being an extra parameter with a callback being invoked - /// either immediately, if the new task has already completed or is , or upon completion. - /// - /// The field notifier to modify. - /// The property's value after the change occurred. - /// A callback to invoke to update the property value. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - /// - /// The and events are not raised - /// if the current and new value for the target property are the same. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action callback, [CallerMemberName] string? propertyName = null) - { - return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); - } - - /// - /// Compares the current and new values for a given field (which should be the backing - /// field for a property). If the value has changed, raises the - /// event, updates the field and then raises the event. - /// The behavior mirrors that of , with the difference being that - /// this method will also monitor the new value of the property (a generic ) and will also - /// raise the again for the target property when it completes. - /// This can be used to update bindings observing that or any of its properties. - /// This method and its overload specifically rely on the type, which needs - /// to be used in the backing field for the target property. The field doesn't need to be - /// initialized, as this method will take care of doing that automatically. The - /// type also includes an implicit operator, so it can be assigned to any instance directly. - /// Here is a sample property declaration using this method: - /// - /// private TaskNotifier<int> myTask; - /// - /// public Task<int> MyTask - /// { - /// get => myTask; - /// private set => SetAndNotifyOnCompletion(ref myTask, value); - /// } - /// - /// - /// The type of result for the to set and monitor. - /// The field notifier to modify. - /// The property's value after the change occurred. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - /// - /// The and events are not raised if the current - /// and new value for the target property are the same. The return value being only - /// indicates that the new value being assigned to is different than the previous one, - /// and it does not mean the new instance passed as argument is in any particular state. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, [CallerMemberName] string? propertyName = null) - { - return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, static _ => { }, propertyName); - } - - /// - /// Compares the current and new values for a given field (which should be the backing - /// field for a property). If the value has changed, raises the - /// event, updates the field and then raises the event. - /// This method is just like , - /// with the difference being an extra parameter with a callback being invoked - /// either immediately, if the new task has already completed or is , or upon completion. - /// - /// The type of result for the to set and monitor. - /// The field notifier to modify. - /// The property's value after the change occurred. - /// A callback to invoke to update the property value. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - /// - /// The and events are not raised - /// if the current and new value for the target property are the same. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetPropertyAndNotifyOnCompletion(ref TaskNotifier? taskNotifier, Task? newValue, Action?> callback, [CallerMemberName] string? propertyName = null) - { - return SetPropertyAndNotifyOnCompletion(taskNotifier ??= new(), newValue, callback, propertyName); - } - - /// - /// Implements the notification logic for the related methods. - /// - /// The type of to set and monitor. - /// The field notifier. - /// The property's value after the change occurred. - /// A callback to invoke to update the property value. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - private bool SetPropertyAndNotifyOnCompletion(ITaskNotifier taskNotifier, TTask? newValue, Action callback, [CallerMemberName] string? propertyName = null) - where TTask : Task - { - if (ReferenceEquals(taskNotifier.Task, newValue)) - { - return false; - } - - // Check the status of the new task before assigning it to the - // target field. This is so that in case the task is either - // null or already completed, we can avoid the overhead of - // scheduling the method to monitor its completion. - bool isAlreadyCompletedOrNull = newValue?.IsCompleted ?? true; - - OnPropertyChanging(propertyName); - - taskNotifier.Task = newValue; - - OnPropertyChanged(propertyName); - - // If the input task is either null or already completed, we don't need to - // execute the additional logic to monitor its completion, so we can just bypass - // the rest of the method and return that the field changed here. The return value - // does not indicate that the task itself has completed, but just that the property - // value itself has changed (ie. the referenced task instance has changed). - // This mirrors the return value of all the other synchronous Set methods as well. - if (isAlreadyCompletedOrNull) - { - callback(newValue); - - return true; - } - - // We use a local async function here so that the main method can - // remain synchronous and return a value that can be immediately - // used by the caller. This mirrors Set(ref T, T, string). - // We use an async void function instead of a Task-returning function - // so that if a binding update caused by the property change notification - // causes a crash, it is immediately reported in the application instead of - // the exception being ignored (as the returned task wouldn't be awaited), - // which would result in a confusing behavior for users. - async void MonitorTask() - { - try - { - // Await the task and ignore any exceptions - await newValue!; - } - catch - { - } - - // Only notify if the property hasn't changed - if (ReferenceEquals(taskNotifier.Task, newValue)) - { - OnPropertyChanged(propertyName); - } - - callback(newValue); - } - - MonitorTask(); - - return true; - } - - /// - /// An interface for task notifiers of a specified type. - /// - /// The type of value to store. - private interface ITaskNotifier - where TTask : Task - { - /// - /// Gets or sets the wrapped value. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - TTask? Task { get; set; } - } - - /// - /// A wrapping class that can hold a value. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected sealed class TaskNotifier : ITaskNotifier - { - /// - /// Initializes a new instance of the class. - /// - internal TaskNotifier() - { - } - - private Task? task; - - /// - Task? ITaskNotifier.Task - { - get => this.task; - set => this.task = value; - } - - /// - /// Unwraps the value stored in the current instance. - /// - /// The input instance. - public static implicit operator Task?(TaskNotifier? notifier) - { - return notifier?.task; - } - } - - /// - /// A wrapping class that can hold a value. - /// - /// The type of value for the wrapped instance. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected sealed class TaskNotifier : ITaskNotifier> - { - /// - /// Initializes a new instance of the class. - /// - internal TaskNotifier() - { - } - - private Task? task; - - /// - Task? ITaskNotifier>.Task - { - get => this.task; - set => this.task = value; - } - - /// - /// Unwraps the value stored in the current instance. - /// - /// The input instance. - public static implicit operator Task?(TaskNotifier? notifier) - { - return notifier?.task; - } - } - } -} \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs deleted file mode 100644 index 7398d933118..00000000000 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/EmbeddedResources/ObservableRecipient.cs +++ /dev/null @@ -1,329 +0,0 @@ -// 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. - -#pragma warning disable - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; -using Microsoft.Toolkit.Mvvm.Messaging; -using Microsoft.Toolkit.Mvvm.Messaging.Messages; - -namespace Microsoft.Toolkit.Mvvm.ComponentModel -{ - /// - /// A base class for observable objects that also acts as recipients for messages. This class is an extension of - /// which also provides built-in support to use the type. - /// - public abstract class ObservableRecipient - { - /// - /// Initializes a new instance of the class. - /// - /// - /// This constructor will produce an instance that will use the instance - /// to perform requested operations. It will also be available locally through the property. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected ObservableRecipient() - : this(WeakReferenceMessenger.Default) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The instance to use to send messages. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected ObservableRecipient(IMessenger messenger) - { - Messenger = messenger; - } - - /// - /// Gets the instance in use. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected IMessenger Messenger { get; } - - private bool isActive; - - /// - /// Gets or sets a value indicating whether the current view model is currently active. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - public bool IsActive - { - get => this.isActive; - set - { - if (SetProperty(ref this.isActive, value, true)) - { - if (value) - { - OnActivated(); - } - else - { - OnDeactivated(); - } - } - } - } - - /// - /// Raised whenever the property is set to . - /// 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. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected virtual void OnActivated() - { - Messenger.RegisterAll(this); - } - - /// - /// Raised whenever the property is set to . - /// Use this method to unregister from messages and do general cleanup for this instance. - /// - /// - /// The base implementation unregisters all messages for this recipient. It does so by - /// invoking , which removes all registered - /// handlers for a given subscriber, regardless of what token was used to register them. - /// That is, all registered handlers across all subscription channels will be removed. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected virtual void OnDeactivated() - { - Messenger.UnregisterAll(this); - } - - /// - /// Broadcasts a with the specified - /// parameters, without using any particular token (so using the default channel). - /// - /// The type of the property that changed. - /// The value of the property before it changed. - /// The value of the property after it changed. - /// The name of the property that changed. - /// - /// You should override this method if you wish to customize the channel being - /// used to send the message (eg. if you need to use a specific token for the channel). - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected virtual void Broadcast(T oldValue, T newValue, string? propertyName) - { - PropertyChangedMessage message = new(this, propertyName, oldValue, newValue); - - Messenger.Send(message); - } - - /// - /// Compares the current and new values for a given property. If the value has changed, - /// raises the event, updates the property with - /// the new value, then raises the event. - /// - /// The type of the property that changed. - /// The field storing the property's value. - /// The property's value after the change occurred. - /// If , will also be invoked. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - /// - /// This method is just like , just with the addition - /// of the parameter. As such, following the behavior of the base method, - /// the and events - /// are not raised if the current and new value for the target property are the same. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null) - { - T oldValue = field; - - // We duplicate the code as in the base class here to leverage - // the intrinsics support for EqualityComparer.Default.Equals. - bool propertyChanged = SetProperty(ref field, newValue, propertyName); - - if (propertyChanged && broadcast) - { - Broadcast(oldValue, newValue, propertyName); - } - - return propertyChanged; - } - - /// - /// Compares the current and new values for a given property. If the value has changed, - /// raises the event, updates the property with - /// the new value, then raises the event. - /// See additional notes about this overload in . - /// - /// The type of the property that changed. - /// The field storing the property's value. - /// The property's value after the change occurred. - /// The instance to use to compare the input values. - /// If , will also be invoked. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(ref T field, T newValue, IEqualityComparer comparer, bool broadcast, [CallerMemberName] string? propertyName = null) - { - T oldValue = field; - - bool propertyChanged = SetProperty(ref field, newValue, comparer, propertyName); - - if (propertyChanged && broadcast) - { - Broadcast(oldValue, newValue, propertyName); - } - - return propertyChanged; - } - - /// - /// Compares the current and new values for a given property. If the value has changed, - /// raises the event, updates the property with - /// the new value, then raises the event. Similarly to - /// the method, this overload should only be - /// used when can't be used directly. - /// - /// The type of the property that changed. - /// The current property value. - /// The property's value after the change occurred. - /// A callback to invoke to update the property value. - /// If , will also be invoked. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - /// - /// This method is just like , just with the addition - /// of the parameter. As such, following the behavior of the base method, - /// the and events - /// are not raised if the current and new value for the target property are the same. - /// - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(T oldValue, T newValue, Action callback, bool broadcast, [CallerMemberName] string? propertyName = null) - { - bool propertyChanged = SetProperty(oldValue, newValue, callback, propertyName); - - if (propertyChanged && broadcast) - { - Broadcast(oldValue, newValue, propertyName); - } - - return propertyChanged; - } - - /// - /// Compares the current and new values for a given property. If the value has changed, - /// raises the event, updates the property with - /// the new value, then raises the event. - /// See additional notes about this overload in . - /// - /// The type of the property that changed. - /// The current property value. - /// The property's value after the change occurred. - /// The instance to use to compare the input values. - /// A callback to invoke to update the property value. - /// If , will also be invoked. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, Action callback, bool broadcast, [CallerMemberName] string? propertyName = null) - { - bool propertyChanged = SetProperty(oldValue, newValue, comparer, callback, propertyName); - - if (propertyChanged && broadcast) - { - Broadcast(oldValue, newValue, propertyName); - } - - return propertyChanged; - } - - /// - /// Compares the current and new values for a given nested property. If the value has changed, - /// raises the event, updates the property and then raises the - /// event. The behavior mirrors that of - /// , with the difference being that this - /// method is used to relay properties from a wrapped model in the current instance. For more info, see the docs for - /// . - /// - /// The type of model whose property (or field) to set. - /// The type of property (or field) to set. - /// The current property value. - /// The property's value after the change occurred. - /// The model - /// The callback to invoke to set the target property value, if a change has occurred. - /// If , will also be invoked. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(T oldValue, T newValue, TModel model, Action callback, bool broadcast, [CallerMemberName] string? propertyName = null) - where TModel : class - { - bool propertyChanged = SetProperty(oldValue, newValue, model, callback, propertyName); - - if (propertyChanged && broadcast) - { - Broadcast(oldValue, newValue, propertyName); - } - - return propertyChanged; - } - - /// - /// Compares the current and new values for a given nested property. If the value has changed, - /// raises the event, updates the property and then raises the - /// event. The behavior mirrors that of - /// , - /// with the difference being that this method is used to relay properties from a wrapped model in the - /// current instance. For more info, see the docs for - /// . - /// - /// The type of model whose property (or field) to set. - /// The type of property (or field) to set. - /// The current property value. - /// The property's value after the change occurred. - /// The instance to use to compare the input values. - /// The model - /// The callback to invoke to set the target property value, if a change has occurred. - /// If , will also be invoked. - /// (optional) The name of the property that changed. - /// if the property was changed, otherwise. - [DebuggerNonUserCode] - [ExcludeFromCodeCoverage] - protected bool SetProperty(T oldValue, T newValue, IEqualityComparer comparer, TModel model, Action callback, bool broadcast, [CallerMemberName] string? propertyName = null) - where TModel : class - { - bool propertyChanged = SetProperty(oldValue, newValue, comparer, model, callback, propertyName); - - if (propertyChanged && broadcast) - { - Broadcast(oldValue, newValue, propertyName); - } - - return propertyChanged; - } - } -} \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index be164dc4a3a..8118b09f328 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -24,10 +24,10 @@ PreserveNewest - + PreserveNewest - + PreserveNewest From 79b36fb0c3bca1ce9e8ec84e20c6897c86aead4a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 29 Mar 2021 17:24:05 +0200 Subject: [PATCH 53/89] Switched to global:: paths for other generated files --- .../ObservablePropertyGenerator.cs | 39 ++++++------ ...ValidatorValidateAllPropertiesGenerator.cs | 49 +++++++-------- .../IMessengerRegisterAllGenerator.cs | 63 +++++++++---------- 3 files changed, 74 insertions(+), 77 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index b216d22d473..72816900eb4 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -56,7 +56,7 @@ public void Execute(GeneratorExecutionContext context) /// The node to process. /// The for . /// The sequence of fields to process. - private static void OnExecute( + private void OnExecute( GeneratorExecutionContext context, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, @@ -71,7 +71,7 @@ private static void OnExecute( var classDeclarationSyntax = ClassDeclaration(classDeclarationSymbol.Name) .WithModifiers(classDeclaration.Modifiers) - .AddMembers(items.Select(static item => CreatePropertyDeclaration(item.LeadingTrivia, item.FieldSymbol)).ToArray()); + .AddMembers(items.Select(item => CreatePropertyDeclaration(item.LeadingTrivia, item.FieldSymbol)).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -92,15 +92,12 @@ private static void OnExecute( // Create the final compilation unit to generate (with leading trivia) var source = - CompilationUnit().AddUsings( - UsingDirective(IdentifierName("System.Collections.Generic")).WithLeadingTrivia(TriviaList( - Comment("// Licensed to the .NET Foundation under one or more agreements."), - Comment("// The .NET Foundation licenses this file to you under the MIT license."), - Comment("// See the LICENSE file in the project root for more information."), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))), - UsingDirective(IdentifierName("System.Diagnostics")), - UsingDirective(IdentifierName("System.Diagnostics.CodeAnalysis"))).AddMembers( - NamespaceDeclaration(IdentifierName(namespaceName)) + CompilationUnit().AddMembers( + NamespaceDeclaration(IdentifierName(namespaceName)).WithLeadingTrivia(TriviaList( + Comment("// Licensed to the .NET Foundation under one or more agreements."), + Comment("// The .NET Foundation licenses this file to you under the MIT license."), + Comment("// See the LICENSE file in the project root for more information."), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))) .AddMembers(typeDeclarationSyntax)) .NormalizeWhitespace() .ToFullString(); @@ -116,7 +113,7 @@ private static void OnExecute( /// The input instance to process. /// A generated instance for the input field. [Pure] - private static PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol) + private PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol) { // Get the field type and the target property name string @@ -154,14 +151,15 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaL // Construct the generated property as follows: // // - // [DebuggerNonUserCode] - // [ExcludeFromCodeCoverage] + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // [global::System.Diagnostics.DebuggerNonUserCode] + // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // public // { // get => ; // set // { - // if (!EqualityComparer<>.Default.Equals(, value)) + // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(, value)) // { // OnPropertyChanging(); // Optional // = value; @@ -172,8 +170,13 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaL return PropertyDeclaration(IdentifierName(typeName), Identifier(propertyName)) .AddAttributeLists( - AttributeList(SingletonSeparatedList(Attribute(IdentifierName("DebuggerNonUserCode")))), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ExcludeFromCodeCoverage"))))) + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .WithLeadingTrivia(leadingTrivia) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( @@ -190,7 +193,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaL SyntaxKind.SimpleMemberAccessExpression, MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, - GenericName(Identifier("EqualityComparer")) + GenericName(Identifier("global::System.Collections.Generic.EqualityComparer")) .AddTypeArgumentListArguments(IdentifierName(typeName)), IdentifierName("Default")), IdentifierName("Equals"))) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs index 894544d5a69..7c9f625770f 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -45,13 +45,18 @@ public void Execute(GeneratorExecutionContext context) // Prepare the attributes to add to the first class declaration AttributeListSyntax[] classAttributes = new[] { - AttributeList(SingletonSeparatedList(Attribute(IdentifierName("DebuggerNonUserCode")))), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ExcludeFromCodeCoverage")))), AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( - AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), + Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))), AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("Obsolete")).AddArgumentListArguments( + Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( AttributeArgument(LiteralExpression( SyntaxKind.StringLiteralExpression, Literal("This type is not intended to be used directly by user code")))))) @@ -68,21 +73,17 @@ public void Execute(GeneratorExecutionContext context) // // #pragma warning disable // - // using System; - // using System.ComponentModel; - // using System.Diagnostics; - // using System.Diagnostics.CodeAnalysis; - // // namespace Microsoft.Toolkit.Mvvm.ComponentModel.__Internals // { - // [DebuggerNonUserCode] - // [ExcludeFromCodeCoverage] - // [EditorBrowsable(EditorBrowsableState.Never)] - // [Obsolete("This type is not intended to be used directly by user code")] + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // [global::System.Diagnostics.DebuggerNonUserCode] + // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + // [global::System.Obsolete("This type is not intended to be used directly by user code")] // internal static partial class __ObservableValidatorExtensions // { - // [EditorBrowsable(EditorBrowsableState.Never)] - // [Obsolete("This method is not intended to be called directly by user code")] + // [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 void ValidateAllProperties( instance) // { // @@ -90,16 +91,12 @@ public void Execute(GeneratorExecutionContext context) // } // } var source = - CompilationUnit().AddUsings( - UsingDirective(IdentifierName("System")).WithLeadingTrivia(TriviaList( + CompilationUnit().AddMembers( + NamespaceDeclaration(IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList( Comment("// Licensed to the .NET Foundation under one or more agreements."), Comment("// The .NET Foundation licenses this file to you under the MIT license."), Comment("// See the LICENSE file in the project root for more information."), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))), - UsingDirective(IdentifierName("System.ComponentModel")), - UsingDirective(IdentifierName("System.Diagnostics")), - UsingDirective(IdentifierName("System.Diagnostics.CodeAnalysis"))).AddMembers( - NamespaceDeclaration(IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals")).AddMembers( + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers( ClassDeclaration("__ObservableValidatorExtensions").AddModifiers( Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword), @@ -108,10 +105,10 @@ public void Execute(GeneratorExecutionContext context) PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier("ValidateAllProperties")).AddAttributeLists( AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( - AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), + Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("Obsolete")).AddArgumentListArguments( + Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( AttributeArgument(LiteralExpression( SyntaxKind.StringLiteralExpression, Literal("This method is not intended to be called directly by user code"))))))).AddModifiers( diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index 7c8336cf23b..d805896b413 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -44,13 +44,18 @@ public void Execute(GeneratorExecutionContext context) // Prepare the attributes to add to the first class declaration AttributeListSyntax[] classAttributes = new[] { - AttributeList(SingletonSeparatedList(Attribute(IdentifierName("DebuggerNonUserCode")))), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName("ExcludeFromCodeCoverage")))), AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( - AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), + Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))), AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("Obsolete")).AddArgumentListArguments( + Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( AttributeArgument(LiteralExpression( SyntaxKind.StringLiteralExpression, Literal("This type is not intended to be used directly by user code")))))) @@ -70,46 +75,38 @@ public void Execute(GeneratorExecutionContext context) // // #pragma warning disable // - // using System; - // using System.ComponentModel; - // using System.Diagnostics; - // using System.Diagnostics.CodeAnalysis; - // // namespace Microsoft.Toolkit.Mvvm.Messaging.__Internals // { - // [DebuggerNonUserCode] - // [ExcludeFromCodeCoverage] - // [EditorBrowsable(EditorBrowsableState.Never)] - // [Obsolete("This type is not intended to be used directly by user code")] + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // [global::System.Diagnostics.DebuggerNonUserCode] + // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + // [global::System.Obsolete("This type is not intended to be used directly by user code")] // internal static partial class __IMessengerExtensions // { - // [EditorBrowsable(EditorBrowsableState.Never)] - // [Obsolete("This method is not intended to be called directly by user code")] + // [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 void RegisterAll(IMessenger messenger, recipient) // { // // } // - // [EditorBrowsable(EditorBrowsableState.Never)] - // [Obsolete("This method is not intended to be called directly by user code")] + // [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 void RegisterAll(IMessenger messenger, recipient, TToken token) - // where TToken : IEquatable + // where TToken : global::System.IEquatable // { // // } // } // } var source = - CompilationUnit().AddUsings( - UsingDirective(IdentifierName("System")).WithLeadingTrivia(TriviaList( + CompilationUnit().AddMembers( + NamespaceDeclaration(IdentifierName("Microsoft.Toolkit.Mvvm.Messaging.__Internals")).WithLeadingTrivia(TriviaList( Comment("// Licensed to the .NET Foundation under one or more agreements."), Comment("// The .NET Foundation licenses this file to you under the MIT license."), Comment("// See the LICENSE file in the project root for more information."), - Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))), - UsingDirective(IdentifierName("System.ComponentModel")), - UsingDirective(IdentifierName("System.Diagnostics")), - UsingDirective(IdentifierName("System.Diagnostics.CodeAnalysis"))).AddMembers( - NamespaceDeclaration(IdentifierName("Microsoft.Toolkit.Mvvm.Messaging.__Internals")).AddMembers( + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers( ClassDeclaration("__IMessengerExtensions").AddModifiers( Token(SyntaxKind.InternalKeyword), Token(SyntaxKind.StaticKeyword), @@ -118,10 +115,10 @@ public void Execute(GeneratorExecutionContext context) PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier("RegisterAll")).AddAttributeLists( AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( - AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), + Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("Obsolete")).AddArgumentListArguments( + Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( AttributeArgument(LiteralExpression( SyntaxKind.StringLiteralExpression, Literal("This method is not intended to be called directly by user code"))))))).AddModifiers( @@ -134,10 +131,10 @@ public void Execute(GeneratorExecutionContext context) PredefinedType(Token(SyntaxKind.VoidKeyword)), Identifier("RegisterAll")).AddAttributeLists( AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("EditorBrowsable")).AddArgumentListArguments( - AttributeArgument(ParseExpression("EditorBrowsableState.Never"))))), + Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("Obsolete")).AddArgumentListArguments( + Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( AttributeArgument(LiteralExpression( SyntaxKind.StringLiteralExpression, Literal("This method is not intended to be called directly by user code"))))))).AddModifiers( @@ -149,7 +146,7 @@ public void Execute(GeneratorExecutionContext context) .AddTypeParameterListParameters(TypeParameter("TToken")) .AddConstraintClauses( TypeParameterConstraintClause("TToken") - .AddConstraints(TypeConstraint(GenericName("IEquatable").AddTypeArgumentListArguments(IdentifierName("TToken"))))) + .AddConstraints(TypeConstraint(GenericName("global::System.IEquatable").AddTypeArgumentListArguments(IdentifierName("TToken"))))) .WithBody(Block(EnumerateRegistrationStatementsWithTokens(classSymbol, iRecipientSymbol).ToArray()))))) .NormalizeWhitespace() .ToFullString(); From 263d406e23a43f59f7c3dd1947abaa36715496ab Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 29 Mar 2021 17:26:42 +0200 Subject: [PATCH 54/89] Fixed leading trivia in some generated files --- .../ComponentModel/TransitiveMembersGenerator.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 8d3fa1e5176..d0a464b7ec4 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -157,7 +157,12 @@ private void OnExecute( CompilationUnit() .AddMembers(NamespaceDeclaration(IdentifierName(namespaceName)) .AddMembers(typeDeclarationSyntax)) - .AddUsings(usingDirectives.ToArray()) + .AddUsings(usingDirectives.First().WithLeadingTrivia(TriviaList( + Comment("// Licensed to the .NET Foundation under one or more agreements."), + Comment("// The .NET Foundation licenses this file to you under the MIT license."), + Comment("// See the LICENSE file in the project root for more information."), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true))))) + .AddUsings(usingDirectives.Skip(1).ToArray()) .NormalizeWhitespace() .ToFullString(); From 62f883dcab672e6b71efea7db57ec7bf1c4aaae8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 29 Mar 2021 17:50:22 +0200 Subject: [PATCH 55/89] Added handling for optional INotifyPropertyChanging in properties --- .../ObservablePropertyGenerator.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 72816900eb4..02cc2921c35 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.Contracts; using System.Linq; using System.Text; @@ -62,6 +63,17 @@ private void OnExecute( INamedTypeSymbol classDeclarationSymbol, IEnumerable items) { + // Check whether INotifyPropertyChanging is present as well + INamedTypeSymbol + iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanging).FullName)!, + observableObjectSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject")!, + observableObjectAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(ObservableObjectAttribute).FullName)!; + + bool isNotifyPropertyChanging = + classDeclarationSymbol.AllInterfaces.Contains(iNotifyPropertyChangingSymbol, SymbolEqualityComparer.Default) || + classDeclarationSymbol.InheritsFrom(observableObjectSymbol) || + classDeclarationSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observableObjectAttributeSymbol)); + // Create the class declaration for the user type. This will produce a tree as follows: // // @@ -71,7 +83,7 @@ private void OnExecute( var classDeclarationSyntax = ClassDeclaration(classDeclarationSymbol.Name) .WithModifiers(classDeclaration.Modifiers) - .AddMembers(items.Select(item => CreatePropertyDeclaration(item.LeadingTrivia, item.FieldSymbol)).ToArray()); + .AddMembers(items.Select(item => CreatePropertyDeclaration(item.LeadingTrivia, item.FieldSymbol, isNotifyPropertyChanging)).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -111,9 +123,10 @@ private void OnExecute( /// /// The leading trivia for the field to process. /// The input instance to process. + /// Indicates whether or not is also implemented. /// A generated instance for the input field. [Pure] - private PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol) + private PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging) { // Get the field type and the target property name string @@ -134,7 +147,10 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaList lea BlockSyntax setter = Block(); // Add the OnPropertyChanging() call if necessary - setter = setter.AddStatements(ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanging")))); + if (isNotifyPropertyChanging) + { + setter = setter.AddStatements(ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanging")))); + } // Add the following statements: // From e4f5648dfe70b8cf47d3d31c6bbe5f70bc3adc67 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Mar 2021 12:30:52 +0200 Subject: [PATCH 56/89] Added AlsoNotifyForAttribute type --- ...ervablePropertyGenerator.SyntaxReceiver.cs | 3 +- .../ObservablePropertyGenerator.cs | 23 +++++- ...osoft.Toolkit.Mvvm.SourceGenerators.csproj | 1 + .../Attributes/AlsoNotifyForAttribute.cs | 82 +++++++++++++++++++ .../Mvvm/Test_ObservablePropertyAttribute.cs | 32 ++++++++ 5 files changed, 138 insertions(+), 3 deletions(-) create mode 100644 Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs index fda1be7d65b..4b4e41b75bc 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs @@ -7,6 +7,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Toolkit.Mvvm.ComponentModel; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -32,7 +33,7 @@ private sealed class SyntaxReceiver : ISyntaxContextReceiver public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { if (context.Node is FieldDeclarationSyntax { AttributeLists: { Count: > 0 } } fieldDeclaration && - context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol attributeSymbol) + context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(ObservablePropertyAttribute).FullName) is INamedTypeSymbol attributeSymbol) { SyntaxTriviaList leadingTrivia = fieldDeclaration.GetLeadingTrivia(); diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 02cc2921c35..d515d4092da 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -83,7 +83,7 @@ private void OnExecute( var classDeclarationSyntax = ClassDeclaration(classDeclarationSymbol.Name) .WithModifiers(classDeclaration.Modifiers) - .AddMembers(items.Select(item => CreatePropertyDeclaration(item.LeadingTrivia, item.FieldSymbol, isNotifyPropertyChanging)).ToArray()); + .AddMembers(items.Select(item => CreatePropertyDeclaration(context, item.LeadingTrivia, item.FieldSymbol, isNotifyPropertyChanging)).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -121,12 +121,13 @@ private void OnExecute( /// /// Creates a instance for a specified field. /// + /// The input instance to use. /// The leading trivia for the field to process. /// The input instance to process. /// Indicates whether or not is also implemented. /// A generated instance for the input field. [Pure] - private PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging) + private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging) { // Get the field type and the target property name string @@ -164,6 +165,24 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(SyntaxTriviaList lea IdentifierName("value"))), ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanged")))); + INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyForAttribute).FullName)!; + + // Add dependent property notifications, if needed + if (fieldSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)) is AttributeData attributeData && + attributeData.ConstructorArguments.Length == 1) + { + foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments[0].Values) + { + if (attributeArgument.Value is string dependentPropertyName) + { + // OnPropertyChanged("OtherPropertyName"); + setter = setter.AddStatements(ExpressionStatement( + InvocationExpression(IdentifierName("OnPropertyChanged")) + .AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(dependentPropertyName)))))); + } + } + } + // Construct the generated property as follows: // // diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index 8118b09f328..a6c7f5fa143 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -14,6 +14,7 @@ + diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs new file mode 100644 index 00000000000..bb65869ac63 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs @@ -0,0 +1,82 @@ +// 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. + +#pragma warning disable CS1574 + +using System; +using System.ComponentModel; + +namespace Microsoft.Toolkit.Mvvm.ComponentModel +{ + /// + /// An attribute that can be used to support in generated properties. When this attribute is + /// used, the generated property setter will also call (or the equivalent + /// method in the target class) for the properties specified in the attribute data. This can be useful to keep the code compact when + /// there are one or more dependent properties that should also be reported as updated when the value of the annotated observable + /// property is changed. If this attribute is used in a field without , it is ignored. + /// + /// In order to use this attribute, the containing type has to implement the interface + /// and expose a method with the same signature as . If the containing + /// type also implements the interface and exposes a method with the same signature as + /// , then this method will be invoked as well by the property setter. + /// + /// + /// This attribute can be used as follows: + /// + /// partial class MyViewModel : ObservableObject + /// { + /// [ObservableProperty] + /// [AlsoNotifyFor(nameof(FullName))] + /// private string name; + /// + /// [ObservableProperty] + /// [AlsoNotifyFor(nameof(FullName))] + /// private string surname; + /// + /// public string FullName => $"{Name} {Surname}"; + /// } + /// + /// + /// And with this, code analogous to this will be generated: + /// + /// partial class MyViewModel + /// { + /// public string Name + /// { + /// get => name; + /// set => SetProperty(ref name, value); + /// } + /// + /// public string Surname + /// { + /// get => surname; + /// set + /// { + /// if (SetProperty(ref name, value)) + /// { + /// OnPropertyChanged(nameof(FullName)); + /// } + /// } + /// } + /// } + /// + /// + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] + public sealed class AlsoNotifyForAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The property names to also notify when the annotated property changes. + public AlsoNotifyForAttribute(params string[] propertyNames) + { + PropertyNames = propertyNames; + } + + /// + /// Gets the property names to also notify when the annotated property changes. + /// + public string[] PropertyNames { get; } + } +} diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs index 7f9957ae8be..dbc63dc9a00 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs @@ -2,6 +2,7 @@ // 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.Collections.Generic; using System.ComponentModel; using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -50,6 +51,23 @@ public void Test_ObservablePropertyAttribute_Events() Assert.AreEqual(changed.Item2, 42); } + [TestCategory("Mvvm")] + [TestMethod] + public void Test_AlsoNotifyForAttribute_Events() + { + var model = new DependentPropertyModel(); + + (PropertyChangedEventArgs, int) changed = default; + List propertyNames = new(); + + model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); + + model.Name = "Bob"; + model.Surname = "Ross"; + + CollectionAssert.AreEqual(new[] { nameof(model.Name), nameof(model.FullName), nameof(model.Surname), nameof(model.FullName) }, propertyNames); + } + public partial class SampleModel : ObservableObject { /// @@ -58,5 +76,19 @@ public partial class SampleModel : ObservableObject [ObservableProperty] private int data; } + + [INotifyPropertyChanged] + public sealed partial class DependentPropertyModel + { + [ObservableProperty] + [AlsoNotifyFor(nameof(FullName))] + private string name; + + [ObservableProperty] + [AlsoNotifyFor(nameof(FullName))] + private string surname; + + public string FullName => $"{Name} {Surname}"; + } } } From 02c87eb71df01f0bcf0fcdd6bca8c886a23bdd65 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Mar 2021 18:50:34 +0200 Subject: [PATCH 57/89] Added initial support for generated validation property attributes --- .../ObservablePropertyGenerator.cs | 32 +++++--- .../Extensions/AttributeDataExtensions.cs | 78 +++++++++++++++++++ 2 files changed, 100 insertions(+), 10 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index d515d4092da..ab304818d58 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -165,22 +165,32 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo IdentifierName("value"))), ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanged")))); - INamedTypeSymbol attributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyForAttribute).FullName)!; + INamedTypeSymbol alsoNotifyForAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyForAttribute).FullName)!; + INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute"); - // Add dependent property notifications, if needed - if (fieldSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)) is AttributeData attributeData && - attributeData.ConstructorArguments.Length == 1) + List validationAttributes = new(); + + foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) { - foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments[0].Values) + // Add dependent property notifications, if needed + if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyForAttributeSymbol)) { - if (attributeArgument.Value is string dependentPropertyName) + foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments[0].Values) { - // OnPropertyChanged("OtherPropertyName"); - setter = setter.AddStatements(ExpressionStatement( - InvocationExpression(IdentifierName("OnPropertyChanged")) - .AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(dependentPropertyName)))))); + if (attributeArgument.Value is string dependentPropertyName) + { + // OnPropertyChanged("OtherPropertyName"); + setter = setter.AddStatements(ExpressionStatement( + InvocationExpression(IdentifierName("OnPropertyChanged")) + .AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(dependentPropertyName)))))); + } } } + else if (validationAttributeSymbol is not null && + attributeData.AttributeClass?.InheritsFrom(validationAttributeSymbol) == true) + { + validationAttributes.Add(attributeData.AsAttributeSyntax()); + } } // Construct the generated property as follows: @@ -189,6 +199,7 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.DebuggerNonUserCode] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + // // Optional // public // { // get => ; @@ -212,6 +223,7 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) + .AddAttributeLists(validationAttributes.Select(static a => AttributeList(SingletonSeparatedList(a))).ToArray()) .WithLeadingTrivia(leadingTrivia) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAccessorListAccessors( diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs index 0d5c5834096..93f9ac9f1d4 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Extensions/AttributeDataExtensions.cs @@ -2,9 +2,14 @@ // 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.Diagnostics.Contracts; +using System.Linq; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions { @@ -36,5 +41,78 @@ properties.Value.Value is T argumentValue && return false; } + + /// + /// Creates an node that is equivalent to the input instance. + /// + /// The input instance to process. + /// An replicating the data in . + [Pure] + public static AttributeSyntax AsAttributeSyntax(this AttributeData attributeData) + { + IdentifierNameSyntax attributeType = IdentifierName(attributeData.AttributeClass!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + AttributeArgumentSyntax[] arguments = + attributeData.ConstructorArguments + .Select(static arg => AttributeArgument(ToExpression(arg))).Concat( + attributeData.NamedArguments + .Select(static arg => + AttributeArgument(ToExpression(arg.Value)) + .WithNameEquals(NameEquals(IdentifierName(arg.Key))))).ToArray(); + + return Attribute(attributeType, AttributeArgumentList(SeparatedList(SeparatedList(arguments)))); + + static ExpressionSyntax ToExpression(TypedConstant arg) + { + if (arg.IsNull) + { + return LiteralExpression(SyntaxKind.NullLiteralExpression); + } + + if (arg.Kind == TypedConstantKind.Array) + { + string elementType = ((IArrayTypeSymbol)arg.Type!).ElementType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + return + ArrayCreationExpression( + ArrayType(IdentifierName(elementType)) + .AddRankSpecifiers(ArrayRankSpecifier(SingletonSeparatedList(OmittedArraySizeExpression())))) + .WithInitializer(InitializerExpression(SyntaxKind.ArrayInitializerExpression) + .AddExpressions(arg.Values.Select(ToExpression).ToArray())); + } + + switch ((arg.Kind, arg.Value)) + { + case (TypedConstantKind.Primitive, string text): + return LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(text)); + case (TypedConstantKind.Primitive, bool flag) when flag: + return LiteralExpression(SyntaxKind.TrueLiteralExpression); + case (TypedConstantKind.Primitive, bool): + return LiteralExpression(SyntaxKind.FalseLiteralExpression); + case (TypedConstantKind.Primitive, object value): + return LiteralExpression(SyntaxKind.NumericLiteralExpression, value switch + { + byte b => Literal(b), + char c => Literal(c), + double d => Literal(d), + float f => Literal(f), + int i => Literal(i), + long l => Literal(l), + sbyte sb => Literal(sb), + short sh => Literal(sh), + uint ui => Literal(ui), + ulong ul => Literal(ul), + ushort ush => Literal(ush), + _ => throw new ArgumentException() + }); + case (TypedConstantKind.Type, ITypeSymbol type): + return TypeOfExpression(IdentifierName(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); + case (TypedConstantKind.Enum, object value): + return CastExpression( + IdentifierName(arg.Type!.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + LiteralExpression(SyntaxKind.NumericLiteralExpression, ParseToken(value.ToString()))); + default: throw new ArgumentException(); + } + } + } } } From 9c335913014d4c921a5346b3e14ba564379bfe9a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Mar 2021 20:14:14 +0200 Subject: [PATCH 58/89] Added unit tests for validation attributes generation --- .../Mvvm/Test_ObservablePropertyAttribute.cs | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs index dbc63dc9a00..64daa2725cd 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs @@ -2,8 +2,12 @@ // 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.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Configuration; +using System.Reflection; using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -68,6 +72,50 @@ public void Test_AlsoNotifyForAttribute_Events() CollectionAssert.AreEqual(new[] { nameof(model.Name), nameof(model.FullName), nameof(model.Surname), nameof(model.FullName) }, propertyNames); } + [TestCategory("Mvvm")] + [TestMethod] + public void Test_ValidationAttributes() + { + var nameProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Name)); + + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.AreEqual(nameProperty.GetCustomAttribute().Length, 1); + Assert.IsNotNull(nameProperty.GetCustomAttribute()); + Assert.AreEqual(nameProperty.GetCustomAttribute().Length, 100); + + var ageProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Age)); + + Assert.IsNotNull(ageProperty.GetCustomAttribute()); + Assert.AreEqual(ageProperty.GetCustomAttribute().Minimum, 0); + Assert.AreEqual(ageProperty.GetCustomAttribute().Maximum, 120); + + var emailProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Email)); + + Assert.IsNotNull(emailProperty.GetCustomAttribute()); + + var comboProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.IfThisWorksThenThatsGreat)); + + TestValidationAttribute? testAttribute = comboProperty.GetCustomAttribute(); + + Assert.IsNotNull(testAttribute); + Assert.IsNull(testAttribute.O); + Assert.AreEqual(testAttribute.T, typeof(SampleModel)); + Assert.AreEqual(testAttribute.Flag, true); + Assert.AreEqual(testAttribute.D, 6.28); + CollectionAssert.AreEqual(testAttribute.Names, new[] { "Bob", "Ross" }); + + object[] nestedArray = (object[])testAttribute.NestedArray; + + Assert.AreEqual(nestedArray.Length, 3); + Assert.AreEqual(nestedArray[0], 1); + Assert.AreEqual(nestedArray[1], "Hello"); + Assert.IsTrue(nestedArray[2] is int[]); + CollectionAssert.AreEqual((int[])nestedArray[2], new[] { 2, 3, 4 }); + + Assert.AreEqual(testAttribute.Animal, Animal.Llama); + } + public partial class SampleModel : ObservableObject { /// @@ -90,5 +138,59 @@ public sealed partial class DependentPropertyModel public string FullName => $"{Name} {Surname}"; } + + public partial class MyFormViewModel : ObservableRecipient + { + [ObservableProperty] + [Required] + [MinLength(1)] + [MaxLength(100)] + private string name; + + [ObservableProperty] + [Range(0, 120)] + private int age; + + [ObservableProperty] + [EmailAddress] + private string email; + + [ObservableProperty] + [TestValidation(null, typeof(SampleModel), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Animal.Llama)] + private int ifThisWorksThenThatsGreat; + } + + private sealed class TestValidationAttribute : ValidationAttribute + { + public TestValidationAttribute(object o, Type t, bool flag, double d, string[] names) + { + O = o; + T = t; + Flag = flag; + D = d; + Names = names; + } + + public object O { get; } + + public Type T { get; } + + public bool Flag { get; } + + public double D { get; } + + public string[] Names { get; } + + public object NestedArray { get; set; } + + public Animal Animal { get; set; } + } + + public enum Animal + { + Cat, + Dog, + Llama + } } } From 96f6e977b6d518cbaecf75873269cd0785cd1326 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Mar 2021 22:44:45 +0200 Subject: [PATCH 59/89] Enabled validation support for generated properties --- .../ObservablePropertyGenerator.cs | 176 ++++++++++++------ ...ValidatorValidateAllPropertiesGenerator.cs | 2 +- .../IMessengerRegisterAllGenerator.cs | 4 +- .../Mvvm/Test_ObservablePropertyAttribute.cs | 1 - 4 files changed, 118 insertions(+), 65 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index ab304818d58..4e559db62e0 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -57,7 +57,7 @@ public void Execute(GeneratorExecutionContext context) /// The node to process. /// The for . /// The sequence of fields to process. - private void OnExecute( + private static void OnExecute( GeneratorExecutionContext context, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, @@ -127,47 +127,18 @@ private void OnExecute( /// Indicates whether or not is also implemented. /// A generated instance for the input field. [Pure] - private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging) + private static PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging) { // Get the field type and the target property name string typeName = fieldSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - propertyName = fieldSymbol.Name; - - if (propertyName.StartsWith("m_")) - { - propertyName = propertyName.Substring(2); - } - else if (propertyName.StartsWith("_")) - { - propertyName = propertyName.TrimStart('_'); - } - - propertyName = $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}"; - - BlockSyntax setter = Block(); - - // Add the OnPropertyChanging() call if necessary - if (isNotifyPropertyChanging) - { - setter = setter.AddStatements(ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanging")))); - } - - // Add the following statements: - // - // = value; - // OnPropertyChanged(); - setter = setter.AddStatements( - ExpressionStatement( - AssignmentExpression( - SyntaxKind.SimpleAssignmentExpression, - IdentifierName(fieldSymbol.Name), - IdentifierName("value"))), - ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanged")))); + propertyName = GetGeneratedPropertyName(fieldSymbol); + INamedTypeSymbol observableValidatorSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator")!; INamedTypeSymbol alsoNotifyForAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyForAttribute).FullName)!; INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute"); + List dependentPropertyNotificationStatements = new(); List validationAttributes = new(); foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) @@ -180,7 +151,7 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo if (attributeArgument.Value is string dependentPropertyName) { // OnPropertyChanged("OtherPropertyName"); - setter = setter.AddStatements(ExpressionStatement( + dependentPropertyNotificationStatements.Add(ExpressionStatement( InvocationExpression(IdentifierName("OnPropertyChanged")) .AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(dependentPropertyName)))))); } @@ -189,28 +160,105 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo else if (validationAttributeSymbol is not null && attributeData.AttributeClass?.InheritsFrom(validationAttributeSymbol) == true) { + // Track the current validation attribute validationAttributes.Add(attributeData.AsAttributeSyntax()); } } + BlockSyntax setterBlock; + + if (validationAttributes.Count > 0) + { + // Generate the inner setter block as follows: + // + // if (SetProperty(ref , value, true)) + // { + // OnPropertyChanged("Property1"); // Optional + // OnPropertyChanged("Property2"); + // ... + // OnPropertyChanged("PropertyN"); + // } + setterBlock = Block( + IfStatement( + InvocationExpression(IdentifierName("SetProperty")) + .AddArgumentListArguments( + Argument(IdentifierName(fieldSymbol.Name)).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(IdentifierName("value")), + Argument(LiteralExpression(SyntaxKind.TrueLiteralExpression))), + Block(dependentPropertyNotificationStatements))); + } + else + { + BlockSyntax updateAndNotificationBlock = Block(); + + // Add the OnPropertyChanging() call if necessary + if (isNotifyPropertyChanging) + { + updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanging")))); + } + + // Add the following statements: + // + // = value; + // OnPropertyChanged(); + updateAndNotificationBlock = updateAndNotificationBlock.AddStatements( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldSymbol.Name), + IdentifierName("value"))), + ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanged")))); + + // Add the dependent property notifications at the end + updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(dependentPropertyNotificationStatements.ToArray()); + + // Generate the inner setter block as follows: + // + // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(, value)) + // { + // OnPropertyChanging(); // Optional + // = value; + // OnPropertyChanged(); + // OnPropertyChanged("Property1"); // Optional + // OnPropertyChanged("Property2"); + // ... + // OnPropertyChanged("PropertyN"); + // } + setterBlock = Block( + IfStatement( + PrefixUnaryExpression( + SyntaxKind.LogicalNotExpression, + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + GenericName(Identifier("global::System.Collections.Generic.EqualityComparer")) + .AddTypeArgumentListArguments(IdentifierName(typeName)), + IdentifierName("Default")), + IdentifierName("Equals"))) + .AddArgumentListArguments( + Argument(IdentifierName(fieldSymbol.Name)), + Argument(IdentifierName("value")))), + updateAndNotificationBlock)); + } + // Construct the generated property as follows: // // // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.DebuggerNonUserCode] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - // // Optional + // // Optional + // + // ... + // // public // { // get => ; // set // { - // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(, value)) - // { - // OnPropertyChanging(); // Optional - // = value; - // OnPropertyChanged(); - // } + // // } // } return @@ -219,8 +267,8 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo AttributeList(SingletonSeparatedList( Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")) .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .AddAttributeLists(validationAttributes.Select(static a => AttributeList(SingletonSeparatedList(a))).ToArray()) @@ -231,23 +279,29 @@ private PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionCo .WithExpressionBody(ArrowExpressionClause(IdentifierName(fieldSymbol.Name))) .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)), AccessorDeclaration(SyntaxKind.SetAccessorDeclaration) - .AddBodyStatements( - IfStatement( - PrefixUnaryExpression( - SyntaxKind.LogicalNotExpression, - InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - GenericName(Identifier("global::System.Collections.Generic.EqualityComparer")) - .AddTypeArgumentListArguments(IdentifierName(typeName)), - IdentifierName("Default")), - IdentifierName("Equals"))) - .AddArgumentListArguments( - Argument(IdentifierName(fieldSymbol.Name)), - Argument(IdentifierName("value")))), - setter))); + .WithBody(setterBlock)); + } + + /// + /// Get the generated property name for an input field. + /// + /// The input instance to process. + /// The generated property name for . + [Pure] + private static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol) + { + string propertyName = fieldSymbol.Name; + + if (propertyName.StartsWith("m_")) + { + propertyName = propertyName.Substring(2); + } + else if (propertyName.StartsWith("_")) + { + propertyName = propertyName.TrimStart('_'); + } + + return $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}"; } } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs index 7c9f625770f..516f717860a 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -154,7 +154,7 @@ private static IEnumerable EnumerateValidationStatements(INamed // // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.)); // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.)); - // // ... + // ... // __ObservableValidatorHelper.ValidateProperty(instance, instance., nameof(instance.)); yield return ExpressionStatement( diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index d805896b413..f3252dd931a 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -179,7 +179,7 @@ private static IEnumerable EnumerateRegistrationStatements(INam // // messenger.Register<>(recipient); // messenger.Register<>(recipient); - // // ... + // ... // messenger.Register<>(recipient); yield return ExpressionStatement( @@ -213,7 +213,7 @@ private static IEnumerable EnumerateRegistrationStatementsWithT // // messenger.Register<, TToken>(recipient, token); // messenger.Register<, TToken>(recipient, token); - // // ... + // ... // messenger.Register<, TToken>(recipient, token); yield return ExpressionStatement( diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs index 64daa2725cd..4c1ea300678 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; -using System.Configuration; using System.Reflection; using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.VisualStudio.TestTools.UnitTesting; From c0d4f009f2102e0518691011fbd32b1f461b75d8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Mar 2021 22:55:20 +0200 Subject: [PATCH 60/89] Removed unnecessary if statement in generated code --- .../ObservablePropertyGenerator.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 4e559db62e0..e99165bfdad 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -171,6 +171,10 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExec { // Generate the inner setter block as follows: // + // SetProperty(ref , value, true); + // + // Or in case there is at least one dependent property: + // // if (SetProperty(ref , value, true)) // { // OnPropertyChanged("Property1"); // Optional @@ -178,14 +182,18 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExec // ... // OnPropertyChanged("PropertyN"); // } - setterBlock = Block( - IfStatement( - InvocationExpression(IdentifierName("SetProperty")) - .AddArgumentListArguments( - Argument(IdentifierName(fieldSymbol.Name)).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), - Argument(IdentifierName("value")), - Argument(LiteralExpression(SyntaxKind.TrueLiteralExpression))), - Block(dependentPropertyNotificationStatements))); + InvocationExpressionSyntax setPropertyExpression = + InvocationExpression(IdentifierName("SetProperty")) + .AddArgumentListArguments( + Argument(IdentifierName(fieldSymbol.Name)).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(IdentifierName("value")), + Argument(LiteralExpression(SyntaxKind.TrueLiteralExpression))); + + setterBlock = dependentPropertyNotificationStatements.Count switch + { + 0 => Block(ExpressionStatement(setPropertyExpression)), + _ => Block(IfStatement(setPropertyExpression, Block(dependentPropertyNotificationStatements))) + }; } else { From 6188d3d1ecf447cb56ba06bfa68bd3df8ac365e1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Mar 2021 23:09:00 +0200 Subject: [PATCH 61/89] Improved constructor definition for [AlsoNotifyForAttribute] type --- .../ObservablePropertyGenerator.cs | 25 +++++++++++++++++-- .../Attributes/AlsoNotifyForAttribute.cs | 20 ++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index e99165bfdad..9a4e2cb17c0 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -146,15 +146,36 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExec // Add dependent property notifications, if needed if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyForAttributeSymbol)) { - foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments[0].Values) + foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments) { - if (attributeArgument.Value is string dependentPropertyName) + if (attributeArgument.IsNull) + { + continue; + } + + if (attributeArgument.Kind == TypedConstantKind.Primitive && + attributeArgument.Value is string dependentPropertyName) { // OnPropertyChanged("OtherPropertyName"); dependentPropertyNotificationStatements.Add(ExpressionStatement( InvocationExpression(IdentifierName("OnPropertyChanged")) .AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(dependentPropertyName)))))); } + else if (attributeArgument.Kind == TypedConstantKind.Array) + { + foreach (TypedConstant nestedAttributeArgument in attributeArgument.Values) + { + if (nestedAttributeArgument.IsNull) + { + continue; + } + + // Additional property names + dependentPropertyNotificationStatements.Add(ExpressionStatement( + InvocationExpression(IdentifierName("OnPropertyChanged")) + .AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal((string)nestedAttributeArgument.Value!)))))); + } + } } } else if (validationAttributeSymbol is not null && diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs index bb65869ac63..817ba9e2c2f 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs @@ -6,6 +6,7 @@ using System; using System.ComponentModel; +using System.Linq; namespace Microsoft.Toolkit.Mvvm.ComponentModel { @@ -68,10 +69,23 @@ public sealed class AlsoNotifyForAttribute : Attribute /// /// Initializes a new instance of the class. /// - /// The property names to also notify when the annotated property changes. - public AlsoNotifyForAttribute(params string[] propertyNames) + /// The name of the property to also notify when the annotated property changes. + public AlsoNotifyForAttribute(string propertyName) { - PropertyNames = propertyNames; + PropertyNames = new[] { propertyName }; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the property to also notify when the annotated property changes. + /// + /// The other property names to also notify when the annotated property changes. This parameter can optionally + /// be used to indicate a series of dependent properties from the same attribute, to keep the code more compact. + /// + public AlsoNotifyForAttribute(string propertyName, string[] otherPropertyNames) + { + PropertyNames = new[] { propertyName }.Concat(otherPropertyNames).ToArray(); } /// From 25cf03012c800c44a3c85e759d19c0f40064f406 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Mar 2021 23:29:35 +0200 Subject: [PATCH 62/89] Added diagnostic for observable property + validation without base class --- .../AnalyzerReleases.Unshipped.md | 3 +- .../ObservablePropertyGenerator.cs | 41 +++++++++++++++---- .../TransitiveMembersGenerator.cs | 1 - .../Diagnostics/DiagnosticDescriptors.cs | 16 ++++++++ .../Mvvm/Test_ObservablePropertyAttribute.cs | 2 +- 5 files changed, 51 insertions(+), 12 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md index c7377fbe147..4340760463f 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -12,4 +12,5 @@ MVVMTK0004 | Microsoft.Toolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGener MVVMTK0005 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit MVVMTK0006 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit MVVMTK0007 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0008 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit \ No newline at end of file +MVVMTK0008 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0009 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 9a4e2cb17c0..4da87a1595d 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -12,9 +12,11 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics; using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; +using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -63,16 +65,19 @@ private static void OnExecute( INamedTypeSymbol classDeclarationSymbol, IEnumerable items) { - // Check whether INotifyPropertyChanging is present as well INamedTypeSymbol iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanging).FullName)!, observableObjectSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject")!, - observableObjectAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(ObservableObjectAttribute).FullName)!; + observableObjectAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(ObservableObjectAttribute).FullName)!, + observableValidatorSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator")!; - bool isNotifyPropertyChanging = - classDeclarationSymbol.AllInterfaces.Contains(iNotifyPropertyChangingSymbol, SymbolEqualityComparer.Default) || - classDeclarationSymbol.InheritsFrom(observableObjectSymbol) || - classDeclarationSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observableObjectAttributeSymbol)); + // Check whether the current type implements INotifyPropertyChanging and whether it inherits from ObservableValidator + bool + isObservableValidator = classDeclarationSymbol.InheritsFrom(observableValidatorSymbol), + isNotifyPropertyChanging = + classDeclarationSymbol.AllInterfaces.Contains(iNotifyPropertyChangingSymbol, SymbolEqualityComparer.Default) || + classDeclarationSymbol.InheritsFrom(observableObjectSymbol) || + classDeclarationSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observableObjectAttributeSymbol)); // Create the class declaration for the user type. This will produce a tree as follows: // @@ -83,7 +88,7 @@ private static void OnExecute( var classDeclarationSyntax = ClassDeclaration(classDeclarationSymbol.Name) .WithModifiers(classDeclaration.Modifiers) - .AddMembers(items.Select(item => CreatePropertyDeclaration(context, item.LeadingTrivia, item.FieldSymbol, isNotifyPropertyChanging)).ToArray()); + .AddMembers(items.Select(item => CreatePropertyDeclaration(context, item.LeadingTrivia, item.FieldSymbol, isNotifyPropertyChanging, isObservableValidator)).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -125,16 +130,21 @@ private static void OnExecute( /// The leading trivia for the field to process. /// The input instance to process. /// Indicates whether or not is also implemented. + /// Indicates whether or not the containing type inherits from ObservableValidator. /// A generated instance for the input field. [Pure] - private static PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging) + private static PropertyDeclarationSyntax CreatePropertyDeclaration( + GeneratorExecutionContext context, + SyntaxTriviaList leadingTrivia, + IFieldSymbol fieldSymbol, + bool isNotifyPropertyChanging, + bool isObservableValidator) { // Get the field type and the target property name string typeName = fieldSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), propertyName = GetGeneratedPropertyName(fieldSymbol); - INamedTypeSymbol observableValidatorSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator")!; INamedTypeSymbol alsoNotifyForAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyForAttribute).FullName)!; INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute"); @@ -190,6 +200,19 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration(GeneratorExec if (validationAttributes.Count > 0) { + // Emit a diagnostic if the current type doesn't inherit from ObservableValidator + if (!isObservableValidator) + { + context.ReportDiagnostic( + MissingObservableValidatorInheritanceError, + fieldSymbol, + fieldSymbol.ContainingType, + fieldSymbol.Name, + validationAttributes.Count); + + setterBlock = Block(); + } + // Generate the inner setter block as follows: // // SetProperty(ref , value, true); diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index d0a464b7ec4..6d2c61e58aa 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.CodeDom.Compiler; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 3381f7eca21..58518be9f17 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -140,5 +140,21 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: $"Cannot apply [{nameof(ObservableRecipientAttribute)}] to a type that lacks necessary base functionality (it should either inherit from ObservableObject, or be annotated with [{nameof(ObservableObjectAttribute)}] or [{nameof(INotifyPropertyChangedAttribute)}]).", helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when the target type doesn't inherit from the ObservableValidator class. + /// + /// Format: "The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator". + /// + /// + public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceError = new( + id: "MVVMTK0009", + title: "Missing ObservableValidator inheritance", + messageFormat: $"The field {{0}}.{{1}} cannot be used to generate an observable property, as it has {{2}} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator", + category: typeof(ObservablePropertyGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot apply [{nameof(ObservablePropertyAttribute)}] to fields with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); } } diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs index 4c1ea300678..196381672ad 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs @@ -138,7 +138,7 @@ public sealed partial class DependentPropertyModel public string FullName => $"{Name} {Surname}"; } - public partial class MyFormViewModel : ObservableRecipient + public partial class MyFormViewModel : ObservableValidator { [ObservableProperty] [Required] From 414531835a5abb691eda14071296c934cfffa287 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Mar 2021 23:34:23 +0200 Subject: [PATCH 63/89] Added diagnostic for failure in ObservablePropertyGenerator --- .../AnalyzerReleases.Unshipped.md | 3 ++- .../ObservablePropertyGenerator.cs | 9 ++++++++- .../Diagnostics/DiagnosticDescriptors.cs | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md index 4340760463f..70332985966 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -13,4 +13,5 @@ MVVMTK0005 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | MVVMTK0006 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit MVVMTK0007 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit MVVMTK0008 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0009 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit \ No newline at end of file +MVVMTK0009 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0010 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 4da87a1595d..35648df3658 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -47,7 +47,14 @@ public void Execute(GeneratorExecutionContext context) if (items.Key.DeclaringSyntaxReferences.Length > 0 && items.Key.DeclaringSyntaxReferences.First().GetSyntax() is ClassDeclarationSyntax classDeclaration) { - OnExecute(context, classDeclaration, items.Key, items); + try + { + OnExecute(context, classDeclaration, items.Key, items); + } + catch + { + context.ReportDiagnostic(ObservablePropertyGeneratorError, items.Key, items.Key); + } } } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index 58518be9f17..f06879c4737 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -156,5 +156,21 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: $"Cannot apply [{nameof(ObservablePropertyAttribute)}] to fields with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.", helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when failed to run on a given type. + /// + /// Format: "The generator ObservablePropertyGenerator failed to execute on type {0}". + /// + /// + public static readonly DiagnosticDescriptor ObservablePropertyGeneratorError = new( + id: "MVVMTK0010", + title: $"Internal error for {nameof(ObservablePropertyGenerator)}", + messageFormat: $"The generator {nameof(ObservablePropertyGenerator)} failed to execute on type {{0}}", + category: typeof(ObservableObjectGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"The {nameof(ObservablePropertyGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); } } From 6b6d66a1ee1420e98694dc56e5efde688d2f752e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 Mar 2021 13:44:08 +0200 Subject: [PATCH 64/89] Minor optimization to [ObservableProperty] syntax receiver --- ...servablePropertyGenerator.SyntaxReceiver.cs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs index 4b4e41b75bc..bdaafa7b966 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs @@ -40,11 +40,9 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) foreach (VariableDeclaratorSyntax variableDeclarator in fieldDeclaration.Declaration.Variables) { if (context.SemanticModel.GetDeclaredSymbol(variableDeclarator) is IFieldSymbol fieldSymbol && - fieldSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)) is AttributeData attributeData && - attributeData.ApplicationSyntaxReference is SyntaxReference syntaxReference && - syntaxReference.GetSyntax() is AttributeSyntax attributeSyntax) + fieldSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol))) { - this.gatheredInfo.Add(new Item(leadingTrivia, variableDeclarator, fieldSymbol, attributeSyntax, attributeData)); + this.gatheredInfo.Add(new Item(leadingTrivia, fieldSymbol)); } } } @@ -54,16 +52,8 @@ attributeData.ApplicationSyntaxReference is SyntaxReference syntaxReference && /// A model for a group of item representing a discovered type to process. /// /// The leading trivia for the field declaration. - /// The instance for the target field variable declaration. - /// The instance for . - /// The instance for the target attribute over . - /// The instance for . - public sealed record Item( - SyntaxTriviaList LeadingTrivia, - VariableDeclaratorSyntax FieldDeclarator, - IFieldSymbol FieldSymbol, - AttributeSyntax AttributeSyntax, - AttributeData AttributeData); + /// The instance for the target field. + public sealed record Item(SyntaxTriviaList LeadingTrivia, IFieldSymbol FieldSymbol); } } } From dadbd48c1f87a8bd922c631ee57f9cd9c6c98bc1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 Mar 2021 18:50:18 +0200 Subject: [PATCH 65/89] Added draft tests for source generator diagnostics --- .../Test_SourceGeneratorsDiagnostics.cs | 258 ++++++++++++++++++ .../UnitTests.SourceGenerators.csproj | 20 ++ Windows Community Toolkit (NET).slnf | 3 +- Windows Community Toolkit.sln | 25 +- 4 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs create mode 100644 UnitTests/UnitTests.SourceGenerators/UnitTests.SourceGenerators.csproj diff --git a/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs b/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs new file mode 100644 index 00000000000..3041a53fb47 --- /dev/null +++ b/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs @@ -0,0 +1,258 @@ +// 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.Collections.Immutable; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Toolkit.Mvvm.SourceGenerators; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.Mvvm +{ + [TestClass] + public class Test_SourceGeneratorsDiagnostics + { + [TestCategory("Mvvm")] + [TestMethod] + public void DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError_Explicit() + { + string source = @" + using System.ComponentModel; + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace MyApp + { + [INotifyPropertyChanged] + public partial class SampleViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + } + }"; + + VerifyGeneratedDiagnostics(source, "MVVMTK0004"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError_Inherited() + { + string source = @" + using System.ComponentModel; + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace Microsoft.Toolkit.Mvvm.ComponentModel + { + public abstract class ObservableObject : INotifyPropertyChanged, INotifyPropertyChanging + { + public event PropertyChangedEventHandler? PropertyChanged; + public event PropertyChangingEventHandler? PropertyChanging; + } + } + + namespace MyApp + { + [INotifyPropertyChanged] + public partial class SampleViewModel : ObservableObject + { + } + }"; + + VerifyGeneratedDiagnostics(source, "MVVMTK0004"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void DuplicateINotifyPropertyChangedInterfaceForObservableObjectAttributeError_Explicit() + { + string source = @" + using System.ComponentModel; + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace MyApp + { + [ObservableObject] + public partial class SampleViewModel : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + } + }"; + + VerifyGeneratedDiagnostics(source, "MVVMTK0005"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void DuplicateINotifyPropertyChangedInterfaceForObservableObjectAttributeError_Inherited() + { + string source = @" + using System.ComponentModel; + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace Microsoft.Toolkit.Mvvm.ComponentModel + { + public abstract class ObservableObject : INotifyPropertyChanged + { + public event PropertyChangedEventHandler? PropertyChanged; + } + } + + namespace MyApp + { + [ObservableObject] + public partial class SampleViewModel : ObservableObject + { + } + }"; + + VerifyGeneratedDiagnostics(source, "MVVMTK0005"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void DuplicateINotifyPropertyChangingInterfaceForObservableObjectAttributeError_Explicit() + { + string source = @" + using System.ComponentModel; + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace MyApp + { + [ObservableObject] + public partial class SampleViewModel : INotifyPropertyChanging + { + public event PropertyChangingEventHandler? PropertyChanging; + } + }"; + + VerifyGeneratedDiagnostics(source, "MVVMTK0006"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void DuplicateINotifyPropertyChangingInterfaceForObservableObjectAttributeError_Inherited() + { + string source = @" + using System.ComponentModel; + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace MyApp + { + public abstract class MyBaseViewModel : INotifyPropertyChanging + { + public event PropertyChangingEventHandler? PropertyChanging; + } + + [ObservableObject] + public partial class SampleViewModel : MyBaseViewModel + { + } + }"; + + VerifyGeneratedDiagnostics(source, "MVVMTK0006"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void DuplicateObservableRecipientError() + { + string source = @" + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace Microsoft.Toolkit.Mvvm.ComponentModel + { + public abstract class ObservableRecipient : ObservableObject + { + } + } + + namespace MyApp + { + [ObservableRecipient] + public partial class SampleViewModel : ObservableRecipient + { + } + }"; + + VerifyGeneratedDiagnostics(source, "MVVMTK0007"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void MissingBaseObservableObjectFunctionalityError() + { + string source = @" + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace MyApp + { + [ObservableRecipient] + public partial class SampleViewModel + { + } + }"; + + VerifyGeneratedDiagnostics(source, "MVVMTK0008"); + } + + [TestCategory("Mvvm")] + [TestMethod] + public void MissingObservableValidatorInheritanceError() + { + string source = @" + using System.ComponentModel.DataAnnotations; + using Microsoft.Toolkit.Mvvm.ComponentModel; + + namespace MyApp + { + [INotifyPropertyChanged] + public partial class SampleViewModel + { + [ObservableProperty] + [Required] + private string name; + } + }"; + + VerifyGeneratedDiagnostics(source, "MVVMTK0009"); + } + + /// + /// Verifies the output of a source generator. + /// + /// The generator type to use. + /// The input source to process. + /// The diagnostic ids to expect for the input source code. + private void VerifyGeneratedDiagnostics(string source, params string[] diagnosticsIds) + where TGenerator : class, ISourceGenerator, new() + { + Type validationAttributeType = typeof(ValidationAttribute); + + SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(source); + + IEnumerable references = + from assembly in AppDomain.CurrentDomain.GetAssemblies() + where !assembly.IsDynamic + let reference = MetadataReference.CreateFromFile(assembly.Location) + select reference; + + CSharpCompilation compilation = CSharpCompilation.Create("original", new SyntaxTree[] { syntaxTree }, references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + ISourceGenerator generator = new TGenerator(); + + CSharpGeneratorDriver driver = CSharpGeneratorDriver.Create(generator); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out Compilation outputCompilation, out ImmutableArray diagnostics); + + HashSet resultingIds = diagnostics.Select(diagnostic => diagnostic.Id).ToHashSet(); + + Assert.IsTrue(resultingIds.SetEquals(diagnosticsIds)); + + GC.KeepAlive(validationAttributeType); + } + } +} diff --git a/UnitTests/UnitTests.SourceGenerators/UnitTests.SourceGenerators.csproj b/UnitTests/UnitTests.SourceGenerators/UnitTests.SourceGenerators.csproj new file mode 100644 index 00000000000..6b580aa0997 --- /dev/null +++ b/UnitTests/UnitTests.SourceGenerators/UnitTests.SourceGenerators.csproj @@ -0,0 +1,20 @@ + + + + net5.0 + false + 9.0 + + + + + + + + + + + + + + diff --git a/Windows Community Toolkit (NET).slnf b/Windows Community Toolkit (NET).slnf index a91a7b512b1..41a012b46b7 100644 --- a/Windows Community Toolkit (NET).slnf +++ b/Windows Community Toolkit (NET).slnf @@ -10,7 +10,8 @@ "UnitTests\\UnitTests.HighPerformance.NetCore\\UnitTests.HighPerformance.NetCore.csproj", "UnitTests\\UnitTests.HighPerformance.Shared\\UnitTests.HighPerformance.Shared.shproj", "UnitTests\\UnitTests.NetCore\\UnitTests.NetCore.csproj", - "UnitTests\\UnitTests.Shared\\UnitTests.Shared.shproj" + "UnitTests\\UnitTests.Shared\\UnitTests.Shared.shproj", + "UnitTests\\UnitTests.SourceGenerators\\UnitTests.SourceGenerators.csproj", ] } } \ No newline at end of file diff --git a/Windows Community Toolkit.sln b/Windows Community Toolkit.sln index bd397f3b7ae..9b0c8e72f87 100644 --- a/Windows Community Toolkit.sln +++ b/Windows Community Toolkit.sln @@ -157,7 +157,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Toolkit.Uwp.UI.Co EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Toolkit.Uwp.UI.Controls", "Microsoft.Toolkit.Uwp.UI.Controls\Microsoft.Toolkit.Uwp.UI.Controls.csproj", "{099B60FD-DAD6-4648-9DE2-8DBF9DCD9557}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Toolkit.Mvvm.SourceGenerators", "Microsoft.Toolkit.Mvvm.SourceGenerators\Microsoft.Toolkit.Mvvm.SourceGenerators.csproj", "{E24D1146-5AD8-498F-A518-4890D8BF4937}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Toolkit.Mvvm.SourceGenerators", "Microsoft.Toolkit.Mvvm.SourceGenerators\Microsoft.Toolkit.Mvvm.SourceGenerators.csproj", "{E24D1146-5AD8-498F-A518-4890D8BF4937}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.SourceGenerators", "UnitTests\UnitTests.SourceGenerators\UnitTests.SourceGenerators.csproj", "{338C3BE4-2E71-4F21-AD30-03FDBB47A272}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -1132,6 +1134,26 @@ Global {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|x64.Build.0 = Release|Any CPU {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|x86.ActiveCfg = Release|Any CPU {E24D1146-5AD8-498F-A518-4890D8BF4937}.Release|x86.Build.0 = Release|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Debug|Any CPU.Build.0 = Debug|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Debug|ARM.ActiveCfg = Debug|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Debug|ARM.Build.0 = Debug|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Debug|ARM64.Build.0 = Debug|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Debug|x64.ActiveCfg = Debug|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Debug|x64.Build.0 = Debug|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Debug|x86.ActiveCfg = Debug|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Debug|x86.Build.0 = Debug|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Release|Any CPU.ActiveCfg = Release|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Release|Any CPU.Build.0 = Release|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Release|ARM.ActiveCfg = Release|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Release|ARM.Build.0 = Release|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Release|ARM64.ActiveCfg = Release|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Release|ARM64.Build.0 = Release|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Release|x64.ActiveCfg = Release|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Release|x64.Build.0 = Release|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Release|x86.ActiveCfg = Release|Any CPU + {338C3BE4-2E71-4F21-AD30-03FDBB47A272}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1181,6 +1203,7 @@ Global {AF1BE4E9-E2E1-4729-B076-B3725D8E21EE} = {F1AFFFA7-28FE-4770-BA48-10D76F3E59BC} {3307BC1D-5D71-41C6-A1B3-B113B8242D08} = {F1AFFFA7-28FE-4770-BA48-10D76F3E59BC} {099B60FD-DAD6-4648-9DE2-8DBF9DCD9557} = {F1AFFFA7-28FE-4770-BA48-10D76F3E59BC} + {338C3BE4-2E71-4F21-AD30-03FDBB47A272} = {B30036C4-D514-4E5B-A323-587A061772CE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5403B0C4-F244-4F73-A35C-FE664D0F4345} From 30d91ea54bce8df96748a87117191eddb4d2b536 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 Mar 2021 20:08:16 +0200 Subject: [PATCH 66/89] Added ICommandAttribute type --- ...osoft.Toolkit.Mvvm.SourceGenerators.csproj | 1 + .../Input/Attributes/ICommandAttribute.cs | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 Microsoft.Toolkit.Mvvm/Input/Attributes/ICommandAttribute.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index a6c7f5fa143..c075395e6c8 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -19,6 +19,7 @@ + diff --git a/Microsoft.Toolkit.Mvvm/Input/Attributes/ICommandAttribute.cs b/Microsoft.Toolkit.Mvvm/Input/Attributes/ICommandAttribute.cs new file mode 100644 index 00000000000..3ece775a72a --- /dev/null +++ b/Microsoft.Toolkit.Mvvm/Input/Attributes/ICommandAttribute.cs @@ -0,0 +1,68 @@ +// 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. + +#pragma warning disable CS1574 + +using System; +using System.Windows.Input; + +namespace Microsoft.Toolkit.Mvvm.Input +{ + /// + /// An attribute that can be used to automatically generate properties from declared methods. When this attribute + /// is used to decorate a method, a generator will create a command property with the corresponding interface + /// depending on the signature of the method. If an invalid method signature is used, the generator will report an error. + /// + /// In order to use this attribute, the containing type doesn't need to implement any interfaces. The generated properties will be lazily + /// assigned but their value will never change, so there is no need to support property change notifications or other additional functionality. + /// + /// + /// This attribute can be used as follows: + /// + /// partial class MyViewModel + /// { + /// [ICommand] + /// private void GreetUser(User? user) + /// { + /// Console.WriteLine($"Hello {user.Name}!"); + /// } + /// } + /// + /// And with this, code analogous to this will be generated: + /// + /// partial class MyViewModel + /// { + /// private IRelayCommand? greetUserCommand; + /// + /// public IRelayCommand GreetUserCommand => greetUserCommand ??= new RelayCommand(GreetUser); + /// } + /// + /// + /// + /// The following signatures are supported for annotated methods: + /// + /// void Method(); + /// + /// Will generate an property (using a instance). + /// + /// void Method(T?); + /// + /// Will generate an property (using a instance). + /// + /// Task Method(); + /// Task Method(CancellationToken); + /// + /// Will both generate an property (using an instance). + /// + /// Task Method(T?); + /// Task Method(T?, CancellationToken); + /// + /// Will both generate an property (using an instance). + /// + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + public sealed class ICommandAttribute : Attribute + { + } +} From 2eacae78960cd54432ea2f2e7c376beb11bcb54e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 Mar 2021 21:49:35 +0200 Subject: [PATCH 67/89] Initial draft implementation of ICommandGenerator --- .../ObservablePropertyGenerator.cs | 2 +- .../Input/ICommandGenerator.SyntaxReceiver.cs | 45 +++++ .../Input/ICommandGenerator.cs | 172 ++++++++++++++++++ 3 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs create mode 100644 Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 35648df3658..b731c476649 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -324,7 +324,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( PropertyDeclaration(IdentifierName(typeName), Identifier(propertyName)) .AddAttributeLists( AttributeList(SingletonSeparatedList( - Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")) + Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservablePropertyGenerator).Assembly.GetName().Version.ToString())))))), diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs new file mode 100644 index 00000000000..f9a926b26d9 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs @@ -0,0 +1,45 @@ +// 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.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.Toolkit.Mvvm.Input; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + public sealed partial class ICommandGenerator + { + /// + /// An that selects candidate nodes to process. + /// + private sealed class SyntaxReceiver : ISyntaxContextReceiver + { + /// + /// The list of info gathered during exploration. + /// + private readonly List gatheredInfo = new(); + + /// + /// Gets the collection of gathered info to process. + /// + public IReadOnlyCollection GatheredInfo => this.gatheredInfo; + + /// + public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + { + if (context.Node is MethodDeclarationSyntax methodDeclaration && + context.SemanticModel.GetDeclaredSymbol(methodDeclaration) is IMethodSymbol methodSymbol && + context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(ICommandAttribute).FullName) is INamedTypeSymbol iCommandSymbol && + methodSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, iCommandSymbol))) + { + this.gatheredInfo.Add(methodSymbol); + } + } + } + } +} diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs new file mode 100644 index 00000000000..586fb2aa8c7 --- /dev/null +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -0,0 +1,172 @@ +// 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.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using Microsoft.Toolkit.Mvvm.Input; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; + +namespace Microsoft.Toolkit.Mvvm.SourceGenerators +{ + /// + /// A source generator for generating command properties from annotated methods. + /// + [Generator] + public sealed partial class ICommandGenerator : ISourceGenerator + { + /// + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver()); + } + + /// + public void Execute(GeneratorExecutionContext context) + { + // Get the syntax receiver with the candidate nodes + if (context.SyntaxContextReceiver is not SyntaxReceiver syntaxReceiver || + syntaxReceiver.GatheredInfo.Count == 0) + { + return; + } + + foreach (var items in syntaxReceiver.GatheredInfo.GroupBy(static item => item.ContainingType, SymbolEqualityComparer.Default)) + { + if (items.Key.DeclaringSyntaxReferences.Length > 0 && + items.Key.DeclaringSyntaxReferences.First().GetSyntax() is ClassDeclarationSyntax classDeclaration) + { + try + { + OnExecute(context, classDeclaration, items.Key, items); + } + catch + { + // TODO + } + } + } + } + + /// + /// Processes a given target type. + /// + /// The input instance to use. + /// The node to process. + /// The for . + /// The sequence of instances to process. + private static void OnExecute( + GeneratorExecutionContext context, + ClassDeclarationSyntax classDeclaration, + INamedTypeSymbol classDeclarationSymbol, + IEnumerable methodSymbols) + { + // Create the class declaration for the user type. This will produce a tree as follows: + // + // + // { + // + // } + var classDeclarationSyntax = + ClassDeclaration(classDeclarationSymbol.Name) + .WithModifiers(classDeclaration.Modifiers) + .AddMembers(methodSymbols.Select(item => CreateCommandMembers(context, default, item)).SelectMany(static g => g).ToArray()); + + TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; + + // Add all parent types in ascending order, if any + foreach (var parentType in classDeclaration.Ancestors().OfType()) + { + typeDeclarationSyntax = parentType + .WithMembers(SingletonList(typeDeclarationSyntax)) + .WithConstraintClauses(List()) + .WithBaseList(null) + .WithAttributeLists(List()) + .WithoutTrivia(); + } + + // Create the compilation unit with the namespace and target member. + // From this, we can finally generate the source code to output. + var namespaceName = classDeclarationSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)); + + // Create the final compilation unit to generate (with leading trivia) + var source = + CompilationUnit().AddMembers( + NamespaceDeclaration(IdentifierName(namespaceName)).WithLeadingTrivia(TriviaList( + Comment("// Licensed to the .NET Foundation under one or more agreements."), + Comment("// The .NET Foundation licenses this file to you under the MIT license."), + Comment("// See the LICENSE file in the project root for more information."), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))) + .AddMembers(typeDeclarationSyntax)) + .NormalizeWhitespace() + .ToFullString(); + + // Add the partial type + context.AddSource($"[{typeof(ICommandAttribute).Name}]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + } + + /// + /// Creates the instances for a specified command. + /// + /// The input instance to use. + /// The leading trivia for the field to process. + /// The input instance to process. + /// The instances for the input command. + [Pure] + private static IEnumerable CreateCommandMembers(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IMethodSymbol methodSymbol) + { + // Construct the generated field as follows: + // + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // private ? ; + FieldDeclarationSyntax fieldDeclaration = + FieldDeclaration( + VariableDeclaration(NullableType(IdentifierName("IRelayCommand"))) + .AddVariables(VariableDeclarator(Identifier("fooCommand")))) + .AddModifiers(Token(SyntaxKind.PrivateKeyword)) + .AddAttributeLists(AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString()))))))); + + // Construct the generated property as follows: + // + // + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // [global::System.Diagnostics.DebuggerNonUserCode] + // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + // public => ??= new (new ()); + PropertyDeclarationSyntax propertyDeclaration = + PropertyDeclaration(IdentifierName("IRelayCommand"), Identifier("FooCommand")) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString())))))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) + .WithExpressionBody( + ArrowExpressionClause( + AssignmentExpression( + SyntaxKind.CoalesceAssignmentExpression, + IdentifierName("fooCommand"), + ObjectCreationExpression(IdentifierName("RelayCommand")) + .AddArgumentListArguments(Argument( + ObjectCreationExpression(IdentifierName("Action")) + .AddArgumentListArguments(Argument(IdentifierName("Foo")))))))); + + return new MemberDeclarationSyntax[] { fieldDeclaration, propertyDeclaration }; + } + } +} From 44762f4a230b84f00881ebfb1c2f60c3c2533b39 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 Mar 2021 22:23:11 +0200 Subject: [PATCH 68/89] Enabled ICommandGenerator for RelayCommand type --- .../Input/ICommandGenerator.cs | 71 ++++++++++++++++--- .../Mvvm/Test_ICommandAttribute.cs | 40 +++++++++++ 2 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs index 586fb2aa8c7..740a4f325a4 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -123,14 +123,27 @@ private static void OnExecute( [Pure] private static IEnumerable CreateCommandMembers(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IMethodSymbol methodSymbol) { + // Get the command member names + string + propertyName = methodSymbol.Name + "Command", + fieldName = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1)}"; + + // Get the command type symbols + ExtractCommandTypesFromMethod( + context, + methodSymbol, + out ITypeSymbol commandInterfaceTypeSymbol, + out ITypeSymbol commandClassTypeSymbol, + out ITypeSymbol delegateTypeSymbol); + // Construct the generated field as follows: // // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // private ? ; FieldDeclarationSyntax fieldDeclaration = FieldDeclaration( - VariableDeclaration(NullableType(IdentifierName("IRelayCommand"))) - .AddVariables(VariableDeclarator(Identifier("fooCommand")))) + VariableDeclaration(NullableType(IdentifierName(commandInterfaceTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) + .AddVariables(VariableDeclarator(Identifier(fieldName)))) .AddModifiers(Token(SyntaxKind.PrivateKeyword)) .AddAttributeLists(AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) @@ -138,15 +151,17 @@ private static IEnumerable CreateCommandMembers(Generat AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString()))))))); - // Construct the generated property as follows: + // Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts): // - // + // // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] // [global::System.Diagnostics.DebuggerNonUserCode] // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] // public => ??= new (new ()); PropertyDeclarationSyntax propertyDeclaration = - PropertyDeclaration(IdentifierName("IRelayCommand"), Identifier("FooCommand")) + PropertyDeclaration( + IdentifierName(commandInterfaceTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + Identifier(propertyName)) .AddModifiers(Token(SyntaxKind.PublicKeyword)) .AddAttributeLists( AttributeList(SingletonSeparatedList( @@ -160,13 +175,51 @@ private static IEnumerable CreateCommandMembers(Generat ArrowExpressionClause( AssignmentExpression( SyntaxKind.CoalesceAssignmentExpression, - IdentifierName("fooCommand"), - ObjectCreationExpression(IdentifierName("RelayCommand")) + IdentifierName(fieldName), + ObjectCreationExpression(IdentifierName(commandClassTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) .AddArgumentListArguments(Argument( - ObjectCreationExpression(IdentifierName("Action")) - .AddArgumentListArguments(Argument(IdentifierName("Foo")))))))); + ObjectCreationExpression(IdentifierName(delegateTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) + .AddArgumentListArguments(Argument(IdentifierName(methodSymbol.Name)))))))) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)); return new MemberDeclarationSyntax[] { fieldDeclaration, propertyDeclaration }; } + + /// + /// Gets the type symbols for the input method, if supported. + /// + /// The input instance to use. + /// The input instance to process. + /// The command interface type symbol. + /// The command class type symbol. + /// The delegate type symbol for the wrapped method. + private static void ExtractCommandTypesFromMethod( + GeneratorExecutionContext context, + IMethodSymbol methodSymbol, + out ITypeSymbol commandInterfaceTypeSymbol, + out ITypeSymbol commandClassTypeSymbol, + out ITypeSymbol delegateTypeSymbol) + { + if (methodSymbol.ReturnsVoid && methodSymbol.Parameters.Length == 0) + { + commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IRelayCommand")!; + commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.RelayCommand")!; + delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Action")!; + } + else if (methodSymbol.ReturnsVoid && + methodSymbol.Parameters.Length == 1 && + methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None } parameter) + { + commandInterfaceTypeSymbol = null; + commandClassTypeSymbol = null; + delegateTypeSymbol = null; + } + else + { + commandInterfaceTypeSymbol = null; + commandClassTypeSymbol = null; + delegateTypeSymbol = null; + } + } } } diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs new file mode 100644 index 00000000000..cc48f747db8 --- /dev/null +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs @@ -0,0 +1,40 @@ +// 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. + +#pragma warning disable CS0618 + +using System; +using Microsoft.Toolkit.Mvvm.Input; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.Mvvm +{ + [TestClass] + public partial class Test_ICommandAttribute + { + [TestCategory("Mvvm")] + [TestMethod] + public void Test_ICommandAttribute_RelayCommand() + { + var model = new MyViewModel(); + + Assert.AreEqual(model.Counter, 0); + + model.IncrementCounterCommand.Execute(null); + + Assert.AreEqual(model.Counter, 1); + } + + public sealed partial class MyViewModel + { + public int Counter { get; private set; } + + [ICommand] + private void IncrementCounter() + { + Counter++; + } + } + } +} From 6def7747d39b871fe9cffe0cda933757d8936744 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 Mar 2021 22:59:03 +0200 Subject: [PATCH 69/89] Enabled [ICommand] generation for all command types --- .../Input/ICommandGenerator.cs | 103 +++++++++++++++--- .../Mvvm/Test_ICommandAttribute.cs | 61 ++++++++++- 2 files changed, 145 insertions(+), 19 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs index 740a4f325a4..b2e4fce5676 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -2,7 +2,9 @@ // 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.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Linq; using System.Text; @@ -129,12 +131,15 @@ private static IEnumerable CreateCommandMembers(Generat fieldName = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1)}"; // Get the command type symbols - ExtractCommandTypesFromMethod( + if (!TryMapCommandTypesFromMethod( context, methodSymbol, - out ITypeSymbol commandInterfaceTypeSymbol, - out ITypeSymbol commandClassTypeSymbol, - out ITypeSymbol delegateTypeSymbol); + out ITypeSymbol? commandInterfaceTypeSymbol, + out ITypeSymbol? commandClassTypeSymbol, + out ITypeSymbol? delegateTypeSymbol)) + { + return Array.Empty(); + } // Construct the generated field as follows: // @@ -193,33 +198,95 @@ private static IEnumerable CreateCommandMembers(Generat /// The command interface type symbol. /// The command class type symbol. /// The delegate type symbol for the wrapped method. - private static void ExtractCommandTypesFromMethod( + /// Whether or not was valid and the requested types have been set. + private static bool TryMapCommandTypesFromMethod( GeneratorExecutionContext context, IMethodSymbol methodSymbol, - out ITypeSymbol commandInterfaceTypeSymbol, - out ITypeSymbol commandClassTypeSymbol, - out ITypeSymbol delegateTypeSymbol) + [NotNullWhen(true)] out ITypeSymbol? commandInterfaceTypeSymbol, + [NotNullWhen(true)] out ITypeSymbol? commandClassTypeSymbol, + [NotNullWhen(true)] out ITypeSymbol? delegateTypeSymbol) { + // Map to IRelayCommand, RelayCommand, Action if (methodSymbol.ReturnsVoid && methodSymbol.Parameters.Length == 0) { commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IRelayCommand")!; commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.RelayCommand")!; delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Action")!; + + return true; } - else if (methodSymbol.ReturnsVoid && - methodSymbol.Parameters.Length == 1 && - methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None } parameter) + + // Map to IRelayCommand, RelayCommand, Action + if (methodSymbol.ReturnsVoid && + methodSymbol.Parameters.Length == 1 && + methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } parameter) { - commandInterfaceTypeSymbol = null; - commandClassTypeSymbol = null; - delegateTypeSymbol = null; + commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IRelayCommand`1")!.Construct(parameter.Type); + commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.RelayCommand`1")!.Construct(parameter.Type); + delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Action`1")!.Construct(parameter.Type); + + return true; } - else + + if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!)) { - commandInterfaceTypeSymbol = null; - commandClassTypeSymbol = null; - delegateTypeSymbol = null; + // Map to IAsyncRelayCommand, AsyncRelayCommand, Func + if (methodSymbol.Parameters.Length == 0) + { + commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IAsyncRelayCommand")!; + commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand")!; + delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`1")!.Construct(context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!); + + return true; + } + + if (methodSymbol.Parameters.Length == 1 && + methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } singleParameter) + { + // Map to IAsyncRelayCommand, AsyncRelayCommand, Func + if (SymbolEqualityComparer.Default.Equals(singleParameter.Type, context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!)) + { + commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IAsyncRelayCommand")!; + commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand")!; + delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`2")!.Construct( + context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!, + context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!); + + return true; + } + + // Map to IAsyncRelayCommand, AsyncRelayCommand, Func + commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IAsyncRelayCommand`1")!.Construct(singleParameter.Type); + commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand`1")!.Construct(singleParameter.Type); + delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`2")!.Construct( + singleParameter.Type, + context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!); + + return true; + } + + // Map to IAsyncRelayCommand, AsyncRelayCommand, Func + if (methodSymbol.Parameters.Length == 2 && + methodSymbol.Parameters[0] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } firstParameter && + methodSymbol.Parameters[1] is IParameterSymbol { RefKind: RefKind.None, Type: { IsRefLikeType: false, TypeKind: not TypeKind.Pointer and not TypeKind.FunctionPointer } } secondParameter && + SymbolEqualityComparer.Default.Equals(secondParameter.Type, context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken")!)) + { + commandInterfaceTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.IAsyncRelayCommand`1")!.Construct(firstParameter.Type); + commandClassTypeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.AsyncRelayCommand`1")!.Construct(firstParameter.Type); + delegateTypeSymbol = context.Compilation.GetTypeByMetadataName("System.Func`3")!.Construct( + firstParameter.Type, + secondParameter.Type, + context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!); + + return true; + } } + + commandInterfaceTypeSymbol = null; + commandClassTypeSymbol = null; + delegateTypeSymbol = null; + + return false; } } } diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs index cc48f747db8..ae054a426cb 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs @@ -4,7 +4,8 @@ #pragma warning disable CS0618 -using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Toolkit.Mvvm.Input; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -24,6 +25,26 @@ public void Test_ICommandAttribute_RelayCommand() model.IncrementCounterCommand.Execute(null); Assert.AreEqual(model.Counter, 1); + + model.IncrementCounterWithValueCommand.Execute(5); + + Assert.AreEqual(model.Counter, 6); + + model.IncrementCounterAsyncCommand.Execute(null); + + Assert.AreEqual(model.Counter, 7); + + model.IncrementCounterWithTokenAsyncCommand.Execute(null); + + Assert.AreEqual(model.Counter, 8); + + model.IncrementCounterWithValueAsyncCommand.Execute(5); + + Assert.AreEqual(model.Counter, 13); + + model.IncrementCounterWithValueAndTokenAsyncCommand.Execute(5); + + Assert.AreEqual(model.Counter, 18); } public sealed partial class MyViewModel @@ -35,6 +56,44 @@ private void IncrementCounter() { Counter++; } + + [ICommand] + private void IncrementCounterWithValue(int count) + { + Counter += count; + } + + [ICommand] + private Task IncrementCounterAsync() + { + Counter += 1; + + return Task.CompletedTask; + } + + [ICommand] + private Task IncrementCounterWithTokenAsync(CancellationToken token) + { + Counter += 1; + + return Task.CompletedTask; + } + + [ICommand] + private Task IncrementCounterWithValueAsync(int count) + { + Counter += count; + + return Task.CompletedTask; + } + + [ICommand] + private Task IncrementCounterWithValueAndTokenAsync(int count, CancellationToken token) + { + Counter += count; + + return Task.CompletedTask; + } } } } From 56f386daae35bf6dbef8b03a7d8ce77a420dfbbf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 1 Apr 2021 01:10:44 +0200 Subject: [PATCH 70/89] Stripped Async suffix to generated command names --- .../Input/ICommandGenerator.cs | 28 +++++++++++++-- .../Mvvm/Test_ICommandAttribute.cs | 34 +++++++++---------- .../Mvvm/Test_ObservablePropertyAttribute.cs | 1 - 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs index b2e4fce5676..f91cd9be7d0 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -126,9 +126,7 @@ private static void OnExecute( private static IEnumerable CreateCommandMembers(GeneratorExecutionContext context, SyntaxTriviaList leadingTrivia, IMethodSymbol methodSymbol) { // Get the command member names - string - propertyName = methodSymbol.Name + "Command", - fieldName = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1)}"; + var (fieldName, propertyName) = GetGeneratedFieldAndPropertyNames(context, methodSymbol); // Get the command type symbols if (!TryMapCommandTypesFromMethod( @@ -190,6 +188,30 @@ private static IEnumerable CreateCommandMembers(Generat return new MemberDeclarationSyntax[] { fieldDeclaration, propertyDeclaration }; } + /// + /// Get the generated field and property names for the input method. + /// + /// The input instance to use. + /// The input instance to process. + /// The generated field and property names for . + [Pure] + private static (string FieldName, string PropertyName) GetGeneratedFieldAndPropertyNames(GeneratorExecutionContext context, IMethodSymbol methodSymbol) + { + string propertyName = methodSymbol.Name; + + if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!) && + methodSymbol.Name.EndsWith("Async")) + { + propertyName = propertyName.Substring(0, propertyName.Length - "Async".Length); + } + + propertyName += "Command"; + + string fieldName = $"{char.ToLower(propertyName[0])}{propertyName.Substring(1)}"; + + return (fieldName, propertyName); + } + /// /// Gets the type symbols for the input method, if supported. /// diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs index ae054a426cb..b3691a2abd0 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs @@ -16,7 +16,7 @@ public partial class Test_ICommandAttribute { [TestCategory("Mvvm")] [TestMethod] - public void Test_ICommandAttribute_RelayCommand() + public async Task Test_ICommandAttribute_RelayCommand() { var model = new MyViewModel(); @@ -30,19 +30,19 @@ public void Test_ICommandAttribute_RelayCommand() Assert.AreEqual(model.Counter, 6); - model.IncrementCounterAsyncCommand.Execute(null); + await model.DelayAndIncrementCounterCommand.ExecuteAsync(null); Assert.AreEqual(model.Counter, 7); - model.IncrementCounterWithTokenAsyncCommand.Execute(null); + await model.DelayAndIncrementCounterWithTokenCommand.ExecuteAsync(null); Assert.AreEqual(model.Counter, 8); - model.IncrementCounterWithValueAsyncCommand.Execute(5); + await model.DelayAndIncrementCounterWithValueCommand.ExecuteAsync(5); Assert.AreEqual(model.Counter, 13); - model.IncrementCounterWithValueAndTokenAsyncCommand.Execute(5); + await model.DelayAndIncrementCounterWithValueAndTokenCommand.ExecuteAsync(5); Assert.AreEqual(model.Counter, 18); } @@ -64,35 +64,35 @@ private void IncrementCounterWithValue(int count) } [ICommand] - private Task IncrementCounterAsync() + private async Task DelayAndIncrementCounterAsync() { - Counter += 1; + await Task.Delay(50); - return Task.CompletedTask; + Counter += 1; } [ICommand] - private Task IncrementCounterWithTokenAsync(CancellationToken token) + private async Task DelayAndIncrementCounterWithTokenAsync(CancellationToken token) { - Counter += 1; + await Task.Delay(50); - return Task.CompletedTask; + Counter += 1; } [ICommand] - private Task IncrementCounterWithValueAsync(int count) + private async Task DelayAndIncrementCounterWithValueAsync(int count) { - Counter += count; + await Task.Delay(50); - return Task.CompletedTask; + Counter += count; } [ICommand] - private Task IncrementCounterWithValueAndTokenAsync(int count, CancellationToken token) + private async Task DelayAndIncrementCounterWithValueAndTokenAsync(int count, CancellationToken token) { - Counter += count; + await Task.Delay(50); - return Task.CompletedTask; + Counter += count; } } } diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs index 196381672ad..06719d6a628 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs @@ -60,7 +60,6 @@ public void Test_AlsoNotifyForAttribute_Events() { var model = new DependentPropertyModel(); - (PropertyChangedEventArgs, int) changed = default; List propertyNames = new(); model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); From 5a08c7afd774d0c11630425b1f85515a541fb58b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 1 Apr 2021 16:12:31 +0200 Subject: [PATCH 71/89] Added ICommandGenerator diagnostics --- .../AnalyzerReleases.Unshipped.md | 4 ++- .../Diagnostics/DiagnosticDescriptors.cs | 35 ++++++++++++++++++- .../Input/ICommandGenerator.cs | 6 +++- .../Test_SourceGeneratorsDiagnostics.cs | 19 ++++++++++ 4 files changed, 61 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md index 70332985966..8218a8e88fc 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -14,4 +14,6 @@ MVVMTK0006 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | MVVMTK0007 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit MVVMTK0008 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit MVVMTK0009 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0010 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit \ No newline at end of file +MVVMTK0010 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0011 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0012 | Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit \ No newline at end of file diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index f06879c4737..bf454c6e8f2 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -5,6 +5,7 @@ using System.ComponentModel; using Microsoft.CodeAnalysis; using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Input; namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics { @@ -150,7 +151,7 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor MissingObservableValidatorInheritanceError = new( id: "MVVMTK0009", title: "Missing ObservableValidator inheritance", - messageFormat: $"The field {{0}}.{{1}} cannot be used to generate an observable property, as it has {{2}} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator", + messageFormat: "The field {0}.{1} cannot be used to generate an observable property, as it has {2} validation attribute(s) but is declared in a type that doesn't inherit from ObservableValidator", category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, @@ -172,5 +173,37 @@ internal static class DiagnosticDescriptors isEnabledByDefault: true, description: $"The {nameof(ObservablePropertyGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.", helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when failed to run on a given type. + /// + /// Format: "The generator ICommandGenerator failed to execute on type {0}". + /// + /// + public static readonly DiagnosticDescriptor ICommandGeneratorError = new( + id: "MVVMTK0011", + title: $"Internal error for {nameof(ICommandGenerator)}", + messageFormat: $"The generator {nameof(ICommandGenerator)} failed to execute on type {{0}}", + category: typeof(ICommandGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"The {nameof(ICommandGenerator)} generator encountered an error while processing a type. Please report this issue at https://aka.ms/mvvmtoolkit.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); + + /// + /// Gets a indicating when an annotated method to generate a command for has an invalid signature. + /// + /// Format: "The method {0}.{1} cannot be used to generate a command property, as its signature isn't compatible with any of the existing relay command types". + /// + /// + public static readonly DiagnosticDescriptor InvalidICommandMethodSignatureError = new( + id: "MVVMTK0012", + title: "Invalid ICommand method signature", + messageFormat: "The method {0}.{1} cannot be used to generate a command property, as its signature isn't compatible with any of the existing relay command types", + category: typeof(ICommandGenerator).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: $"Cannot apply [{nameof(ICommandAttribute)}] to methods with a signature that doesn't match any of the existing relay command types.", + helpLinkUri: "https://aka.ms/mvvmtoolkit"); } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs index f91cd9be7d0..17336761feb 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -13,9 +13,11 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using Microsoft.Toolkit.Mvvm.Input; +using Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics; using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; +using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -52,7 +54,7 @@ public void Execute(GeneratorExecutionContext context) } catch { - // TODO + context.ReportDiagnostic(ICommandGeneratorError, classDeclaration, items.Key); } } } @@ -136,6 +138,8 @@ private static IEnumerable CreateCommandMembers(Generat out ITypeSymbol? commandClassTypeSymbol, out ITypeSymbol? delegateTypeSymbol)) { + context.ReportDiagnostic(InvalidICommandMethodSignatureError, methodSymbol, methodSymbol.ContainingType, methodSymbol); + return Array.Empty(); } diff --git a/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs b/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs index 3041a53fb47..1df3ac3ab44 100644 --- a/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs +++ b/UnitTests/UnitTests.SourceGenerators/Test_SourceGeneratorsDiagnostics.cs @@ -221,6 +221,25 @@ public partial class SampleViewModel VerifyGeneratedDiagnostics(source, "MVVMTK0009"); } + [TestCategory("Mvvm")] + [TestMethod] + public void InvalidICommandMethodSignatureError() + { + string source = @" + using Microsoft.Toolkit.Mvvm.Input; + + namespace MyApp + { + public partial class SampleViewModel + { + [ICommand] + private string GreetUser() => ""Hello world!""; + } + }"; + + VerifyGeneratedDiagnostics(source, "MVVMTK0012"); + } + /// /// Verifies the output of a source generator. /// From 2e8149f2a3750532066e8a20ec0d96e00393e721 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Apr 2021 20:32:54 +0200 Subject: [PATCH 72/89] Added support for XML docs for generated commands --- .../Input/ICommandGenerator.SyntaxReceiver.cs | 13 ++++++-- .../Input/ICommandGenerator.cs | 33 ++++++++++++++++--- .../Mvvm/Test_ICommandAttribute.cs | 13 ++++++++ 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs index f9a926b26d9..d4d73358f5e 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs @@ -22,12 +22,12 @@ private sealed class SyntaxReceiver : ISyntaxContextReceiver /// /// The list of info gathered during exploration. /// - private readonly List gatheredInfo = new(); + private readonly List gatheredInfo = new(); /// /// Gets the collection of gathered info to process. /// - public IReadOnlyCollection GatheredInfo => this.gatheredInfo; + public IReadOnlyCollection GatheredInfo => this.gatheredInfo; /// public void OnVisitSyntaxNode(GeneratorSyntaxContext context) @@ -37,9 +37,16 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(ICommandAttribute).FullName) is INamedTypeSymbol iCommandSymbol && methodSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, iCommandSymbol))) { - this.gatheredInfo.Add(methodSymbol); + this.gatheredInfo.Add(new Item(methodDeclaration.GetLeadingTrivia(), methodSymbol)); } } + + /// + /// A model for a group of item representing a discovered type to process. + /// + /// The leading trivia for the field declaration. + /// The instance for the target method. + public sealed record Item(SyntaxTriviaList LeadingTrivia, IMethodSymbol MethodSymbol); } } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs index 17336761feb..38d1f37d432 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -8,6 +8,7 @@ using System.Diagnostics.Contracts; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -43,7 +44,7 @@ public void Execute(GeneratorExecutionContext context) return; } - foreach (var items in syntaxReceiver.GatheredInfo.GroupBy(static item => item.ContainingType, SymbolEqualityComparer.Default)) + foreach (var items in syntaxReceiver.GatheredInfo.GroupBy(static item => item.MethodSymbol.ContainingType, SymbolEqualityComparer.Default)) { if (items.Key.DeclaringSyntaxReferences.Length > 0 && items.Key.DeclaringSyntaxReferences.First().GetSyntax() is ClassDeclarationSyntax classDeclaration) @@ -66,12 +67,12 @@ public void Execute(GeneratorExecutionContext context) /// The input instance to use. /// The node to process. /// The for . - /// The sequence of instances to process. + /// The sequence of instances to process. private static void OnExecute( GeneratorExecutionContext context, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, - IEnumerable methodSymbols) + IEnumerable items) { // Create the class declaration for the user type. This will produce a tree as follows: // @@ -82,7 +83,7 @@ private static void OnExecute( var classDeclarationSyntax = ClassDeclaration(classDeclarationSymbol.Name) .WithModifiers(classDeclaration.Modifiers) - .AddMembers(methodSymbols.Select(item => CreateCommandMembers(context, default, item)).SelectMany(static g => g).ToArray()); + .AddMembers(items.Select(item => CreateCommandMembers(context, item.LeadingTrivia, item.MethodSymbol)).SelectMany(static g => g).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -158,6 +159,27 @@ private static IEnumerable CreateCommandMembers(Generat AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString()))))))); + SyntaxTriviaList summaryTrivia = SyntaxTriviaList.Empty; + + // Parse the docs, if present + foreach (SyntaxTrivia trivia in leadingTrivia) + { + if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia) || + trivia.IsKind(SyntaxKind.SingleLineDocumentationCommentTrivia)) + { + string text = trivia.ToString(); + + Match match = Regex.Match(text, @".*?<\/summary>", RegexOptions.Singleline); + + if (match.Success) + { + summaryTrivia = TriviaList(Comment($"/// {match.Value}")); + + break; + } + } + } + // Construct the generated property as follows (the explicit delegate cast is needed to avoid overload resolution conflicts): // // @@ -175,7 +197,8 @@ private static IEnumerable CreateCommandMembers(Generat Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) .AddArgumentListArguments( AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString())))))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ICommandGenerator).Assembly.GetName().Version.ToString())))))) + .WithOpenBracketToken(Token(summaryTrivia, SyntaxKind.OpenBracketToken, TriviaList())), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) .WithExpressionBody( diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs index b3691a2abd0..2bcea9e41dd 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ICommandAttribute.cs @@ -51,18 +51,25 @@ public sealed partial class MyViewModel { public int Counter { get; private set; } + /// This is a single line summary. [ICommand] private void IncrementCounter() { Counter++; } + /// + /// This is a multiline summary + /// [ICommand] private void IncrementCounterWithValue(int count) { Counter += count; } + /// This is single line with also other stuff below + /// Foo bar baz + /// A task [ICommand] private async Task DelayAndIncrementCounterAsync() { @@ -71,6 +78,11 @@ private async Task DelayAndIncrementCounterAsync() Counter += 1; } + /// + /// This is multi line with also other stuff below + /// + /// Foo bar baz + /// A task [ICommand] private async Task DelayAndIncrementCounterWithTokenAsync(CancellationToken token) { @@ -79,6 +91,7 @@ private async Task DelayAndIncrementCounterWithTokenAsync(CancellationToken toke Counter += 1; } + // This should not be ported over [ICommand] private async Task DelayAndIncrementCounterWithValueAsync(int count) { From 27f429fa3e03b65c4a42b03fbf41ee5a21df56f7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Apr 2021 23:37:08 +0200 Subject: [PATCH 73/89] Improved [ObservableProperty] codegen for ObservableObject --- .../ObservablePropertyGenerator.cs | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index b731c476649..fb65ee3b0a1 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -80,10 +80,11 @@ private static void OnExecute( // Check whether the current type implements INotifyPropertyChanging and whether it inherits from ObservableValidator bool + isObservableObject = classDeclarationSymbol.InheritsFrom(observableObjectSymbol), isObservableValidator = classDeclarationSymbol.InheritsFrom(observableValidatorSymbol), isNotifyPropertyChanging = + isObservableObject || classDeclarationSymbol.AllInterfaces.Contains(iNotifyPropertyChangingSymbol, SymbolEqualityComparer.Default) || - classDeclarationSymbol.InheritsFrom(observableObjectSymbol) || classDeclarationSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observableObjectAttributeSymbol)); // Create the class declaration for the user type. This will produce a tree as follows: @@ -95,7 +96,14 @@ private static void OnExecute( var classDeclarationSyntax = ClassDeclaration(classDeclarationSymbol.Name) .WithModifiers(classDeclaration.Modifiers) - .AddMembers(items.Select(item => CreatePropertyDeclaration(context, item.LeadingTrivia, item.FieldSymbol, isNotifyPropertyChanging, isObservableValidator)).ToArray()); + .AddMembers(items.Select(item => + CreatePropertyDeclaration( + context, + item.LeadingTrivia, + item.FieldSymbol, + isNotifyPropertyChanging, + isObservableObject, + isObservableValidator)).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -137,6 +145,7 @@ private static void OnExecute( /// The leading trivia for the field to process. /// The input instance to process. /// Indicates whether or not is also implemented. + /// Indicates whether or not the containing type inherits from ObservableObject. /// Indicates whether or not the containing type inherits from ObservableValidator. /// A generated instance for the input field. [Pure] @@ -145,6 +154,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging, + bool isObservableObject, bool isObservableValidator) { // Get the field type and the target property name @@ -246,6 +256,33 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( _ => Block(IfStatement(setPropertyExpression, Block(dependentPropertyNotificationStatements))) }; } + else if (isObservableObject) + { + // Generate the inner setter block as follows: + // + // SetProperty(ref , value); + // + // Or in case there is at least one dependent property: + // + // if (SetProperty(ref , value)) + // { + // OnPropertyChanged("Property1"); // Optional + // OnPropertyChanged("Property2"); + // ... + // OnPropertyChanged("PropertyN"); + // } + InvocationExpressionSyntax setPropertyExpression = + InvocationExpression(IdentifierName("SetProperty")) + .AddArgumentListArguments( + Argument(IdentifierName(fieldSymbol.Name)).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), + Argument(IdentifierName("value"))); + + setterBlock = dependentPropertyNotificationStatements.Count switch + { + 0 => Block(ExpressionStatement(setPropertyExpression)), + _ => Block(IfStatement(setPropertyExpression, Block(dependentPropertyNotificationStatements))) + }; + } else { BlockSyntax updateAndNotificationBlock = Block(); From b733e6820d679a6a3ec017ee05d9647bdf265aec Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Apr 2021 15:48:04 +0200 Subject: [PATCH 74/89] Enabled nullability annotations for [ObservableProperty] --- .../ComponentModel/ObservablePropertyGenerator.cs | 12 ++++++++++-- .../Mvvm/Test_ObservablePropertyAttribute.cs | 14 ++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index fb65ee3b0a1..8d8e3396057 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -339,6 +339,14 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( updateAndNotificationBlock)); } + // Get the right type for the declared property (including nullability annotations) + TypeSyntax propertyType = IdentifierName(typeName); + + if (fieldSymbol.Type is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated }) + { + propertyType = NullableType(propertyType); + } + // Construct the generated property as follows: // // @@ -349,7 +357,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( // // ... // - // public + // public // { // get => ; // set @@ -358,7 +366,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( // } // } return - PropertyDeclaration(IdentifierName(typeName), Identifier(propertyName)) + PropertyDeclaration(propertyType, Identifier(propertyName)) .AddAttributeLists( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs index 06719d6a628..2a3eb6e3139 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs @@ -10,6 +10,8 @@ using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.VisualStudio.TestTools.UnitTesting; +#nullable enable + namespace UnitTests.Mvvm { [TestClass] @@ -128,11 +130,11 @@ public sealed partial class DependentPropertyModel { [ObservableProperty] [AlsoNotifyFor(nameof(FullName))] - private string name; + private string? name; [ObservableProperty] [AlsoNotifyFor(nameof(FullName))] - private string surname; + private string? surname; public string FullName => $"{Name} {Surname}"; } @@ -143,7 +145,7 @@ public partial class MyFormViewModel : ObservableValidator [Required] [MinLength(1)] [MaxLength(100)] - private string name; + private string? name; [ObservableProperty] [Range(0, 120)] @@ -151,7 +153,7 @@ public partial class MyFormViewModel : ObservableValidator [ObservableProperty] [EmailAddress] - private string email; + private string? email; [ObservableProperty] [TestValidation(null, typeof(SampleModel), true, 6.28, new[] { "Bob", "Ross" }, NestedArray = new object[] { 1, "Hello", new int[] { 2, 3, 4 } }, Animal = Animal.Llama)] @@ -160,7 +162,7 @@ public partial class MyFormViewModel : ObservableValidator private sealed class TestValidationAttribute : ValidationAttribute { - public TestValidationAttribute(object o, Type t, bool flag, double d, string[] names) + public TestValidationAttribute(object? o, Type t, bool flag, double d, string[] names) { O = o; T = t; @@ -169,7 +171,7 @@ public TestValidationAttribute(object o, Type t, bool flag, double d, string[] n Names = names; } - public object O { get; } + public object? O { get; } public Type T { get; } From 8567605dcbd55e7b7ed4b62d059fc54ca754b7d9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 11 Apr 2021 15:43:26 +0200 Subject: [PATCH 75/89] Initial support for cached property names --- .../ObservablePropertyGenerator.cs | 152 +++++++++++++++++- 1 file changed, 144 insertions(+), 8 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 8d8e3396057..689ff032f01 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -42,6 +42,12 @@ public void Execute(GeneratorExecutionContext context) return; } + // Sets of discovered property names + HashSet + propertyChangedNames = new(), + propertyChangingNames = new(); + + // Process the annotated fields foreach (var items in syntaxReceiver.GatheredInfo.GroupBy(static item => item.FieldSymbol.ContainingType, SymbolEqualityComparer.Default)) { if (items.Key.DeclaringSyntaxReferences.Length > 0 && @@ -49,7 +55,7 @@ public void Execute(GeneratorExecutionContext context) { try { - OnExecute(context, classDeclaration, items.Key, items); + OnExecuteForProperties(context, classDeclaration, items.Key, items, propertyChangedNames, propertyChangingNames); } catch { @@ -57,20 +63,27 @@ public void Execute(GeneratorExecutionContext context) } } } + + // Process the fields for the cached args + OnExecuteForPropertyArgs(context, propertyChangedNames, propertyChangingNames); } /// - /// Processes a given target type. + /// Processes a given target type for declared observable properties. /// /// The input instance to use. /// The node to process. /// The for . /// The sequence of fields to process. - private static void OnExecute( + /// The collection of discovered property changed names. + /// The collection of discovered property changing names. + private static void OnExecuteForProperties( GeneratorExecutionContext context, ClassDeclarationSyntax classDeclaration, INamedTypeSymbol classDeclarationSymbol, - IEnumerable items) + IEnumerable items, + ICollection propertyChangedNames, + ICollection propertyChangingNames) { INamedTypeSymbol iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanging).FullName)!, @@ -103,7 +116,9 @@ private static void OnExecute( item.FieldSymbol, isNotifyPropertyChanging, isObservableObject, - isObservableValidator)).ToArray()); + isObservableValidator, + propertyChangedNames, + propertyChangingNames)).ToArray()); TypeDeclarationSyntax typeDeclarationSyntax = classDeclarationSyntax; @@ -147,6 +162,8 @@ private static void OnExecute( /// Indicates whether or not is also implemented. /// Indicates whether or not the containing type inherits from ObservableObject. /// Indicates whether or not the containing type inherits from ObservableValidator. + /// The collection of discovered property changed names. + /// The collection of discovered property changing names. /// A generated instance for the input field. [Pure] private static PropertyDeclarationSyntax CreatePropertyDeclaration( @@ -155,7 +172,9 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging, bool isObservableObject, - bool isObservableValidator) + bool isObservableValidator, + ICollection propertyChangedNames, + ICollection propertyChangingNames) { // Get the field type and the target property name string @@ -183,10 +202,15 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( if (attributeArgument.Kind == TypedConstantKind.Primitive && attributeArgument.Value is string dependentPropertyName) { + propertyChangedNames.Add(dependentPropertyName); + // OnPropertyChanged("OtherPropertyName"); dependentPropertyNotificationStatements.Add(ExpressionStatement( InvocationExpression(IdentifierName("OnPropertyChanged")) - .AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(dependentPropertyName)))))); + .AddArgumentListArguments(Argument(MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs")), + IdentifierName($"{dependentPropertyName}{nameof(PropertyChangedEventArgs)}")))))); } else if (attributeArgument.Kind == TypedConstantKind.Array) { @@ -197,10 +221,17 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( continue; } + string currentPropertyName = (string)nestedAttributeArgument.Value!; + + propertyChangedNames.Add(currentPropertyName); + // Additional property names dependentPropertyNotificationStatements.Add(ExpressionStatement( InvocationExpression(IdentifierName("OnPropertyChanged")) - .AddArgumentListArguments(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal((string)nestedAttributeArgument.Value!)))))); + .AddArgumentListArguments(Argument(MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs")), + IdentifierName($"{currentPropertyName}{nameof(PropertyChangedEventArgs)}")))))); } } } @@ -407,5 +438,110 @@ private static string GetGeneratedPropertyName(IFieldSymbol fieldSymbol) return $"{char.ToUpper(propertyName[0])}{propertyName.Substring(1)}"; } + + /// + /// Processes the cached property changed/changing args. + /// + /// The input instance to use. + /// The collection of discovered property changed names. + /// The collection of discovered property changing names. + public void OnExecuteForPropertyArgs(GeneratorExecutionContext context, IReadOnlyCollection propertyChangedNames, IReadOnlyCollection propertyChangingNames) + { + if (propertyChangedNames.Count == 0 && + propertyChangingNames.Count == 0) + { + return; + } + + static FieldDeclarationSyntax CreateFieldDeclaration(INamedTypeSymbol type, string propertyName) + { + // Create a static field with a cached property changed/changing argument for a specified property. + // This code produces a field declaration as follows: + // + // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + // [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + // public static readonly = new(""); + return + FieldDeclaration( + VariableDeclaration(IdentifierName(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) + .AddVariables( + VariableDeclarator(Identifier($"{propertyName}{type.Name}")) + .WithInitializer(EqualsValueClause( + ImplicitObjectCreationExpression() + .AddArgumentListArguments(Argument( + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName)))))))) + .AddModifiers( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword), + Token(SyntaxKind.ReadOnlyKeyword)) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( + AttributeArgument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal("This field is not intended to be referenced directly by user code"))))))); + } + + INamedTypeSymbol + propertyChangedEventArgsSymbol = context.Compilation.GetTypeByMetadataName(typeof(PropertyChangedEventArgs).FullName)!, + propertyChangingEventArgsSymbol = context.Compilation.GetTypeByMetadataName(typeof(PropertyChangingEventArgs).FullName)!; + + // Create a static method to validate all properties in a given class. + // This code takes a class symbol and produces a compilation unit as follows: + // + // // 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. + // + // #pragma warning disable + // + // namespace Microsoft.Toolkit.Mvvm.ComponentModel.__Internals + // { + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // [global::System.Diagnostics.DebuggerNonUserCode] + // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + // [global::System.Obsolete("This type is not intended to be used directly by user code")] + // internal static class __KnownINotifyPropertyChangedOrChangingArgs + // { + // + // } + // } + var source = + CompilationUnit().AddMembers( + NamespaceDeclaration(IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals")).WithLeadingTrivia(TriviaList( + Comment("// Licensed to the .NET Foundation under one or more agreements."), + Comment("// The .NET Foundation licenses this file to you under the MIT license."), + Comment("// See the LICENSE file in the project root for more information."), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)))).AddMembers( + ClassDeclaration("__KnownINotifyPropertyChangedOrChangingArgs").AddModifiers( + Token(SyntaxKind.InternalKeyword), + Token(SyntaxKind.StaticKeyword)).AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName($"global::System.CodeDom.Compiler.GeneratedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(GetType().Assembly.GetName().Version.ToString())))))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.DebuggerNonUserCode")))), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( + AttributeArgument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal("This type is not intended to be used directly by user code"))))))) + .AddMembers(propertyChangedNames.Select(name => CreateFieldDeclaration(propertyChangedEventArgsSymbol, name)).ToArray()) + .AddMembers(propertyChangingNames.Select(name => CreateFieldDeclaration(propertyChangingEventArgsSymbol, name)).ToArray()))) + .NormalizeWhitespace() + .ToFullString(); + + // Add the partial type + context.AddSource($"[{typeof(ObservablePropertyAttribute).Name}]_[__KnownINotifyPropertyChangedOrChangingArgs].cs", SourceText.From(source, Encoding.UTF8)); + } } } From 386276c800ae90d6f8d6fc3aedb602ce2af477bd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 11 Apr 2021 15:54:29 +0200 Subject: [PATCH 76/89] Enabled property args caching for non dependent names --- .../ObservablePropertyGenerator.cs | 68 +++++++------------ 1 file changed, 26 insertions(+), 42 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 689ff032f01..1ddfe0b3f26 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -115,7 +115,6 @@ private static void OnExecuteForProperties( item.LeadingTrivia, item.FieldSymbol, isNotifyPropertyChanging, - isObservableObject, isObservableValidator, propertyChangedNames, propertyChangingNames)).ToArray()); @@ -160,7 +159,6 @@ private static void OnExecuteForProperties( /// The leading trivia for the field to process. /// The input instance to process. /// Indicates whether or not is also implemented. - /// Indicates whether or not the containing type inherits from ObservableObject. /// Indicates whether or not the containing type inherits from ObservableValidator. /// The collection of discovered property changed names. /// The collection of discovered property changing names. @@ -171,7 +169,6 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( SyntaxTriviaList leadingTrivia, IFieldSymbol fieldSymbol, bool isNotifyPropertyChanging, - bool isObservableObject, bool isObservableValidator, ICollection propertyChangedNames, ICollection propertyChangingNames) @@ -269,10 +266,10 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( // // if (SetProperty(ref , value, true)) // { - // OnPropertyChanged("Property1"); // Optional - // OnPropertyChanged("Property2"); + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property1PropertyChangedEventArgs); // Optional + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property2PropertyChangedEventArgs); // ... - // OnPropertyChanged("PropertyN"); + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNPropertyChangedEventArgs); // } InvocationExpressionSyntax setPropertyExpression = InvocationExpression(IdentifierName("SetProperty")) @@ -287,54 +284,41 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( _ => Block(IfStatement(setPropertyExpression, Block(dependentPropertyNotificationStatements))) }; } - else if (isObservableObject) - { - // Generate the inner setter block as follows: - // - // SetProperty(ref , value); - // - // Or in case there is at least one dependent property: - // - // if (SetProperty(ref , value)) - // { - // OnPropertyChanged("Property1"); // Optional - // OnPropertyChanged("Property2"); - // ... - // OnPropertyChanged("PropertyN"); - // } - InvocationExpressionSyntax setPropertyExpression = - InvocationExpression(IdentifierName("SetProperty")) - .AddArgumentListArguments( - Argument(IdentifierName(fieldSymbol.Name)).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), - Argument(IdentifierName("value"))); - - setterBlock = dependentPropertyNotificationStatements.Count switch - { - 0 => Block(ExpressionStatement(setPropertyExpression)), - _ => Block(IfStatement(setPropertyExpression, Block(dependentPropertyNotificationStatements))) - }; - } else { BlockSyntax updateAndNotificationBlock = Block(); - // Add the OnPropertyChanging() call if necessary + // Add OnPropertyChanging(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangingEventArgs) if necessary if (isNotifyPropertyChanging) { - updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanging")))); + propertyChangingNames.Add(propertyName); + + updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(ExpressionStatement( + InvocationExpression(IdentifierName("OnPropertyChanging")) + .AddArgumentListArguments(Argument(MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs")), + IdentifierName($"{propertyName}{nameof(PropertyChangingEventArgs)}")))))); } + propertyChangedNames.Add(propertyName); + // Add the following statements: // // = value; - // OnPropertyChanged(); + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangedEventArgs); updateAndNotificationBlock = updateAndNotificationBlock.AddStatements( ExpressionStatement( AssignmentExpression( SyntaxKind.SimpleAssignmentExpression, IdentifierName(fieldSymbol.Name), IdentifierName("value"))), - ExpressionStatement(InvocationExpression(IdentifierName("OnPropertyChanged")))); + ExpressionStatement( + InvocationExpression(IdentifierName("OnPropertyChanged")) + .AddArgumentListArguments(Argument(MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs")), + IdentifierName($"{propertyName}{nameof(PropertyChangedEventArgs)}")))))); // Add the dependent property notifications at the end updateAndNotificationBlock = updateAndNotificationBlock.AddStatements(dependentPropertyNotificationStatements.ToArray()); @@ -343,13 +327,13 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( // // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(, value)) // { - // OnPropertyChanging(); // Optional + // OnPropertyChanging(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangingEventArgs); // Optional // = value; - // OnPropertyChanged(); - // OnPropertyChanged("Property1"); // Optional - // OnPropertyChanged("Property2"); + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangedEventArgs); + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property1PropertyChangedEventArgs); // Optional + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property2PropertyChangedEventArgs); // ... - // OnPropertyChanged("PropertyN"); + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNPropertyChangedEventArgs); // } setterBlock = Block( IfStatement( From 1ba2b284d5772576232be14eae95a7acdbc6e4d9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 11 Apr 2021 16:17:59 +0200 Subject: [PATCH 77/89] Enabled property args caching for ObservableValidator properties --- .../ObservablePropertyGenerator.cs | 94 +++++++++++++------ 1 file changed, 65 insertions(+), 29 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 1ddfe0b3f26..16ebe7ca93f 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -206,7 +206,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( InvocationExpression(IdentifierName("OnPropertyChanged")) .AddArgumentListArguments(Argument(MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, - AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs")), + IdentifierName("global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"), IdentifierName($"{dependentPropertyName}{nameof(PropertyChangedEventArgs)}")))))); } else if (attributeArgument.Kind == TypedConstantKind.Array) @@ -227,7 +227,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( InvocationExpression(IdentifierName("OnPropertyChanged")) .AddArgumentListArguments(Argument(MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, - AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs")), + IdentifierName("global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"), IdentifierName($"{currentPropertyName}{nameof(PropertyChangedEventArgs)}")))))); } } @@ -257,32 +257,68 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( setterBlock = Block(); } - - // Generate the inner setter block as follows: - // - // SetProperty(ref , value, true); - // - // Or in case there is at least one dependent property: - // - // if (SetProperty(ref , value, true)) - // { - // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property1PropertyChangedEventArgs); // Optional - // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property2PropertyChangedEventArgs); - // ... - // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNPropertyChangedEventArgs); - // } - InvocationExpressionSyntax setPropertyExpression = - InvocationExpression(IdentifierName("SetProperty")) - .AddArgumentListArguments( - Argument(IdentifierName(fieldSymbol.Name)).WithRefOrOutKeyword(Token(SyntaxKind.RefKeyword)), - Argument(IdentifierName("value")), - Argument(LiteralExpression(SyntaxKind.TrueLiteralExpression))); - - setterBlock = dependentPropertyNotificationStatements.Count switch + else { - 0 => Block(ExpressionStatement(setPropertyExpression)), - _ => Block(IfStatement(setPropertyExpression, Block(dependentPropertyNotificationStatements))) - }; + propertyChangedNames.Add(propertyName); + propertyChangingNames.Add(propertyName); + + // Generate the inner setter block as follows: + // + // if (!global::System.Collections.Generic.EqualityComparer<>.Default.Equals(, value)) + // { + // OnPropertyChanging(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangingEventArgs); // Optional + // = value; + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNamePropertyChangedEventArgs); + // ValidateProperty(value, ); + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property1PropertyChangedEventArgs); // Optional + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.Property2PropertyChangedEventArgs); + // ... + // OnPropertyChanged(global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs.PropertyNPropertyChangedEventArgs); + // } + // + // The reason why the code is explicitly generated instead of just calling ObservableValidator.SetProperty() is so that we can + // take advantage of the cached property changed arguments for the current property as well, not just for the dependent ones. + setterBlock = Block( + IfStatement( + PrefixUnaryExpression( + SyntaxKind.LogicalNotExpression, + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + GenericName(Identifier("global::System.Collections.Generic.EqualityComparer")) + .AddTypeArgumentListArguments(IdentifierName(typeName)), + IdentifierName("Default")), + IdentifierName("Equals"))) + .AddArgumentListArguments( + Argument(IdentifierName(fieldSymbol.Name)), + Argument(IdentifierName("value")))), + Block( + ExpressionStatement( + InvocationExpression(IdentifierName("OnPropertyChanging")) + .AddArgumentListArguments(Argument(MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"), + IdentifierName($"{propertyName}{nameof(PropertyChangingEventArgs)}"))))), + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(fieldSymbol.Name), + IdentifierName("value"))), + ExpressionStatement( + InvocationExpression(IdentifierName("OnPropertyChanged")) + .AddArgumentListArguments(Argument(MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"), + IdentifierName($"{propertyName}{nameof(PropertyChangedEventArgs)}"))))), + ExpressionStatement( + InvocationExpression(IdentifierName("ValidateProperty")) + .AddArgumentListArguments( + Argument(IdentifierName("value")), + Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName)))))) + .AddStatements(dependentPropertyNotificationStatements.ToArray()))); + } } else { @@ -297,7 +333,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( InvocationExpression(IdentifierName("OnPropertyChanging")) .AddArgumentListArguments(Argument(MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, - AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs")), + IdentifierName("global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"), IdentifierName($"{propertyName}{nameof(PropertyChangingEventArgs)}")))))); } @@ -317,7 +353,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( InvocationExpression(IdentifierName("OnPropertyChanged")) .AddArgumentListArguments(Argument(MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, - AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs")), + IdentifierName("global::Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedOrChangingArgs"), IdentifierName($"{propertyName}{nameof(PropertyChangedEventArgs)}")))))); // Add the dependent property notifications at the end From 4deae2316e52d48815fc87820c0cb30e9adc62c8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 12 Apr 2021 18:21:15 +0200 Subject: [PATCH 78/89] Minor code refactoring --- .../ObservablePropertyGenerator.cs | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index 16ebe7ca93f..bba86015b55 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -473,38 +473,6 @@ public void OnExecuteForPropertyArgs(GeneratorExecutionContext context, IReadOnl return; } - static FieldDeclarationSyntax CreateFieldDeclaration(INamedTypeSymbol type, string propertyName) - { - // Create a static field with a cached property changed/changing argument for a specified property. - // This code produces a field declaration as follows: - // - // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - // [global::System.Obsolete("This field is not intended to be referenced directly by user code")] - // public static readonly = new(""); - return - FieldDeclaration( - VariableDeclaration(IdentifierName(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) - .AddVariables( - VariableDeclarator(Identifier($"{propertyName}{type.Name}")) - .WithInitializer(EqualsValueClause( - ImplicitObjectCreationExpression() - .AddArgumentListArguments(Argument( - LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName)))))))) - .AddModifiers( - Token(SyntaxKind.PublicKeyword), - Token(SyntaxKind.StaticKeyword), - Token(SyntaxKind.ReadOnlyKeyword)) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( - AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( - AttributeArgument(LiteralExpression( - SyntaxKind.StringLiteralExpression, - Literal("This field is not intended to be referenced directly by user code"))))))); - } - INamedTypeSymbol propertyChangedEventArgsSymbol = context.Compilation.GetTypeByMetadataName(typeof(PropertyChangedEventArgs).FullName)!, propertyChangingEventArgsSymbol = context.Compilation.GetTypeByMetadataName(typeof(PropertyChangingEventArgs).FullName)!; @@ -563,5 +531,44 @@ static FieldDeclarationSyntax CreateFieldDeclaration(INamedTypeSymbol type, stri // Add the partial type context.AddSource($"[{typeof(ObservablePropertyAttribute).Name}]_[__KnownINotifyPropertyChangedOrChangingArgs].cs", SourceText.From(source, Encoding.UTF8)); } + + /// + /// Creates a field declaration for a cached property change name. + /// + /// The type of cached property change argument (either or ). + /// The name of the cached property name. + /// A instance for the input cached property name. + [Pure] + private static FieldDeclarationSyntax CreateFieldDeclaration(INamedTypeSymbol type, string propertyName) + { + // Create a static field with a cached property changed/changing argument for a specified property. + // This code produces a field declaration as follows: + // + // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + // [global::System.Obsolete("This field is not intended to be referenced directly by user code")] + // public static readonly = new(""); + return + FieldDeclaration( + VariableDeclaration(IdentifierName(type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))) + .AddVariables( + VariableDeclarator(Identifier($"{propertyName}{type.Name}")) + .WithInitializer(EqualsValueClause( + ImplicitObjectCreationExpression() + .AddArgumentListArguments(Argument( + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(propertyName)))))))) + .AddModifiers( + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.StaticKeyword), + Token(SyntaxKind.ReadOnlyKeyword)) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( + AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.Obsolete")).AddArgumentListArguments( + AttributeArgument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal("This field is not intended to be referenced directly by user code"))))))); + } } } From 9184ba5be660f3265af1cd352d5efd95d345e654 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 23 Apr 2021 18:40:32 +0200 Subject: [PATCH 79/89] Improved code generation for ValidateAllProperties generator Improved type safety, more AOT-friendly, removed Unsafe.As hack --- ...ValidatorValidateAllPropertiesGenerator.cs | 39 +++++++++++++++---- .../ComponentModel/ObservableValidator.cs | 6 +-- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs index 516f717860a..8d160cba685 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -64,7 +64,11 @@ public void Execute(GeneratorExecutionContext context) foreach (INamedTypeSymbol classSymbol in syntaxReceiver.GatheredInfo) { - // Create a static method to validate all properties in a given class. + // Create a static factory method creating a delegate that can be used to validate all properties in a given class. + // This pattern is used so that the library doesn't have to use MakeGenericType(...) at runtime, nor use unsafe casts + // over the created delegate to be able to cache it as an Action instance. This pattern enables the same + // functionality and with almost identical performance (not noticeable in this context anyway), but while preserving + // full runtime type safety (as a safe cast is used to validate the input argument), and with less reflection needed. // This code takes a class symbol and produces a compilation unit as follows: // // // Licensed to the .NET Foundation under one or more agreements. @@ -84,9 +88,14 @@ public void Execute(GeneratorExecutionContext context) // { // [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 void ValidateAllProperties( instance) + // public static global::System.Action CreateAllPropertiesValidator( _) // { - // + // static void ValidateAllProperties( instance) + // { + // + // } + // + // return static o => ValidateAllProperties(()o); // } // } // } @@ -102,8 +111,8 @@ public void Execute(GeneratorExecutionContext context) Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.PartialKeyword)).AddAttributeLists(classAttributes).AddMembers( MethodDeclaration( - PredefinedType(Token(SyntaxKind.VoidKeyword)), - Identifier("ValidateAllProperties")).AddAttributeLists( + GenericName("global::System.Action").AddTypeArgumentListArguments(PredefinedType(Token(SyntaxKind.ObjectKeyword))), + Identifier("CreateAllPropertiesValidator")).AddAttributeLists( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), @@ -114,8 +123,24 @@ public void Execute(GeneratorExecutionContext context) Literal("This method is not intended to be called directly by user code"))))))).AddModifiers( Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)).AddParameterListParameters( - Parameter(Identifier("instance")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) - .WithBody(Block(EnumerateValidationStatements(classSymbol, validationSymbol).ToArray()))))) + Parameter(Identifier("_")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) + .WithBody(Block( + LocalFunctionStatement( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + Identifier("ValidateAllProperties")) + .AddModifiers(Token(SyntaxKind.StaticKeyword)) + .AddParameterListParameters( + Parameter(Identifier("instance")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) + .WithBody(Block(EnumerateValidationStatements(classSymbol, validationSymbol).ToArray())), + ReturnStatement( + SimpleLambdaExpression(Parameter(Identifier("o"))) + .AddModifiers(Token(SyntaxKind.StaticKeyword)) + .WithExpressionBody( + InvocationExpression(IdentifierName("ValidateAllProperties")) + .AddArgumentListArguments(Argument( + CastExpression( + IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + IdentifierName("o"))))))))))) .NormalizeWhitespace() .ToFullString(); diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs index dead1935df0..0390ba60247 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableValidator.cs @@ -476,11 +476,9 @@ protected void ValidateAllProperties() static Action GetValidationAction(Type type) { if (type.Assembly.GetType("Microsoft.Toolkit.Mvvm.ComponentModel.__Internals.__ObservableValidatorExtensions") is Type extensionsType && - extensionsType.GetMethod("ValidateAllProperties", new[] { type }) is MethodInfo methodInfo) + extensionsType.GetMethod("CreateAllPropertiesValidator", new[] { type }) is MethodInfo methodInfo) { - Type delegateType = typeof(Action<>).MakeGenericType(type); - - return Unsafe.As>(methodInfo.CreateDelegate(delegateType)); + return (Action)methodInfo.Invoke(null, new object?[] { null })!; } return GetValidationActionFallback(type); From 5ba929fc75269c29d63490f2ad989de465f61445 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 23 Apr 2021 20:20:26 +0200 Subject: [PATCH 80/89] Improved code generation for IRecipient generator --- .../IMessengerRegisterAllGenerator.cs | 92 +++++++++++++++---- .../Messaging/IMessengerExtensions.cs | 49 ++-------- .../Mvvm/Test_IRecipientGenerator.cs | 5 +- 3 files changed, 89 insertions(+), 57 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index f3252dd931a..13792838491 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -63,10 +63,13 @@ public void Execute(GeneratorExecutionContext context) foreach (INamedTypeSymbol classSymbol in syntaxReceiver.GatheredInfo) { - // Create a static method to register all messages for a given recipient type. + // 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.). // There are two versions that are generated: a non-generic one doing the registration // with no tokens, which is the most common scenario and will help particularly in AOT // scenarios, and 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 takes a class symbol and produces a compilation unit as follows: // // // Licensed to the .NET Foundation under one or more agreements. @@ -86,17 +89,27 @@ public void Execute(GeneratorExecutionContext context) // { // [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 void RegisterAll(IMessenger messenger, recipient) + // public static global::System.Action CreateAllMessagesRegistrator( _) // { - // + // static void RegisterAll(IMessenger messenger, instance) + // { + // + // } + // + // return static (m, r) => RegisterAll(m, ()r); // } // // [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 void RegisterAll(IMessenger messenger, recipient, TToken token) + // public static global::System.Action CreateAllMessagesRegistratorWithToken( _) // where TToken : global::System.IEquatable // { - // + // static void RegisterAll(IMessenger messenger, instance, TToken token) + // { + // + // } + // + // return static (m, r, t) => RegisterAll(m, ()r, t); // } // } // } @@ -112,8 +125,10 @@ public void Execute(GeneratorExecutionContext context) Token(SyntaxKind.StaticKeyword), Token(SyntaxKind.PartialKeyword)).AddAttributeLists(classAttributes).AddMembers( MethodDeclaration( - PredefinedType(Token(SyntaxKind.VoidKeyword)), - Identifier("RegisterAll")).AddAttributeLists( + GenericName("global::System.Action").AddTypeArgumentListArguments( + IdentifierName("IMessenger"), + PredefinedType(Token(SyntaxKind.ObjectKeyword))), + Identifier("CreateAllMessagesRegistrator")).AddAttributeLists( AttributeList(SingletonSeparatedList( Attribute(IdentifierName("global::System.ComponentModel.EditorBrowsable")).AddArgumentListArguments( AttributeArgument(ParseExpression("global::System.ComponentModel.EditorBrowsableState.Never"))))), @@ -124,12 +139,35 @@ public void Execute(GeneratorExecutionContext context) Literal("This method is not intended to be called directly by user code"))))))).AddModifiers( Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)).AddParameterListParameters( - Parameter(Identifier("messenger")).WithType(IdentifierName("IMessenger")), - Parameter(Identifier("recipient")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) - .WithBody(Block(EnumerateRegistrationStatements(classSymbol, iRecipientSymbol).ToArray())), + Parameter(Identifier("_")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) + .WithBody(Block( + LocalFunctionStatement( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + Identifier("RegisterAll")) + .AddModifiers(Token(SyntaxKind.StaticKeyword)) + .AddParameterListParameters( + Parameter(Identifier("messenger")).WithType(IdentifierName("IMessenger")), + Parameter(Identifier("recipient")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) + .WithBody(Block(EnumerateRegistrationStatements(classSymbol, iRecipientSymbol).ToArray())), + ReturnStatement( + ParenthesizedLambdaExpression() + .AddModifiers(Token(SyntaxKind.StaticKeyword)) + .AddParameterListParameters( + Parameter(Identifier("m")), + Parameter(Identifier("r"))) + .WithExpressionBody( + InvocationExpression(IdentifierName("RegisterAll")) + .AddArgumentListArguments( + Argument(IdentifierName("m")), + Argument(CastExpression( + IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + IdentifierName("r")))))))), MethodDeclaration( - PredefinedType(Token(SyntaxKind.VoidKeyword)), - Identifier("RegisterAll")).AddAttributeLists( + GenericName("global::System.Action").AddTypeArgumentListArguments( + IdentifierName("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"))))), @@ -140,14 +178,36 @@ public void Execute(GeneratorExecutionContext context) Literal("This method is not intended to be called directly by user code"))))))).AddModifiers( Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)).AddParameterListParameters( - Parameter(Identifier("messenger")).WithType(IdentifierName("IMessenger")), - Parameter(Identifier("recipient")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))), - Parameter(Identifier("token")).WithType(IdentifierName("TToken"))) + Parameter(Identifier("_")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) .AddTypeParameterListParameters(TypeParameter("TToken")) .AddConstraintClauses( TypeParameterConstraintClause("TToken") .AddConstraints(TypeConstraint(GenericName("global::System.IEquatable").AddTypeArgumentListArguments(IdentifierName("TToken"))))) - .WithBody(Block(EnumerateRegistrationStatementsWithTokens(classSymbol, iRecipientSymbol).ToArray()))))) + .WithBody(Block( + LocalFunctionStatement( + PredefinedType(Token(SyntaxKind.VoidKeyword)), + Identifier("RegisterAll")) + .AddModifiers(Token(SyntaxKind.StaticKeyword)) + .AddParameterListParameters( + Parameter(Identifier("messenger")).WithType(IdentifierName("IMessenger")), + Parameter(Identifier("recipient")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))), + Parameter(Identifier("token")).WithType(IdentifierName("TToken"))) + .WithBody(Block(EnumerateRegistrationStatementsWithTokens(classSymbol, iRecipientSymbol).ToArray())), + ReturnStatement( + ParenthesizedLambdaExpression() + .AddModifiers(Token(SyntaxKind.StaticKeyword)) + .AddParameterListParameters( + Parameter(Identifier("m")), + Parameter(Identifier("r")), + Parameter(Identifier("t"))) + .WithExpressionBody( + InvocationExpression(IdentifierName("RegisterAll")) + .AddArgumentListArguments( + Argument(IdentifierName("m")), + Argument(CastExpression( + IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + IdentifierName("r"))), + Argument(IdentifierName("t")))))))))) .NormalizeWhitespace() .ToFullString(); diff --git a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs index 999f6544deb..2c1fe66f629 100644 --- a/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs +++ b/Microsoft.Toolkit.Mvvm/Messaging/IMessengerExtensions.cs @@ -86,16 +86,13 @@ public static void RegisterAll(this IMessenger messenger, object 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 create a delegate wrapping that method if it is found. + // target recipient type, and just invoke it to get the delegate to cache and use later. static Action? LoadRegistrationMethodsForType(Type recipientType) { if (recipientType.Assembly.GetType("Microsoft.Toolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType && - extensionsType.GetMethod("RegisterAll", new[] { typeof(IMessenger), recipientType }) is MethodInfo methodInfo) + extensionsType.GetMethod("CreateAllMessagesRegistrator", new[] { recipientType }) is MethodInfo methodInfo) { - Type delegateType = typeof(Action<,>).MakeGenericType(typeof(IMessenger), recipientType); - - // Create the delegate and use an unsafe cast to achieve input covariance (as detailed below) - return Unsafe.As>(methodInfo.CreateDelegate(delegateType)); + return (Action)methodInfo.Invoke(null, new object?[] { null })!; } return null; @@ -135,49 +132,21 @@ public static void RegisterAll(this IMessenger messenger, object recipie { // 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 create a delegate wrapping that method if it is found. + // 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. static Action LoadRegistrationMethodsForType(Type recipientType) { - if (recipientType.Assembly.GetType("Microsoft.Toolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType) + if (recipientType.Assembly.GetType("Microsoft.Toolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions") is Type extensionsType && + extensionsType.GetMethod("CreateAllMessagesRegistratorWithToken", new[] { recipientType }) is MethodInfo methodInfo) { -#if NETSTANDARD2_0 - // .NET Standard 2.0 doesn't have Type.MakeGenericMethodParameter, so we need to iterate manually - foreach (MethodInfo methodInfo in extensionsType.GetMethods(BindingFlags.Static | BindingFlags.Public)) - { - if (methodInfo.Name is "RegisterAll" && - methodInfo.IsGenericMethod && - methodInfo.GetParameters()[1].ParameterType == recipientType) - { - return CreateGenericDelegate(recipientType, methodInfo); - } - } -#else - // On .NET Standard 2.1 and up, we can directly look for the target method in one call - Type[] methodTypes = new[] { typeof(IMessenger), recipientType, Type.MakeGenericMethodParameter(0) }; + MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(TToken)); - if (extensionsType.GetMethod("RegisterAll", methodTypes) is MethodInfo methodInfo) - { - return CreateGenericDelegate(recipientType, methodInfo); - } -#endif + return (Action)genericMethodInfo.Invoke(null, new object?[] { null })!; } return LoadRegistrationMethodsForTypeFallback(recipientType); } - // A shared method to create a generic delegate from an identified method - static Action CreateGenericDelegate(Type recipientType, MethodInfo methodInfo) - { - MethodInfo genericMethodInfo = methodInfo.MakeGenericMethod(typeof(TToken)); - Type delegateType = typeof(Action<,,>).MakeGenericType(typeof(IMessenger), recipientType, typeof(TToken)); - - // We need an unsafe cast here like we did in StrongReferenceMessenger to be able to treat the new delegate - // type as if it was covariant in its input recipient. This allows us to keep the type-specific overloads in - // the generated code while still creating non-generic delegates here. This code is technically safe since - // we have control over what types we're working with, and we know the type conversions will always be valid. - return Unsafe.As>(genericMethodInfo.CreateDelegate(delegateType)); - } - // 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. diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs index dd196545f47..dece3973370 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_IRecipientGenerator.cs @@ -4,6 +4,7 @@ #pragma warning disable CS0618 +using System; using Microsoft.Toolkit.Mvvm.Messaging; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -22,7 +23,9 @@ public void Test_IRecipientGenerator_GeneratedRegistration() var messageA = new MessageA(); var messageB = new MessageB(); - Microsoft.Toolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions.RegisterAll(messenger, recipient, 42); + Action registrator = Microsoft.Toolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions.CreateAllMessagesRegistratorWithToken(recipient); + + registrator(messenger, recipient, 42); Assert.IsTrue(messenger.IsRegistered(recipient, 42)); Assert.IsTrue(messenger.IsRegistered(recipient, 42)); From 7f5bd6c2db866e309d446c327150fc944a8f60c4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 25 Apr 2021 18:25:32 +0200 Subject: [PATCH 81/89] Switched generated command field to concrete type Doing this enables the JIT to devirtualize function calls through the property --- .../Input/ICommandGenerator.cs | 2 +- .../Mvvm/Test_ObservablePropertyAttribute.cs | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs index 38d1f37d432..ed0630d3375 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -150,7 +150,7 @@ private static IEnumerable CreateCommandMembers(Generat // private ? ; FieldDeclarationSyntax fieldDeclaration = FieldDeclaration( - VariableDeclaration(NullableType(IdentifierName(commandInterfaceTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) + VariableDeclaration(NullableType(IdentifierName(commandClassTypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) .AddVariables(VariableDeclarator(Identifier(fieldName)))) .AddModifiers(Token(SyntaxKind.PrivateKeyword)) .AddAttributeLists(AttributeList(SingletonSeparatedList( diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs index 2a3eb6e3139..2959683b597 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs @@ -62,7 +62,7 @@ public void Test_AlsoNotifyForAttribute_Events() { var model = new DependentPropertyModel(); - List propertyNames = new(); + List propertyNames = new(); model.PropertyChanged += (s, e) => propertyNames.Add(e.PropertyName); @@ -76,27 +76,27 @@ public void Test_AlsoNotifyForAttribute_Events() [TestMethod] public void Test_ValidationAttributes() { - var nameProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Name)); + var nameProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Name))!; Assert.IsNotNull(nameProperty.GetCustomAttribute()); Assert.IsNotNull(nameProperty.GetCustomAttribute()); - Assert.AreEqual(nameProperty.GetCustomAttribute().Length, 1); + Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 1); Assert.IsNotNull(nameProperty.GetCustomAttribute()); - Assert.AreEqual(nameProperty.GetCustomAttribute().Length, 100); + Assert.AreEqual(nameProperty.GetCustomAttribute()!.Length, 100); - var ageProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Age)); + var ageProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Age))!; Assert.IsNotNull(ageProperty.GetCustomAttribute()); - Assert.AreEqual(ageProperty.GetCustomAttribute().Minimum, 0); - Assert.AreEqual(ageProperty.GetCustomAttribute().Maximum, 120); + Assert.AreEqual(ageProperty.GetCustomAttribute()!.Minimum, 0); + Assert.AreEqual(ageProperty.GetCustomAttribute()!.Maximum, 120); - var emailProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Email)); + var emailProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.Email))!; Assert.IsNotNull(emailProperty.GetCustomAttribute()); - var comboProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.IfThisWorksThenThatsGreat)); + var comboProperty = typeof(MyFormViewModel).GetProperty(nameof(MyFormViewModel.IfThisWorksThenThatsGreat))!; - TestValidationAttribute? testAttribute = comboProperty.GetCustomAttribute(); + TestValidationAttribute testAttribute = comboProperty.GetCustomAttribute()!; Assert.IsNotNull(testAttribute); Assert.IsNull(testAttribute.O); From c5b90500cc27f7a05885eca87d8d8746a4fb6a0a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 25 Apr 2021 21:31:34 +0200 Subject: [PATCH 82/89] Performance improvements to validation/messenger generated code --- ...ValidatorValidateAllPropertiesGenerator.cs | 32 +++++---- .../IMessengerRegisterAllGenerator.cs | 66 +++++++++---------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs index 8d160cba685..f2e3bc78205 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -69,6 +69,10 @@ public void Execute(GeneratorExecutionContext context) // over the created delegate to be able to cache it as an Action instance. This pattern enables the same // functionality and with almost identical performance (not noticeable in this context anyway), but while preserving // full runtime type safety (as a safe cast is used to validate the input argument), and with less reflection needed. + // Note that we're deliberately creating a new delegate instance here and not using code that could see the C# compiler + // create a static class to cache a reusable delegate, because each generated method will only be called at most once, + // as the returned delegate will be cached by the MVVM Toolkit itself. So this ensures the the produced code is minimal, + // and that there will be no unnecessary static fields and objects being created and possibly never collected. // This code takes a class symbol and produces a compilation unit as follows: // // // Licensed to the .NET Foundation under one or more agreements. @@ -90,12 +94,13 @@ public void Execute(GeneratorExecutionContext context) // [global::System.Obsolete("This method is not intended to be called directly by user code")] // public static global::System.Action CreateAllPropertiesValidator( _) // { - // static void ValidateAllProperties( instance) + // static void ValidateAllProperties(object obj) // { + // var instance = ()obj; // // } // - // return static o => ValidateAllProperties(()o); + // return ValidateAllProperties; // } // } // } @@ -130,17 +135,18 @@ public void Execute(GeneratorExecutionContext context) Identifier("ValidateAllProperties")) .AddModifiers(Token(SyntaxKind.StaticKeyword)) .AddParameterListParameters( - Parameter(Identifier("instance")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) - .WithBody(Block(EnumerateValidationStatements(classSymbol, validationSymbol).ToArray())), - ReturnStatement( - SimpleLambdaExpression(Parameter(Identifier("o"))) - .AddModifiers(Token(SyntaxKind.StaticKeyword)) - .WithExpressionBody( - InvocationExpression(IdentifierName("ValidateAllProperties")) - .AddArgumentListArguments(Argument( - CastExpression( - IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), - IdentifierName("o"))))))))))) + Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword)))) + .WithBody(Block( + LocalDeclarationStatement( + VariableDeclaration(IdentifierName("var")) // Cannot Token(SyntaxKind.VarKeyword) here (throws an ArgumentException) + .AddVariables( + VariableDeclarator(Identifier("instance")) + .WithInitializer(EqualsValueClause( + CastExpression( + IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + IdentifierName("obj"))))))) + .AddStatements(EnumerateValidationStatements(classSymbol, validationSymbol).ToArray())), + ReturnStatement(IdentifierName("ValidateAllProperties"))))))) .NormalizeWhitespace() .ToFullString(); diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index 13792838491..4e8ba909211 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -91,12 +91,13 @@ public void Execute(GeneratorExecutionContext context) // [global::System.Obsolete("This method is not intended to be called directly by user code")] // public static global::System.Action CreateAllMessagesRegistrator( _) // { - // static void RegisterAll(IMessenger messenger, instance) + // static void RegisterAll(IMessenger messenger, object obj) // { + // var recipient = ()obj; // // } // - // return static (m, r) => RegisterAll(m, ()r); + // return RegisterAll; // } // // [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] @@ -104,12 +105,13 @@ public void Execute(GeneratorExecutionContext context) // public static global::System.Action CreateAllMessagesRegistratorWithToken( _) // where TToken : global::System.IEquatable // { - // static void RegisterAll(IMessenger messenger, instance, TToken token) + // static void RegisterAll(IMessenger messenger, object obj, TToken token) // { + // var recipient = ()obj; // // } // - // return static (m, r, t) => RegisterAll(m, ()r, t); + // return RegisterAll; // } // } // } @@ -147,21 +149,18 @@ public void Execute(GeneratorExecutionContext context) .AddModifiers(Token(SyntaxKind.StaticKeyword)) .AddParameterListParameters( Parameter(Identifier("messenger")).WithType(IdentifierName("IMessenger")), - Parameter(Identifier("recipient")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)))) - .WithBody(Block(EnumerateRegistrationStatements(classSymbol, iRecipientSymbol).ToArray())), - ReturnStatement( - ParenthesizedLambdaExpression() - .AddModifiers(Token(SyntaxKind.StaticKeyword)) - .AddParameterListParameters( - Parameter(Identifier("m")), - Parameter(Identifier("r"))) - .WithExpressionBody( - InvocationExpression(IdentifierName("RegisterAll")) - .AddArgumentListArguments( - Argument(IdentifierName("m")), - Argument(CastExpression( - IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), - IdentifierName("r")))))))), + Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword)))) + .WithBody(Block( + LocalDeclarationStatement( + VariableDeclaration(IdentifierName("var")) + .AddVariables( + VariableDeclarator(Identifier("recipient")) + .WithInitializer(EqualsValueClause( + CastExpression( + IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + IdentifierName("obj"))))))) + .AddStatements(EnumerateRegistrationStatements(classSymbol, iRecipientSymbol).ToArray())), + ReturnStatement(IdentifierName("RegisterAll")))), MethodDeclaration( GenericName("global::System.Action").AddTypeArgumentListArguments( IdentifierName("IMessenger"), @@ -190,24 +189,19 @@ public void Execute(GeneratorExecutionContext context) .AddModifiers(Token(SyntaxKind.StaticKeyword)) .AddParameterListParameters( Parameter(Identifier("messenger")).WithType(IdentifierName("IMessenger")), - Parameter(Identifier("recipient")).WithType(IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))), + Parameter(Identifier("obj")).WithType(PredefinedType(Token(SyntaxKind.ObjectKeyword))), Parameter(Identifier("token")).WithType(IdentifierName("TToken"))) - .WithBody(Block(EnumerateRegistrationStatementsWithTokens(classSymbol, iRecipientSymbol).ToArray())), - ReturnStatement( - ParenthesizedLambdaExpression() - .AddModifiers(Token(SyntaxKind.StaticKeyword)) - .AddParameterListParameters( - Parameter(Identifier("m")), - Parameter(Identifier("r")), - Parameter(Identifier("t"))) - .WithExpressionBody( - InvocationExpression(IdentifierName("RegisterAll")) - .AddArgumentListArguments( - Argument(IdentifierName("m")), - Argument(CastExpression( - IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), - IdentifierName("r"))), - Argument(IdentifierName("t")))))))))) + .WithBody(Block( + LocalDeclarationStatement( + VariableDeclaration(IdentifierName("var")) + .AddVariables( + VariableDeclarator(Identifier("recipient")) + .WithInitializer(EqualsValueClause( + CastExpression( + IdentifierName(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + IdentifierName("obj"))))))) + .AddStatements(EnumerateRegistrationStatementsWithTokens(classSymbol, iRecipientSymbol).ToArray())), + ReturnStatement(IdentifierName("RegisterAll"))))))) .NormalizeWhitespace() .ToFullString(); From 134058d564a02da6d89f18e13098acf43f8afd29 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 13 Jul 2021 14:20:17 +0200 Subject: [PATCH 83/89] Renamed [AlsoNotifyFor] attribute to [AlsoNotifyChangeFor] --- .../ComponentModel/ObservablePropertyGenerator.cs | 4 ++-- .../Input/ICommandGenerator.cs | 2 +- .../Microsoft.Toolkit.Mvvm.SourceGenerators.csproj | 2 +- ...ttribute.cs => AlsoNotifyChangeForAttribute.cs} | 14 +++++++------- .../Mvvm/Test_ObservablePropertyAttribute.cs | 6 +++--- 5 files changed, 14 insertions(+), 14 deletions(-) rename Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/{AlsoNotifyForAttribute.cs => AlsoNotifyChangeForAttribute.cs} (90%) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index bba86015b55..e54e8cac111 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -178,7 +178,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( typeName = fieldSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), propertyName = GetGeneratedPropertyName(fieldSymbol); - INamedTypeSymbol alsoNotifyForAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyForAttribute).FullName)!; + INamedTypeSymbol alsoNotifyChangeForAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyChangeForAttribute).FullName)!; INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute"); List dependentPropertyNotificationStatements = new(); @@ -187,7 +187,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( foreach (AttributeData attributeData in fieldSymbol.GetAttributes()) { // Add dependent property notifications, if needed - if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyForAttributeSymbol)) + if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, alsoNotifyChangeForAttributeSymbol)) { foreach (TypedConstant attributeArgument in attributeData.ConstructorArguments) { diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs index ed0630d3375..ee8350974ac 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -226,7 +226,7 @@ private static (string FieldName, string PropertyName) GetGeneratedFieldAndPrope { string propertyName = methodSymbol.Name; - if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")!) && + if (SymbolEqualityComparer.Default.Equals(methodSymbol.ReturnType, context.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task")) && methodSymbol.Name.EndsWith("Async")) { propertyName = propertyName.Substring(0, propertyName.Length - "Async".Length); diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index c075395e6c8..427fcf63ee1 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -14,7 +14,7 @@ - + diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs similarity index 90% rename from Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs rename to Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs index 817ba9e2c2f..9315340ca5c 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyForAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs @@ -28,11 +28,11 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// partial class MyViewModel : ObservableObject /// { /// [ObservableProperty] - /// [AlsoNotifyFor(nameof(FullName))] + /// [AlsoNotifyChangeFor(nameof(FullName))] /// private string name; /// /// [ObservableProperty] - /// [AlsoNotifyFor(nameof(FullName))] + /// [AlsoNotifyChangeFor(nameof(FullName))] /// private string surname; /// /// public string FullName => $"{Name} {Surname}"; @@ -64,26 +64,26 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// /// [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] - public sealed class AlsoNotifyForAttribute : Attribute + public sealed class AlsoNotifyChangeForAttribute : Attribute { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The name of the property to also notify when the annotated property changes. - public AlsoNotifyForAttribute(string propertyName) + public AlsoNotifyChangeForAttribute(string propertyName) { PropertyNames = new[] { propertyName }; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The name of the property to also notify when the annotated property changes. /// /// The other property names to also notify when the annotated property changes. This parameter can optionally /// be used to indicate a series of dependent properties from the same attribute, to keep the code more compact. /// - public AlsoNotifyForAttribute(string propertyName, string[] otherPropertyNames) + public AlsoNotifyChangeForAttribute(string propertyName, string[] otherPropertyNames) { PropertyNames = new[] { propertyName }.Concat(otherPropertyNames).ToArray(); } diff --git a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs index 2959683b597..97ef214b38c 100644 --- a/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs +++ b/UnitTests/UnitTests.NetCore/Mvvm/Test_ObservablePropertyAttribute.cs @@ -58,7 +58,7 @@ public void Test_ObservablePropertyAttribute_Events() [TestCategory("Mvvm")] [TestMethod] - public void Test_AlsoNotifyForAttribute_Events() + public void Test_AlsoNotifyChangeForAttribute_Events() { var model = new DependentPropertyModel(); @@ -129,11 +129,11 @@ public partial class SampleModel : ObservableObject public sealed partial class DependentPropertyModel { [ObservableProperty] - [AlsoNotifyFor(nameof(FullName))] + [AlsoNotifyChangeFor(nameof(FullName))] private string? name; [ObservableProperty] - [AlsoNotifyFor(nameof(FullName))] + [AlsoNotifyChangeFor(nameof(FullName))] private string? surname; public string FullName => $"{Name} {Surname}"; From fe54a9ce7fb768036ecf31af701886144c4febd6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 16 Jul 2021 12:02:44 +0200 Subject: [PATCH 84/89] Switch branch name to main in analyzer files Co-authored-by: Michael Hawker MSFT (XAML Llama) <24302614+michael-hawker@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 3 +-- .../AnalyzerReleases.Unshipped.md | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md index d567f14248e..5ccc9f037f6 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Shipped.md @@ -1,3 +1,2 @@ ; Shipped analyzer releases -; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md - +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md index 8218a8e88fc..c575520f2ab 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -1,5 +1,5 @@ ; Unshipped analyzer release -; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md ### New Rules @@ -16,4 +16,4 @@ MVVMTK0008 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerato MVVMTK0009 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit MVVMTK0010 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit MVVMTK0011 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0012 | Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit \ No newline at end of file +MVVMTK0012 | Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit From eb0b4756da429d614e6022df8d1d39f5f9c259e2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 16 Jul 2021 12:15:21 +0200 Subject: [PATCH 85/89] Fixed an incorrect example in XML docs --- .../Attributes/AlsoNotifyChangeForAttribute.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs index 9315340ca5c..06651c7c77d 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs @@ -46,7 +46,13 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// public string Name /// { /// get => name; - /// set => SetProperty(ref name, value); + /// set + /// { + /// if (SetProperty(ref name, value)) + /// { + /// OnPropertyChanged(nameof(FullName)); + /// } + /// } /// } /// /// public string Surname @@ -54,7 +60,7 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// get => surname; /// set /// { - /// if (SetProperty(ref name, value)) + /// if (SetProperty(ref surname, value)) /// { /// OnPropertyChanged(nameof(FullName)); /// } From 37caf88b68463ebb23b67c2e5d3f095485c9ecc5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 21 Jul 2021 02:38:20 +0200 Subject: [PATCH 86/89] Improved XML docs for [ObservableProperty] Added more info and remarks about field name conversion --- .../Attributes/ObservablePropertyAttribute.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs index 2957ac312b6..e0fd3e71ff7 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs @@ -22,6 +22,9 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// { /// [ObservableProperty] /// private string name; + /// + /// [ObservableProperty] + /// private bool isEnabled; /// } /// /// @@ -34,9 +37,21 @@ namespace Microsoft.Toolkit.Mvvm.ComponentModel /// get => name; /// set => SetProperty(ref name, value); /// } + /// + /// public bool IsEnabled + /// { + /// get => name; + /// set => SetProperty(ref isEnabled, value); + /// } /// } /// /// + /// + /// The generated properties will automatically use the UpperCamelCase format for their names, + /// which will be derived from the field names. The generator can also recognize fields using either + /// the _lowerCamel or m_lowerCamel naming scheme. Otherwise, the first character in the + /// source field name will be converted to uppercase (eg. isEnabled to IsEnabled). + /// [AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)] public sealed class ObservablePropertyAttribute : Attribute { From 806b6da6f5606fe5af6a3ab429b777db4331c59b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 21 Jul 2021 14:28:44 +0200 Subject: [PATCH 87/89] Fixed aka.ms link for generator errors info --- .../AnalyzerReleases.Unshipped.md | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md index c575520f2ab..b27b97bcbc6 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -5,15 +5,15 @@ Rule ID | Category | Severity | Notes --------|----------|----------|------- -MVVMTK0001 | Microsoft.Toolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0002 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0003 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0004 | Microsoft.Toolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0005 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0006 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0007 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0008 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0009 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0010 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0011 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit -MVVMTK0012 | Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit +MVVMTK0001 | Microsoft.Toolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0002 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0003 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0004 | Microsoft.Toolkit.Mvvm.SourceGenerators.INotifyPropertyChangedGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0005 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0006 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableObjectGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0007 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0008 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservableRecipientGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0009 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0010 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0011 | Microsoft.Toolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator | Error | See https://aka.ms/mvvmtoolkit/error +MVVMTK0012 | Microsoft.Toolkit.Mvvm.SourceGenerators.ICommandGenerator | Error | See https://aka.ms/mvvmtoolkit/error From 9283f2c7859164bba14eb479c99454f33b906625 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 23 Jul 2021 15:05:53 +0200 Subject: [PATCH 88/89] Switched to fully qualified names to resolve all symbols --- .../INotifyPropertyChangedGenerator.cs | 18 ++++++---- .../ObservableObjectGenerator.cs | 18 ++++++---- ...ervablePropertyGenerator.SyntaxReceiver.cs | 3 +- .../ObservablePropertyGenerator.cs | 17 +++++---- .../ObservableRecipientGenerator.cs | 18 ++++++---- ...ansitiveMembersGenerator.SyntaxReceiver.cs | 20 +++++++++-- .../TransitiveMembersGenerator.cs | 35 ++++++++++++++----- .../Diagnostics/DiagnosticDescriptors.cs | 28 +++++++-------- .../Input/ICommandGenerator.SyntaxReceiver.cs | 3 +- .../Input/ICommandGenerator.cs | 3 +- ...osoft.Toolkit.Mvvm.SourceGenerators.csproj | 9 ----- .../AlsoNotifyChangeForAttribute.cs | 2 -- .../INotifyPropertyChangedAttribute.cs | 2 -- .../Attributes/ObservableObjectAttribute.cs | 2 -- .../Attributes/ObservablePropertyAttribute.cs | 2 -- .../ObservableRecipientAttribute.cs | 2 -- .../Input/Attributes/ICommandAttribute.cs | 2 -- 17 files changed, 103 insertions(+), 81 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs index e3c356d62ae..4f4534b5818 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/INotifyPropertyChangedGenerator.cs @@ -3,23 +3,29 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { /// - /// A source generator for the type. + /// A source generator for the INotifyPropertyChangedAttribute type. /// [Generator] - public sealed class INotifyPropertyChangedGenerator : TransitiveMembersGenerator + public sealed class INotifyPropertyChangedGenerator : TransitiveMembersGenerator { + /// + /// Initializes a new instance of the class. + /// + public INotifyPropertyChangedGenerator() + : base("Microsoft.Toolkit.Mvvm.ComponentModel.INotifyPropertyChangedAttribute") + { + } + /// protected override DiagnosticDescriptor TargetTypeErrorDescriptor => INotifyPropertyChangedGeneratorError; @@ -31,7 +37,7 @@ protected override bool ValidateTargetType( INamedTypeSymbol classDeclarationSymbol, [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) { - INamedTypeSymbol iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanged).FullName)!; + INamedTypeSymbol iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged")!; // Check if the type already implements INotifyPropertyChanged if (classDeclarationSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iNotifyPropertyChangedSymbol))) @@ -55,7 +61,7 @@ protected override IEnumerable FilterDeclaredMembers( ClassDeclarationSyntax sourceDeclaration) { // If requested, only include the event and the basic methods to raise it, but not the additional helpers - if (attributeData.HasNamedArgument(nameof(INotifyPropertyChangedAttribute.IncludeAdditionalHelperMethods), false)) + if (attributeData.HasNamedArgument("IncludeAdditionalHelperMethods", false)) { return sourceDeclaration.Members.Where(static member => { diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs index 081c95fcf66..c1b1a37ffbc 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableObjectGenerator.cs @@ -2,22 +2,28 @@ // 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.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Toolkit.Mvvm.ComponentModel; using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { /// - /// A source generator for the type. + /// A source generator for the ObservableObjectAttribute type. /// [Generator] - public sealed class ObservableObjectGenerator : TransitiveMembersGenerator + public sealed class ObservableObjectGenerator : TransitiveMembersGenerator { + /// + /// Initializes a new instance of the class. + /// + public ObservableObjectGenerator() + : base("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObjectAttribute") + { + } + /// protected override DiagnosticDescriptor TargetTypeErrorDescriptor => ObservableObjectGeneratorError; @@ -30,8 +36,8 @@ protected override bool ValidateTargetType( [NotNullWhen(false)] out DiagnosticDescriptor? descriptor) { INamedTypeSymbol - iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanged).FullName)!, - iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanging).FullName)!; + iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged")!, + iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanging")!; // Check if the type already implements INotifyPropertyChanged... if (classDeclarationSymbol.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iNotifyPropertyChangedSymbol))) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs index bdaafa7b966..f71dfa367ac 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.SyntaxReceiver.cs @@ -7,7 +7,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Toolkit.Mvvm.ComponentModel; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -33,7 +32,7 @@ private sealed class SyntaxReceiver : ISyntaxContextReceiver public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { if (context.Node is FieldDeclarationSyntax { AttributeLists: { Count: > 0 } } fieldDeclaration && - context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(ObservablePropertyAttribute).FullName) is INamedTypeSymbol attributeSymbol) + context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservablePropertyAttribute") is INamedTypeSymbol attributeSymbol) { SyntaxTriviaList leadingTrivia = fieldDeclaration.GetLeadingTrivia(); diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index e54e8cac111..cec903e7990 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -11,7 +11,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics; using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -21,7 +20,7 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators { /// - /// A source generator for the type. + /// A source generator for the ObservablePropertyAttribute type. /// [Generator] public sealed partial class ObservablePropertyGenerator : ISourceGenerator @@ -86,9 +85,9 @@ private static void OnExecuteForProperties( ICollection propertyChangingNames) { INamedTypeSymbol - iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanging).FullName)!, + iNotifyPropertyChangingSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanging")!, observableObjectSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject")!, - observableObjectAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(ObservableObjectAttribute).FullName)!, + observableObjectAttributeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObjectAttribute")!, observableValidatorSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableValidator")!; // Check whether the current type implements INotifyPropertyChanging and whether it inherits from ObservableValidator @@ -149,7 +148,7 @@ private static void OnExecuteForProperties( .ToFullString(); // Add the partial type - context.AddSource($"[{typeof(ObservablePropertyAttribute).Name}]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"[ObservablePropertyAttribute]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); } /// @@ -178,7 +177,7 @@ private static PropertyDeclarationSyntax CreatePropertyDeclaration( typeName = fieldSymbol.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), propertyName = GetGeneratedPropertyName(fieldSymbol); - INamedTypeSymbol alsoNotifyChangeForAttributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(AlsoNotifyChangeForAttribute).FullName)!; + INamedTypeSymbol alsoNotifyChangeForAttributeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.AlsoNotifyChangeForAttribute")!; INamedTypeSymbol? validationAttributeSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.DataAnnotations.ValidationAttribute"); List dependentPropertyNotificationStatements = new(); @@ -474,8 +473,8 @@ public void OnExecuteForPropertyArgs(GeneratorExecutionContext context, IReadOnl } INamedTypeSymbol - propertyChangedEventArgsSymbol = context.Compilation.GetTypeByMetadataName(typeof(PropertyChangedEventArgs).FullName)!, - propertyChangingEventArgsSymbol = context.Compilation.GetTypeByMetadataName(typeof(PropertyChangingEventArgs).FullName)!; + propertyChangedEventArgsSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangedEventArgs")!, + propertyChangingEventArgsSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.PropertyChangingEventArgs")!; // Create a static method to validate all properties in a given class. // This code takes a class symbol and produces a compilation unit as follows: @@ -529,7 +528,7 @@ public void OnExecuteForPropertyArgs(GeneratorExecutionContext context, IReadOnl .ToFullString(); // Add the partial type - context.AddSource($"[{typeof(ObservablePropertyAttribute).Name}]_[__KnownINotifyPropertyChangedOrChangingArgs].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"[ObservablePropertyAttribute]_[__KnownINotifyPropertyChangedOrChangingArgs].cs", SourceText.From(source, Encoding.UTF8)); } /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs index 323c08c991c..7a2228bfde0 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableRecipientGenerator.cs @@ -3,13 +3,11 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics.DiagnosticDescriptors; @@ -17,11 +15,19 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators { /// - /// A source generator for the type. + /// A source generator for the ObservableRecipientAttribute type. /// [Generator] - public sealed class ObservableRecipientGenerator : TransitiveMembersGenerator + public sealed class ObservableRecipientGenerator : TransitiveMembersGenerator { + /// + /// Initializes a new instance of the class. + /// + public ObservableRecipientGenerator() + : base("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableRecipientAttribute") + { + } + /// protected override DiagnosticDescriptor TargetTypeErrorDescriptor => ObservableRecipientGeneratorError; @@ -37,7 +43,7 @@ protected override bool ValidateTargetType( observableRecipientSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableRecipient")!, observableObjectSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject")!, observableObjectAttributeSymbol = context.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObjectAttribute")!, - iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName(typeof(INotifyPropertyChanged).FullName)!; + iNotifyPropertyChangedSymbol = context.Compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged")!; // Check if the type already inherits from ObservableRecipient if (classDeclarationSymbol.InheritsFrom(observableRecipientSymbol)) @@ -53,7 +59,7 @@ protected override bool ValidateTargetType( !classDeclarationSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, observableObjectAttributeSymbol)) && !classDeclarationSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, iNotifyPropertyChangedSymbol) && - !a.HasNamedArgument(nameof(INotifyPropertyChangedAttribute.IncludeAdditionalHelperMethods), false))) + !a.HasNamedArgument("IncludeAdditionalHelperMethods", false))) { descriptor = MissingBaseObservableObjectFunctionalityError; diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs index 780b4c4d0fd..b370089d644 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.SyntaxReceiver.cs @@ -10,19 +10,33 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators { - /// - public abstract partial class TransitiveMembersGenerator + /// + public abstract partial class TransitiveMembersGenerator { /// /// An that selects candidate nodes to process. /// private sealed class SyntaxReceiver : ISyntaxContextReceiver { + /// + /// The fully qualified name of the attribute type to look for. + /// + private readonly string attributeTypeFullName; + /// /// The list of info gathered during exploration. /// private readonly List gatheredInfo = new(); + /// + /// Initializes a new instance of the class. + /// + /// The fully qualified name of the attribute type to look for. + public SyntaxReceiver(string attributeTypeFullName) + { + this.attributeTypeFullName = attributeTypeFullName; + } + /// /// Gets the collection of gathered info to process. /// @@ -33,7 +47,7 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { if (context.Node is ClassDeclarationSyntax { AttributeLists: { Count: > 0 } } classDeclaration && context.SemanticModel.GetDeclaredSymbol(classDeclaration) is INamedTypeSymbol classSymbol && - context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(TAttribute).FullName) is INamedTypeSymbol attributeSymbol && + context.SemanticModel.Compilation.GetTypeByMetadataName(this.attributeTypeFullName) is INamedTypeSymbol attributeSymbol && classSymbol.GetAttributes().FirstOrDefault(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attributeSymbol)) is AttributeData attributeData && attributeData.ApplicationSyntaxReference is SyntaxReference syntaxReference && syntaxReference.GetSyntax() is AttributeSyntax attributeSyntax) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index 6d2c61e58aa..fe3accbc18c 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -14,7 +14,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -using Microsoft.Toolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics; using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -23,12 +22,30 @@ namespace Microsoft.Toolkit.Mvvm.SourceGenerators { /// - /// A source generator for the type. + /// A source generator for a given attribute type. /// - /// The type of the source attribute to look for. - public abstract partial class TransitiveMembersGenerator : ISourceGenerator - where TAttribute : Attribute + public abstract partial class TransitiveMembersGenerator : ISourceGenerator { + /// + /// The fully qualified name of the attribute type to look for. + /// + private readonly string attributeTypeFullName; + + /// + /// The name of the attribute type to look for. + /// + private readonly string attributeTypeName; + + /// + /// Initializes a new instance of the class. + /// + /// The fully qualified name of the attribute type to look for. + protected TransitiveMembersGenerator(string attributeTypeFullName) + { + this.attributeTypeFullName = attributeTypeFullName; + this.attributeTypeName = attributeTypeFullName.Split('.').Last(); + } + /// /// Gets a indicating when the generation failed for a given type. /// @@ -37,7 +54,7 @@ public abstract partial class TransitiveMembersGenerator : ISourceGe /// public void Initialize(GeneratorInitializationContext context) { - context.RegisterForSyntaxNotifications(static () => new SyntaxReceiver()); + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver(this.attributeTypeFullName)); } /// @@ -78,9 +95,9 @@ public void Execute(GeneratorExecutionContext context) /// /// The syntax tree with the elements to emit in the generated code. [Pure] - private static SyntaxTree LoadSourceSyntaxTree() + private SyntaxTree LoadSourceSyntaxTree() { - string filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.EmbeddedResources.{typeof(TAttribute).Name.Replace("Attribute", string.Empty)}.cs"; + string filename = $"Microsoft.Toolkit.Mvvm.SourceGenerators.EmbeddedResources.{this.attributeTypeName.Replace("Attribute", string.Empty)}.cs"; Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(filename); StreamReader reader = new(stream); @@ -166,7 +183,7 @@ private void OnExecute( .ToFullString(); // Add the partial type - context.AddSource($"[{typeof(TAttribute).Name}]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"[{this.attributeTypeName}]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); } /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs index bf454c6e8f2..81fa09ee7ee 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -4,8 +4,6 @@ using System.ComponentModel; using Microsoft.CodeAnalysis; -using Microsoft.Toolkit.Mvvm.ComponentModel; -using Microsoft.Toolkit.Mvvm.Input; namespace Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics { @@ -71,11 +69,11 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor DuplicateINotifyPropertyChangedInterfaceForINotifyPropertyChangedAttributeError = new( id: "MVVMTK0004", title: $"Duplicate {nameof(INotifyPropertyChanged)} definition", - messageFormat: $"Cannot apply [{nameof(INotifyPropertyChangedAttribute)}] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanged)} interface", + messageFormat: $"Cannot apply [INotifyPropertyChanged] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanged)} interface", category: typeof(INotifyPropertyChangedGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: $"Cannot apply [{nameof(INotifyPropertyChangedAttribute)}] to a type that already declares the {nameof(INotifyPropertyChanged)} interface.", + description: $"Cannot apply [INotifyPropertyChanged] to a type that already declares the {nameof(INotifyPropertyChanged)} interface.", helpLinkUri: "https://aka.ms/mvvmtoolkit"); /// @@ -87,11 +85,11 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor DuplicateINotifyPropertyChangedInterfaceForObservableObjectAttributeError = new( id: "MVVMTK0005", title: $"Duplicate {nameof(INotifyPropertyChanged)} definition", - messageFormat: $"Cannot apply [{nameof(ObservableObjectAttribute)}] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanged)} interface", + messageFormat: $"Cannot apply [ObservableObject] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanged)} interface", category: typeof(ObservableObjectGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: $"Cannot apply [{nameof(ObservableObjectAttribute)}] to a type that already declares the {nameof(INotifyPropertyChanged)} interface.", + description: $"Cannot apply [ObservableObject] to a type that already declares the {nameof(INotifyPropertyChanged)} interface.", helpLinkUri: "https://aka.ms/mvvmtoolkit"); /// @@ -103,11 +101,11 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor DuplicateINotifyPropertyChangingInterfaceForObservableObjectAttributeError = new( id: "MVVMTK0006", title: $"Duplicate {nameof(INotifyPropertyChanging)} definition", - messageFormat: $"Cannot apply [{nameof(ObservableObjectAttribute)}] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanging)} interface", + messageFormat: $"Cannot apply [ObservableObject] to type {{0}}, as it already declares the {nameof(INotifyPropertyChanging)} interface", category: typeof(ObservableObjectGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: $"Cannot apply [{nameof(ObservableObjectAttribute)}] to a type that already declares the {nameof(INotifyPropertyChanging)} interface.", + description: $"Cannot apply [ObservableObject] to a type that already declares the {nameof(INotifyPropertyChanging)} interface.", helpLinkUri: "https://aka.ms/mvvmtoolkit"); /// @@ -119,15 +117,15 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor DuplicateObservableRecipientError = new( id: "MVVMTK0007", title: "Duplicate ObservableRecipient definition", - messageFormat: $"Cannot apply [{nameof(ObservableRecipientAttribute)}] to type {{0}}, as it already inherits from the ObservableRecipient class", + messageFormat: $"Cannot apply [ObservableRecipient] to type {{0}}, as it already inherits from the ObservableRecipient class", category: typeof(ObservableRecipientGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: $"Cannot apply [{nameof(ObservableRecipientAttribute)}] to a type that already inherits from the ObservableRecipient class.", + description: $"Cannot apply [ObservableRecipient] to a type that already inherits from the ObservableRecipient class.", helpLinkUri: "https://aka.ms/mvvmtoolkit"); /// - /// Gets a indicating when there is a missing base functionality to enable . + /// Gets a indicating when there is a missing base functionality to enable ObservableRecipientAttribute. /// /// Format: "Cannot apply [ObservableRecipientAttribute] to type {0}, as it lacks necessary base functionality (it should either inherit from ObservableObject, or be annotated with [ObservableObjectAttribute] or [INotifyPropertyChangedAttribute])". /// @@ -135,11 +133,11 @@ internal static class DiagnosticDescriptors public static readonly DiagnosticDescriptor MissingBaseObservableObjectFunctionalityError = new( id: "MVVMTK0008", title: "Missing base ObservableObject functionality", - messageFormat: $"Cannot apply [{nameof(ObservableRecipientAttribute)}] to type {{0}}, as it lacks necessary base functionality (it should either inherit from ObservableObject, or be annotated with [{nameof(ObservableObjectAttribute)}] or [{nameof(INotifyPropertyChangedAttribute)}])", + messageFormat: $"Cannot apply [ObservableRecipient] to type {{0}}, as it lacks necessary base functionality (it should either inherit from ObservableObject, or be annotated with [ObservableObject] or [INotifyPropertyChanged])", category: typeof(ObservableRecipientGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: $"Cannot apply [{nameof(ObservableRecipientAttribute)}] to a type that lacks necessary base functionality (it should either inherit from ObservableObject, or be annotated with [{nameof(ObservableObjectAttribute)}] or [{nameof(INotifyPropertyChangedAttribute)}]).", + description: $"Cannot apply [ObservableRecipient] to a type that lacks necessary base functionality (it should either inherit from ObservableObject, or be annotated with [ObservableObject] or [INotifyPropertyChanged]).", helpLinkUri: "https://aka.ms/mvvmtoolkit"); /// @@ -155,7 +153,7 @@ internal static class DiagnosticDescriptors category: typeof(ObservablePropertyGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: $"Cannot apply [{nameof(ObservablePropertyAttribute)}] to fields with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.", + description: $"Cannot apply [ObservableProperty] to fields with validation attributes if they are declared in a type that doesn't inherit from ObservableValidator.", helpLinkUri: "https://aka.ms/mvvmtoolkit"); /// @@ -203,7 +201,7 @@ internal static class DiagnosticDescriptors category: typeof(ICommandGenerator).FullName, defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, - description: $"Cannot apply [{nameof(ICommandAttribute)}] to methods with a signature that doesn't match any of the existing relay command types.", + description: $"Cannot apply [ICommand] to methods with a signature that doesn't match any of the existing relay command types.", helpLinkUri: "https://aka.ms/mvvmtoolkit"); } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs index d4d73358f5e..5e365a90b97 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.SyntaxReceiver.cs @@ -7,7 +7,6 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.Toolkit.Mvvm.Input; namespace Microsoft.Toolkit.Mvvm.SourceGenerators { @@ -34,7 +33,7 @@ public void OnVisitSyntaxNode(GeneratorSyntaxContext context) { if (context.Node is MethodDeclarationSyntax methodDeclaration && context.SemanticModel.GetDeclaredSymbol(methodDeclaration) is IMethodSymbol methodSymbol && - context.SemanticModel.Compilation.GetTypeByMetadataName(typeof(ICommandAttribute).FullName) is INamedTypeSymbol iCommandSymbol && + context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.Toolkit.Mvvm.Input.ICommandAttribute") is INamedTypeSymbol iCommandSymbol && methodSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, iCommandSymbol))) { this.gatheredInfo.Add(new Item(methodDeclaration.GetLeadingTrivia(), methodSymbol)); diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs index ee8350974ac..541dc25693a 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -13,7 +13,6 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; -using Microsoft.Toolkit.Mvvm.Input; using Microsoft.Toolkit.Mvvm.SourceGenerators.Diagnostics; using Microsoft.Toolkit.Mvvm.SourceGenerators.Extensions; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; @@ -115,7 +114,7 @@ private static void OnExecute( .ToFullString(); // Add the partial type - context.AddSource($"[{typeof(ICommandAttribute).Name}]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"[ICommandAttribute]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); } /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj index 427fcf63ee1..8e718f68287 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Microsoft.Toolkit.Mvvm.SourceGenerators.csproj @@ -13,15 +13,6 @@ - - - - - - - - - PreserveNewest diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs index 06651c7c77d..ae5a66feaf3 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/AlsoNotifyChangeForAttribute.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#pragma warning disable CS1574 - using System; using System.ComponentModel; using System.Linq; diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs index e23410bdcd3..eef91ae0ef8 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/INotifyPropertyChangedAttribute.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#pragma warning disable CS1574 - using System; using System.ComponentModel; diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs index ef900966900..72c4b078c85 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableObjectAttribute.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#pragma warning disable CS1574 - using System; using System.ComponentModel; diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs index e0fd3e71ff7..2395ec31ad9 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservablePropertyAttribute.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#pragma warning disable CS1574 - using System; using System.ComponentModel; diff --git a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs index 7d92aa45e92..382cce5c7e1 100644 --- a/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/ComponentModel/Attributes/ObservableRecipientAttribute.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#pragma warning disable CS1574 - using System; namespace Microsoft.Toolkit.Mvvm.ComponentModel diff --git a/Microsoft.Toolkit.Mvvm/Input/Attributes/ICommandAttribute.cs b/Microsoft.Toolkit.Mvvm/Input/Attributes/ICommandAttribute.cs index 3ece775a72a..2a701d86b72 100644 --- a/Microsoft.Toolkit.Mvvm/Input/Attributes/ICommandAttribute.cs +++ b/Microsoft.Toolkit.Mvvm/Input/Attributes/ICommandAttribute.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -#pragma warning disable CS1574 - using System; using System.Windows.Input; From fa939b48518c2c43cdc5bf33c918d6fe6d9f636f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 23 Jul 2021 15:09:00 +0200 Subject: [PATCH 89/89] Simplified generated filenames Adding the type name of the generator is not necessary since Visual Studio already groups generated sources for each individual generator, which also automatically avoids conflicts --- .../ComponentModel/ObservablePropertyGenerator.cs | 4 ++-- .../ObservableValidatorValidateAllPropertiesGenerator.cs | 4 ++-- .../ComponentModel/TransitiveMembersGenerator.cs | 2 +- .../Input/ICommandGenerator.cs | 4 ++-- .../Messaging/IMessengerRegisterAllGenerator.cs | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs index cec903e7990..e5b9402e042 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservablePropertyGenerator.cs @@ -148,7 +148,7 @@ private static void OnExecuteForProperties( .ToFullString(); // Add the partial type - context.AddSource($"[ObservablePropertyAttribute]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"{classDeclarationSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8)); } /// @@ -528,7 +528,7 @@ public void OnExecuteForPropertyArgs(GeneratorExecutionContext context, IReadOnl .ToFullString(); // Add the partial type - context.AddSource($"[ObservablePropertyAttribute]_[__KnownINotifyPropertyChangedOrChangingArgs].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource("__KnownINotifyPropertyChangedOrChangingArgs.cs", SourceText.From(source, Encoding.UTF8)); } /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs index f2e3bc78205..45a6bb3d449 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/ObservableValidatorValidateAllPropertiesGenerator.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -154,7 +154,7 @@ public void Execute(GeneratorExecutionContext context) classAttributes = Array.Empty(); // Add the partial type - context.AddSource($"[ObservableValidator]_[{classSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"{classSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8)); } } diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs index fe3accbc18c..bd3f48e284c 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/ComponentModel/TransitiveMembersGenerator.cs @@ -183,7 +183,7 @@ private void OnExecute( .ToFullString(); // Add the partial type - context.AddSource($"[{this.attributeTypeName}]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"{classDeclarationSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8)); } /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs index 541dc25693a..ffcca24f30a 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Input/ICommandGenerator.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -114,7 +114,7 @@ private static void OnExecute( .ToFullString(); // Add the partial type - context.AddSource($"[ICommandAttribute]_[{classDeclarationSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"{classDeclarationSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8)); } /// diff --git a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs index 4e8ba909211..91e4925dd99 100644 --- a/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs +++ b/Microsoft.Toolkit.Mvvm.SourceGenerators/Messaging/IMessengerRegisterAllGenerator.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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. @@ -209,7 +209,7 @@ public void Execute(GeneratorExecutionContext context) classAttributes = Array.Empty(); // Add the partial type - context.AddSource($"[IRecipient{{T}}]_[{classSymbol.GetFullMetadataNameForFileName()}].cs", SourceText.From(source, Encoding.UTF8)); + context.AddSource($"{classSymbol.GetFullMetadataNameForFileName()}.cs", SourceText.From(source, Encoding.UTF8)); } }