diff --git a/README.md b/README.md index 4d9ecdb..6d5a239 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ ReactiveUI Source Generators automatically generate ReactiveUI objects to stream - `[ViewModelControlHost("YourNameSpace.CustomControl")]` - `[BindableDerivedList]` Generates a derived list from a ReadOnlyObservableCollection backing field - `[ReactiveCollection]` Generates property changed notifications on add, remove, new actions on a ObservableCollection backing field +- `[IReactiveObject]` Generates IReactiveObject implementation for classes not able to inherit from ReactiveObject #### IViewFor Registration generator @@ -684,6 +685,19 @@ public partial class MyReactiveClass : ReactiveObject } ``` +### ReactiveObject implementation for classes not able to inherit from ReactiveObject +```csharp +using ReactiveUI; +using ReactiveUI.SourceGenerators; + +[IReactiveObject] +public partial class MyReactiveClass +{ + [Reactive] + private string _myProperty; +} +``` + ### TODO: - Add ObservableAsProperty to generate from a IObservable method with parameters. diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 1462b40..bcdbe60 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -10,7 +10,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// ReativeCommandAttribute. +/// ReactiveCommand Attribute. /// /// [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)] diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 1462b40..bcdbe60 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveAsyncCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -10,7 +10,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// ReativeCommandAttribute. +/// ReactiveCommand Attribute. /// /// [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)] diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 1462b40..bcdbe60 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommand#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -10,7 +10,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// ReativeCommandAttribute. +/// ReactiveCommand Attribute. /// /// [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)] diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithAccessModifier#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithAccessModifier#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 1462b40..bcdbe60 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithAccessModifier#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithAccessModifier#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -10,7 +10,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// ReativeCommandAttribute. +/// ReactiveCommand Attribute. /// /// [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)] diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNestedClasses#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNestedClasses#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 1462b40..bcdbe60 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNestedClasses#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNestedClasses#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -10,7 +10,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// ReativeCommandAttribute. +/// ReactiveCommand Attribute. /// /// [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)] diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNullableTypeAndNullableReturnType#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNullableTypeAndNullableReturnType#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 1462b40..bcdbe60 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNullableTypeAndNullableReturnType#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithNullableTypeAndNullableReturnType#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -10,7 +10,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// ReativeCommandAttribute. +/// ReactiveCommand Attribute. /// /// [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)] diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithOutputScheduler#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithOutputScheduler#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 1462b40..bcdbe60 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithOutputScheduler#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithOutputScheduler#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -10,7 +10,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// ReativeCommandAttribute. +/// ReactiveCommand Attribute. /// /// [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)] diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs index 1462b40..bcdbe60 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVECMD/ReactiveCMDGeneratorTests.FromReactiveCommandWithParameter#ReactiveUI.SourceGenerators.ReactiveCommandAttribute.g.verified.cs @@ -10,7 +10,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// ReativeCommandAttribute. +/// ReactiveCommand Attribute. /// /// [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)] diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVEOBJ/ReactiveObjectGeneratorTests.FromReactiveObject#ReactiveUI.SourceGenerators.IReactiveObjectAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVEOBJ/ReactiveObjectGeneratorTests.FromReactiveObject#ReactiveUI.SourceGenerators.IReactiveObjectAttribute.g.verified.cs new file mode 100644 index 0000000..3a7be2f --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVEOBJ/ReactiveObjectGeneratorTests.FromReactiveObject#ReactiveUI.SourceGenerators.IReactiveObjectAttribute.g.verified.cs @@ -0,0 +1,19 @@ +//HintName: ReactiveUI.SourceGenerators.IReactiveObjectAttribute.g.cs +// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved. +// 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 full license information. + +// +#pragma warning disable +#nullable enable +namespace ReactiveUI.SourceGenerators; + +/// +/// IReactiveObject Attribute. +/// +/// +[global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +internal sealed class IReactiveObjectAttribute : global::System.Attribute; +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVEOBJ/ReactiveObjectGeneratorTests.FromReactiveObject#TestNs.TestVM.IReactiveObject.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVEOBJ/ReactiveObjectGeneratorTests.FromReactiveObject#TestNs.TestVM.IReactiveObject.g.verified.cs new file mode 100644 index 0000000..cb482d4 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVEOBJ/ReactiveObjectGeneratorTests.FromReactiveObject#TestNs.TestVM.IReactiveObject.g.verified.cs @@ -0,0 +1,68 @@ +//HintName: TestNs.TestVM.IReactiveObject.g.cs +// +#pragma warning disable +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; +using System.ComponentModel; +using ReactiveUI; + +namespace TestNs +{ + /// + /// Partial class for the TestVM which contains ReactiveUI IReactiveObject initialization. + /// + public partial class TestVM : IReactiveObject + { + private bool _propertyChangingEventsSubscribed; + private bool _propertyChangedEventsSubscribed; + + /// + public event PropertyChangingEventHandler? PropertyChanging + { + add + { + if (!_propertyChangingEventsSubscribed) + { + this.SubscribePropertyChangingEvents(); + _propertyChangingEventsSubscribed = true; + } + + PropertyChangingHandler += value; + } + remove => PropertyChangingHandler -= value; + } + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (!_propertyChangedEventsSubscribed) + { + this.SubscribePropertyChangedEvents(); + _propertyChangedEventsSubscribed = true; + } + + PropertyChangedHandler += value; + } + remove => PropertyChangedHandler -= value; + } + + [SuppressMessage("Roslynator", "RCS1159:Use EventHandler", Justification = "Long term design.")] + private event PropertyChangingEventHandler? PropertyChangingHandler; + + [SuppressMessage("Roslynator", "RCS1159:Use EventHandler", Justification = "Long term design.")] + private event PropertyChangedEventHandler? PropertyChangedHandler; + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => + PropertyChangingHandler?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => + PropertyChangedHandler?.Invoke(this, args); + } +} +#nullable restore +#pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj b/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj index 8630fcf..f89d767 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj +++ b/src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj @@ -40,6 +40,7 @@ + diff --git a/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs b/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs index 8309201..6f471a3 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs @@ -87,6 +87,7 @@ public string VerifiedFilePath() nameof(ViewModelControlHostGenerator) => "CONTROLHOST", nameof(BindableDerivedListGenerator) => "DERIVEDLIST", nameof(ReactiveCollectionGenerator) => "REACTIVECOLL", + nameof(ReactiveObjectGenerator) => "REACTIVEOBJ", _ => name, }; } diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs new file mode 100644 index 0000000..fde4731 --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/ReactiveObjectGeneratorTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2025 ReactiveUI and contributors. All rights reserved. +// Licensed to the ReactiveUI and contributors under one or more agreements. +// The ReactiveUI and contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using ReactiveUI.SourceGenerators; + +namespace ReactiveUI.SourceGenerator.Tests; + +/// +/// Unit tests for the Reactive generator. +/// +[TestFixture] +public class ReactiveObjectGeneratorTests : TestBase +{ + /// + /// Tests the ReactiveObject generator with IReactiveObjectAttribute. + /// + /// A task to monitor the async. + [Test] + public Task FromReactiveObject() + { + // Arrange: Setup the source code that matches the generator input expectations. + const string sourceCode = """ + using System; + using ReactiveUI.SourceGenerators; + using System.Reactive.Linq; + namespace TestNs; + + [IReactiveObject] + public partial class TestVM + { + [Reactive] + private int _test1 = 10; + } + """; + + // Act: Initialize the helper and run the generator. Assert: Verify the generated code. + return TestHelper.TestPass(sourceCode); + } +} diff --git a/src/ReactiveUI.SourceGenerators.Execute/Person.cs b/src/ReactiveUI.SourceGenerators.Execute/Person.cs index 2c05550..1958f51 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/Person.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/Person.cs @@ -4,7 +4,6 @@ // See the LICENSE file in the project root for full license information. using System.Diagnostics.CodeAnalysis; -using ReactiveUI; using ReactiveUI.SourceGenerators; namespace SGReactiveUI.SourceGenerators.Test; @@ -14,7 +13,8 @@ namespace SGReactiveUI.SourceGenerators.Test; /// /// [ExcludeFromCodeCoverage] -public partial class Person : ReactiveObject +[IReactiveObject] +public partial class Person { /// /// Gets or sets a value indicating whether this is deleted. @@ -23,5 +23,5 @@ public partial class Person : ReactiveObject /// true if deleted; otherwise, false. /// [Reactive] - public bool Deleted { get; set; } + public partial bool Deleted { get; set; } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs index ccecb84..2cdfdd8 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs @@ -77,6 +77,8 @@ internal enum SplatRegistrationType #pragma warning restore """; + public const string ReactiveObjectAttributeType = "ReactiveUI.SourceGenerators.IReactiveObjectAttribute"; + public static string ReactiveObjectAttribute => $$""" // Copyright (c) {{DateTime.Now.Year}} .NET Foundation and Contributors. All rights reserved. // Licensed to the .NET Foundation under one or more agreements. @@ -89,12 +91,12 @@ internal enum SplatRegistrationType namespace ReactiveUI.SourceGenerators; /// -/// ReactiveObjectAttribute. +/// IReactiveObject Attribute. /// /// [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveObjectGenerator", "{{ReactiveGenerator.GeneratorVersion}}")] [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -internal sealed class ReactiveObjectAttribute : global::System.Attribute; +internal sealed class IReactiveObjectAttribute : global::System.Attribute; #nullable restore #pragma warning restore """; @@ -119,7 +121,7 @@ internal sealed class ReactiveObjectAttribute : global::System.Attribute; namespace ReactiveUI.SourceGenerators; /// -/// ReativeCommandAttribute. +/// ReactiveCommand Attribute. /// /// [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveCommandGenerator", "{{ReactiveGenerator.GeneratorVersion}}")] diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs index fdf8b79..51f9e2f 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs @@ -3,8 +3,6 @@ // The ReactiveUI and contributors licenses this file to you under the MIT license. // See the LICENSE file in the project root for full license information. -using System; -using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text; diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs index fb62a63..067883a 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs @@ -5,6 +5,7 @@ using System.Globalization; using Microsoft.CodeAnalysis; +using ReactiveUI.SourceGenerators.Helpers; namespace ReactiveUI.SourceGenerators.Extensions; @@ -124,7 +125,7 @@ internal static bool IsTargetTypeValid(this IFieldSymbol fieldSymbol) { var isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); var isIObservableObject = fieldSymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); + var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveObjectAttributeType); return isIObservableObject || isObservableObject || hasObservableObjectAttribute; } @@ -138,7 +139,7 @@ internal static bool IsTargetTypeValid(this IPropertySymbol propertySymbol) { var isObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); var isIObservableObject = propertySymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = propertySymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); + var hasObservableObjectAttribute = propertySymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveObjectAttributeType); return isIObservableObject || isObservableObject || hasObservableObjectAttribute; } @@ -152,7 +153,7 @@ internal static bool IsTargetTypeValid(this IMethodSymbol methodSymbol) { var isObservableObject = methodSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject"); var isIObservableObject = methodSymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject"); - var hasObservableObjectAttribute = methodSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute"); + var hasObservableObjectAttribute = methodSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveObjectAttributeType); return isIObservableObject || isObservableObject || hasObservableObjectAttribute; } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs index 700bfef..d55ee07 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs @@ -26,8 +26,6 @@ public partial class IViewForGenerator internal static readonly string GeneratorName = typeof(IViewForGenerator).FullName!; internal static readonly string GeneratorVersion = typeof(IViewForGenerator).Assembly.GetName().Version.ToString(); - private static readonly string[] excludeFromCodeCoverage = ["[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"]; - private static IViewForInfo? GetClassInfo(in GenericGeneratorAttributeSyntaxContext context, CancellationToken token) { if (!(context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword))) @@ -126,7 +124,7 @@ public partial class IViewForGenerator private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, IViewForInfo iviewForInfo) { // Prepare any forwarded property attributes - var forwardedAttributesString = string.Join("\n ", excludeFromCodeCoverage); + var forwardedAttributesString = string.Join("\n ", AttributeDefinitions.ExcludeFromCodeCoverage); switch (iviewForInfo.BaseType) { diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs index fef7d26..1bb994e 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs @@ -14,7 +14,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// A source generator for generating reative properties. +/// A source generator for generating reactive properties. /// [Generator(LanguageNames.CSharp)] public sealed partial class IViewForGenerator : IIncrementalGenerator diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs index ed6ee33..838816d 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromField}.cs @@ -14,7 +14,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// A source generator for generating reative properties. +/// A source generator for generating reactive properties. /// public sealed partial class ObservableAsPropertyGenerator { diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs index 038b534..3f100d8 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.cs @@ -14,7 +14,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// A source generator for generating reative properties. +/// A source generator for generating reactive properties. /// public sealed partial class ObservableAsPropertyGenerator { diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.cs index c9f7fa5..f4469c4 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/Reactive/ReactiveGenerator.cs @@ -14,7 +14,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// A source generator for generating reative properties. +/// A source generator for generating reactive properties. /// [Generator(LanguageNames.CSharp)] public sealed partial class ReactiveGenerator : IIncrementalGenerator diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.cs index 5114905..6d717ac 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCollection/ReactiveCollectionGenerator.cs @@ -14,7 +14,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// A source generator for generating reative properties. +/// A source generator for generating reactive properties. /// [Generator(LanguageNames.CSharp)] public sealed partial class ReactiveCollectionGenerator : IIncrementalGenerator diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.cs index e40aef2..e2f3822 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveCommand/ReactiveCommandGenerator.cs @@ -14,7 +14,7 @@ namespace ReactiveUI.SourceGenerators; /// -/// A source generator for generating reative properties. +/// A source generator for generating reactive properties. /// [Generator(LanguageNames.CSharp)] public sealed partial class ReactiveCommandGenerator : IIncrementalGenerator diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/Models/ReactiveObjectInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/Models/ReactiveObjectInfo.cs new file mode 100644 index 0000000..8181866 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/Models/ReactiveObjectInfo.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2025 ReactiveUI and contributors. All rights reserved. +// Licensed to the ReactiveUI and contributors under one or more agreements. +// The ReactiveUI and contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.SourceGenerators.Models; + +/// +/// A model with gathered information about a generated ReactiveObject (view model). +/// +internal sealed record ReactiveObjectInfo( + TargetInfo TargetInfo); diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.Execute.cs new file mode 100644 index 0000000..65d076d --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.Execute.cs @@ -0,0 +1,119 @@ +// Copyright (c) 2025 ReactiveUI and contributors. All rights reserved. +// Licensed to the ReactiveUI and contributors under one or more agreements. +// The ReactiveUI and contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.Extensions; +using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Models; + +namespace ReactiveUI.SourceGenerators; + +/// +/// A source generator for generating reactiveObject properties. +/// +public partial class ReactiveObjectGenerator +{ + internal static readonly string GeneratorName = typeof(ReactiveObjectGenerator).FullName!; + internal static readonly string GeneratorVersion = typeof(ReactiveObjectGenerator).Assembly.GetName().Version.ToString(); + + private static ReactiveObjectInfo? GetClassInfo(in GenericGeneratorAttributeSyntaxContext context, CancellationToken token) + { + if (!(context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword))) + { + return default; + } + + var symbol = context.TargetSymbol; + token.ThrowIfCancellationRequested(); + + if (symbol is not INamedTypeSymbol classSymbol) + { + return default; + } + + token.ThrowIfCancellationRequested(); + + // Get the containing type info + var targetInfo = TargetInfo.From(classSymbol); + + token.ThrowIfCancellationRequested(); + return new(targetInfo); + } + + private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, ReactiveObjectInfo reactiveObjectInfo) => + $$""" +// +#pragma warning disable +#nullable enable +using System; +using System.Diagnostics.CodeAnalysis; +using System.ComponentModel; +using ReactiveUI; + +namespace {{containingNamespace}} +{ + /// + /// Partial class for the {{containingTypeName}} which contains ReactiveUI IReactiveObject initialization. + /// + {{containingClassVisibility}} partial {{containingType}} {{containingTypeName}} : IReactiveObject + { + private bool _propertyChangingEventsSubscribed; + private bool _propertyChangedEventsSubscribed; + + /// + public event PropertyChangingEventHandler? PropertyChanging + { + add + { + if (!_propertyChangingEventsSubscribed) + { + this.SubscribePropertyChangingEvents(); + _propertyChangingEventsSubscribed = true; + } + + PropertyChangingHandler += value; + } + remove => PropertyChangingHandler -= value; + } + + /// + public event PropertyChangedEventHandler? PropertyChanged + { + add + { + if (!_propertyChangedEventsSubscribed) + { + this.SubscribePropertyChangedEvents(); + _propertyChangedEventsSubscribed = true; + } + + PropertyChangedHandler += value; + } + remove => PropertyChangedHandler -= value; + } + + [SuppressMessage("Roslynator", "RCS1159:Use EventHandler", Justification = "Long term design.")] + private event PropertyChangingEventHandler? PropertyChangingHandler; + + [SuppressMessage("Roslynator", "RCS1159:Use EventHandler", Justification = "Long term design.")] + private event PropertyChangedEventHandler? PropertyChangedHandler; + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => + PropertyChangingHandler?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => + PropertyChangedHandler?.Invoke(this, args); + } +} +#nullable restore +#pragma warning restore +"""; +} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.cs new file mode 100644 index 0000000..afba4e5 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ReactiveObject/ReactiveObjectGenerator.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2025 ReactiveUI and contributors. All rights reserved. +// Licensed to the ReactiveUI and contributors under one or more agreements. +// The ReactiveUI and contributors licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using ReactiveUI.SourceGenerators.Helpers; + +namespace ReactiveUI.SourceGenerators; + +/// +/// A source generator for generating reactive properties. +/// +[Generator(LanguageNames.CSharp)] +public sealed partial class ReactiveObjectGenerator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => + ctx.AddSource(AttributeDefinitions.ReactiveObjectAttributeType + ".g.cs", SourceText.From(AttributeDefinitions.ReactiveObjectAttribute, Encoding.UTF8))); + + // Gather info for all annotated IReactiveObject Classes + var reactiveObjectInfo = + context.SyntaxProvider + .ForAttributeWithMetadataNameWithGenerics( + AttributeDefinitions.ReactiveObjectAttributeType, + static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, + static (context, token) => GetClassInfo(context, token)) + .Where(x => x != null) + .Select((x, _) => x!) + .Collect(); + + // Generate the requested properties and methods for IReactiveObject + context.RegisterSourceOutput(reactiveObjectInfo, static (context, input) => + { + var groupedPropertyInfo = input.GroupBy( + static info => (info.TargetInfo.FileHintName, info.TargetInfo.TargetName, info.TargetInfo.TargetNamespace, info.TargetInfo.TargetVisibility, info.TargetInfo.TargetType), + static info => info) + .ToImmutableArray(); + + foreach (var grouping in groupedPropertyInfo) + { + var items = grouping.ToImmutableArray(); + + if (items.Length == 0) + { + continue; + } + + var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.FirstOrDefault()); + context.AddSource(grouping.Key.FileHintName + ".IReactiveObject.g.cs", source); + } + }); + } +} diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.cs index 5fe349e..9ea0bf6 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/RoutedControlHost/RoutedControlHostGenerator.cs @@ -15,7 +15,7 @@ namespace ReactiveUI.SourceGenerators.WinForms; /// -/// A source generator for generating reative properties. +/// A source generator for generating reactive properties. /// [Generator(LanguageNames.CSharp)] public sealed partial class RoutedControlHostGenerator : IIncrementalGenerator diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.cs index 3984534..d0922b4 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/ViewModelControlHost/ViewModelControlHostGenerator.cs @@ -14,7 +14,7 @@ namespace ReactiveUI.SourceGenerators.WinForms; /// -/// A source generator for generating reative properties. +/// A source generator for generating reactive properties. /// [Generator(LanguageNames.CSharp)] public sealed partial class ViewModelControlHostGenerator : IIncrementalGenerator