From dd0c8e6604db8e27f725e8a1f94156faf1cf8233 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Mon, 1 Sep 2025 22:33:40 +0100 Subject: [PATCH 1/4] Add Attribute Property --- .../AttributeDefinitions.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs index 950d6c3..1afc984 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs @@ -52,6 +52,14 @@ internal enum InheritanceModifier Override, New, } + + internal enum SplatRegistrationType + { + None, + LazySingleton, + Constant, + PerRequest, + } #nullable restore #pragma warning restore """; @@ -344,7 +352,14 @@ namespace ReactiveUI.SourceGenerators; /// Type of the view model. [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.IViewForGenerator", "1.1.0.0")] [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -internal sealed class IViewForAttribute : global::System.Attribute; +internal sealed class IViewForAttribute : global::System.Attribute +{ + /// + /// Gets the Splat registration type for Splat registration. + /// Registers IViewFor in the Splat service locator. + /// + public SplatRegistrationType RegistrationType { get; init; } = SplatRegistrationType.None; +} /// /// IViewForAttribute. @@ -356,7 +371,14 @@ internal sealed class IViewForAttribute : global::System.Attribute; /// Type of the view model. [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.IViewForGenerator", "1.1.0.0")] [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -internal sealed class IViewForAttribute(string? viewModelType) : global::System.Attribute; +internal sealed class IViewForAttribute(string? viewModelType) : global::System.Attribute +{ + /// + /// Gets the Splat registration type for Splat registration. + /// Registers IViewFor in the Splat service locator. + /// + public SplatRegistrationType RegistrationType { get; init; } = SplatRegistrationType.None; +} #nullable restore #pragma warning restore """; From 7ffa8207ecc55a61749fbba3aab2bb2926e3454d Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Tue, 2 Sep 2025 00:43:06 +0100 Subject: [PATCH 2/4] Add Splat registration support to IViewFor generator Introduces SplatRegistrationType to IViewFor attributes and source generator, enabling automatic registration of views for view models in Splat's service locator. Updates documentation, tests, and sample usage to demonstrate new registration options and extension method for bulk registration. --- README.md | 47 ++++++++++- src/Directory.Packages.props | 3 +- ...UISourceGeneratorsExtensions.g.verified.cs | 24 ++++++ ...Generators.IViewForAttribute.g.verified.cs | 20 ++++- ...rceGenerators.AccessModifier.g.verified.cs | 8 ++ ...rceGenerators.AccessModifier.g.verified.cs | 8 ++ ...rceGenerators.AccessModifier.g.verified.cs | 8 ++ ...rceGenerators.AccessModifier.g.verified.cs | 8 ++ ...rceGenerators.AccessModifier.g.verified.cs | 8 ++ ...rceGenerators.AccessModifier.g.verified.cs | 8 ++ ...rceGenerators.AccessModifier.g.verified.cs | 8 ++ ...rceGenerators.AccessModifier.g.verified.cs | 8 ++ ...rceGenerators.AccessModifier.g.verified.cs | 8 ++ .../Program.cs | 9 +- ...ReactiveUI.SourceGenerators.Execute.csproj | 1 + .../TestViewWinForms.cs | 2 +- .../TestViewWpf.cs | 9 +- .../TestViewWpf2.cs | 2 +- .../AttributeDefinitions.cs | 4 +- .../IViewFor/IViewForGenerator.Execute.cs | 82 ++++++++++++++++++- .../IViewFor/IViewForGenerator.cs | 13 ++- .../IViewFor/Models/IViewForInfo.cs | 3 +- 22 files changed, 275 insertions(+), 16 deletions(-) create mode 100644 src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs diff --git a/README.md b/README.md index f9986d6..8f60e7e 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,23 @@ ReactiveUI Source Generators automatically generate ReactiveUI objects to stream - `[ReactiveCommand(OutputScheduler = nameof(_isheduler))]` using a Scheduler defined in the class - `[ReactiveCommand][property: AttributeToAddToCommand]` with Attribute passthrough - `[IViewFor(nameof(ViewModelName))]` +- `[IViewFor]` +- `[IViewFor("YourNameSpace.YourGenericViewModel")]` Generic +- `[IViewFor(RegistrationType = SplatRegistrationType.PerRequest)]` with Splat Registration Type for IViewFor registration. +- `[IViewFor(RegistrationType = SplatRegistrationType.LazySingleton)]` Generic with Splat Registration Type for IViewFor registration. +- `[IViewFor(RegistrationType = SplatRegistrationType.Constant)]` Generic with Splat Registration Type for IViewFor registration. - `[RoutedControlHost("YourNameSpace.CustomControl")]` - `[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 +#### IViewFor Registration generator + +To register all views for view models registered via the IViewFor Source Generator with a specified `RegistrationType`, call the following method during application startup: +```csharp +Splat.Locator.CurrentMutable.RegisterViewsForViewModelsSourceGenerated(); +``` + ### Compatibility Notes - For ReactiveUI versions **older than V19.5.31**, all `[ReactiveCommand]` options are supported except for async methods with a `CancellationToken`. - For **.NET Framework 4.8 and older**, add [Polyfill by Simon Cropp](https://github.com/SimonCropp/Polyfill) or [PolySharp by Sergio Pedri](https://github.com/Sergio0694/PolySharp) to your project and set the `LangVersion` to 12.0 or later in your project file. @@ -63,11 +75,17 @@ Generates read-only properties backed by an `ObservableAsPropertyHelper` based o Generates commands, with options to add attributes or enable `CanExecute` functionality. ### `[IViewFor]` -Links a view to a view model for data binding. +Links a view to a view model for data binding. Supports generic types and Splat registration. ### `[RoutedControlHost]` and `[ViewModelControlHost]` Platform-specific attributes for control hosting in WinForms applications. +### `[BindableDerivedList]` +Generates a derived list from a `ReadOnlyObservableCollection` backing field. + +### `[ReactiveCollection]` +Generates property changed notifications on add, remove, and new actions on an `ObservableCollection` backing field. + ## Historical Approach ### Read-Write Properties @@ -525,6 +543,33 @@ The class must inherit from a UI Control from any of the following platforms and - Avalonia (Avalonia) - Uno (Windows.UI.Xaml). +### IViewFor with Splat Registration Type + +Choose from the following Splat Registration Types: +- `SplatRegistrationType.PerRequest` +- `SplatRegistrationType.LazySingleton` +- `SplatRegistrationType.Constant` +- `SplatRegistrationType.None` (Default if not specified - no registration is performed) + +```csharp +using ReactiveUI.SourceGenerators; +using Splat; +[IViewFor(RegistrationType = SplatRegistrationType.PerRequest)] +public partial class MyReactiveControl : UserControl +{ + public MyReactiveControl() + { + InitializeComponent(); + ViewModel = Locator.Current.GetService(); + } +} +``` + +this will generate the following code to enable you register the marked Views as `IViewFor` with Splat: +```csharp +Splat.Locator.CurrentMutable.RegisterViewsForViewModelsSourceGenerated(); +``` + ### Usage IViewFor with ViewModel Name - Generic Types should be used with the fully qualified name, otherwise use nameof(ViewModelTypeName) ```csharp using ReactiveUI.SourceGenerators; diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 457513a..de87779 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -9,6 +9,7 @@ + @@ -45,4 +46,4 @@ - + \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs new file mode 100644 index 0000000..d6a012b --- /dev/null +++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs @@ -0,0 +1,24 @@ +//HintName: ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.cs +// +#pragma warning disable +#nullable enable + +using global::ReactiveUI; +using global::Splat; + +namespace ReactiveUI +{ + /// + /// Source-generated registration extensions for ReactiveUI views. + /// + public static class ReactiveUISourceGeneratorsExtensions + { + public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver) + { + if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver)); + + } + } +} +#nullable restore +#pragma warning restore diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.SourceGenerators.IViewForAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.SourceGenerators.IViewForAttribute.g.verified.cs index 0ad03a7..11d67f6 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.SourceGenerators.IViewForAttribute.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.SourceGenerators.IViewForAttribute.g.verified.cs @@ -18,7 +18,14 @@ namespace ReactiveUI.SourceGenerators; /// /// Type of the view model. [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -internal sealed class IViewForAttribute : global::System.Attribute; +internal sealed class IViewForAttribute : global::System.Attribute +{ + /// + /// Gets the Splat registration type for Splat registration. + /// Registers IViewFor in the Splat service locator. + /// + public SplatRegistrationType RegistrationType { get; init; } = SplatRegistrationType.None; +} /// /// IViewForAttribute. @@ -27,8 +34,15 @@ internal sealed class IViewForAttribute : global::System.Attribute; /// /// Initializes a new instance of the class. /// -/// Type of the view model. +/// Type of the view model, ensure to use the full type name including namespace. [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -internal sealed class IViewForAttribute(string? viewModelType) : global::System.Attribute; +internal sealed class IViewForAttribute(string? viewModelType) : global::System.Attribute +{ + /// + /// Gets the Splat registration type for Splat registration. + /// Registers IViewFor in the Splat service locator. + /// + public SplatRegistrationType RegistrationType { get; init; } = SplatRegistrationType.None; +} #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePartialProperties#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePartialProperties#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs index 2f7525d..0e3ac88 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePartialProperties#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePartialProperties#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs @@ -33,5 +33,13 @@ internal enum InheritanceModifier Override, New, } + +internal enum SplatRegistrationType +{ + None, + LazySingleton, + Constant, + PerRequest, +} #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperiesWithAttributes#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperiesWithAttributes#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs index 2f7525d..0e3ac88 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperiesWithAttributes#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperiesWithAttributes#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs @@ -33,5 +33,13 @@ internal enum InheritanceModifier Override, New, } + +internal enum SplatRegistrationType +{ + None, + LazySingleton, + Constant, + PerRequest, +} #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperties#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperties#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs index 2f7525d..0e3ac88 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperties#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactiveProperties#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs @@ -33,5 +33,13 @@ internal enum InheritanceModifier Override, New, } + +internal enum SplatRegistrationType +{ + None, + LazySingleton, + Constant, + PerRequest, +} #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesCalledValue#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesCalledValue#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs index 2f7525d..0e3ac88 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesCalledValue#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesCalledValue#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs @@ -33,5 +33,13 @@ internal enum InheritanceModifier Override, New, } + +internal enum SplatRegistrationType +{ + None, + LazySingleton, + Constant, + PerRequest, +} #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAccess#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAccess#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs index 2f7525d..0e3ac88 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAccess#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAccess#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs @@ -33,5 +33,13 @@ internal enum InheritanceModifier Override, New, } + +internal enum SplatRegistrationType +{ + None, + LazySingleton, + Constant, + PerRequest, +} #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAttributesAccessAndInheritance#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAttributesAccessAndInheritance#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs index 2f7525d..0e3ac88 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAttributesAccessAndInheritance#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithAttributesAccessAndInheritance#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs @@ -33,5 +33,13 @@ internal enum InheritanceModifier Override, New, } + +internal enum SplatRegistrationType +{ + None, + LazySingleton, + Constant, + PerRequest, +} #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithIdenticalClass#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithIdenticalClass#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs index 2f7525d..0e3ac88 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithIdenticalClass#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithIdenticalClass#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs @@ -33,5 +33,13 @@ internal enum InheritanceModifier Override, New, } + +internal enum SplatRegistrationType +{ + None, + LazySingleton, + Constant, + PerRequest, +} #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithInit#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithInit#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs index 2f7525d..0e3ac88 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithInit#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithInit#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs @@ -33,5 +33,13 @@ internal enum InheritanceModifier Override, New, } + +internal enum SplatRegistrationType +{ + None, + LazySingleton, + Constant, + PerRequest, +} #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithNestedClass#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithNestedClass#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs index 2f7525d..0e3ac88 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithNestedClass#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/REACTIVE/ReactiveGeneratorTests.FromReactivePropertiesWithNestedClass#ReactiveUI.SourceGenerators.AccessModifier.g.verified.cs @@ -33,5 +33,13 @@ internal enum InheritanceModifier Override, New, } + +internal enum SplatRegistrationType +{ + None, + LazySingleton, + Constant, + PerRequest, +} #nullable restore #pragma warning restore \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerators.Execute/Program.cs b/src/ReactiveUI.SourceGenerators.Execute/Program.cs index df652e7..c216360 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/Program.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/Program.cs @@ -3,6 +3,9 @@ // 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; +using Splat; + namespace SGReactiveUI.SourceGenerators.Test; /// @@ -13,5 +16,9 @@ public static class Program /// /// Defines the entry point of the application. /// - public static void Main() => Application.Run(new TestViewWinForms()); + public static void Main() + { + AppLocator.CurrentMutable.RegisterViewsForViewModelsSourceGenerated(); + Application.Run(new TestViewWinForms()); + } } diff --git a/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj b/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj index 2bdda74..bd130b1 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj +++ b/src/ReactiveUI.SourceGenerators.Execute/ReactiveUI.SourceGenerators.Execute.csproj @@ -15,6 +15,7 @@ + diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs index 92e87dc..e73c3d0 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewWinForms.cs @@ -11,7 +11,7 @@ namespace SGReactiveUI.SourceGenerators.Test /// TestViewWinForms. /// /// - [IViewFor] + [IViewFor(RegistrationType = SplatRegistrationType.LazySingleton)] public partial class TestViewWinForms : Form { /// diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs index 1045826..1047ca1 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf.cs @@ -6,19 +6,24 @@ using System.Windows; using ReactiveUI; using ReactiveUI.SourceGenerators; +using Splat; namespace SGReactiveUI.SourceGenerators.Test; /// /// TestView. /// -[IViewFor] +[IViewFor(RegistrationType = SplatRegistrationType.PerRequest)] public partial class TestViewWpf : Window { /// /// Initializes a new instance of the class. /// - public TestViewWpf() => ViewModel = TestViewModel.Instance; + public TestViewWpf() + { + Locator.CurrentMutable.RegisterLazySingleton>(() => new TestViewWpf()); + ViewModel = TestViewModel.Instance; + } /// /// Gets or sets the test property. diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf2.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf2.cs index b7f4fc7..5015e97 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf2.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewWpf2.cs @@ -12,7 +12,7 @@ namespace SGReactiveUI.SourceGenerators.Test; /// TestViewWpf2. /// /// -[IViewFor("TestViewModel2")] +[IViewFor("SGReactiveUI.SourceGenerators.Test.TestViewModel2", RegistrationType = SplatRegistrationType.PerRequest)] public partial class TestViewWpf2 : Window { /// diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs b/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs index 1afc984..13d2207 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs @@ -79,7 +79,7 @@ namespace ReactiveUI.SourceGenerators; /// ReactiveObjectAttribute. /// /// -[global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.IViewForGenerator", "1.1.0.0")] +[global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveObjectGenerator", "1.1.0.0")] [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] internal sealed class ReactiveObjectAttribute : global::System.Attribute; #nullable restore @@ -368,7 +368,7 @@ internal sealed class IViewForAttribute : global::System.Attribute /// /// Initializes a new instance of the class. /// -/// Type of the view model. +/// Type of the view model, ensure to use the full type name including namespace. [global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.IViewForGenerator", "1.1.0.0")] [global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)] internal sealed class IViewForAttribute(string? viewModelType) : global::System.Attribute diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs index 2043645..f22899d 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs @@ -3,7 +3,9 @@ // 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 System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; @@ -91,10 +93,21 @@ public partial class IViewForGenerator token.ThrowIfCancellationRequested(); + // Get RegistrationType enum value from the attribute + attributeData.TryGetNamedArgument("RegistrationType", out int splatRegistrationType); + var registrationType = splatRegistrationType switch + { + 1 => "RegisterLazySingleton", + 2 => "RegisterConstant", + 3 => "Register", + _ => string.Empty, + }; + return new( targetInfo, viewModelTypeName!, - viewForBaseType); + viewForBaseType, + registrationType); } private static string GenerateSource(string containingTypeName, string containingNamespace, string containingClassVisibility, string containingType, IViewForInfo iviewForInfo) @@ -299,4 +312,71 @@ protected override void OnBindingContextChanged() return string.Empty; } + + private static string GenerateRegistrationExtensions(in ImmutableArray iviewForInfo) + { + // Collapse to unique registrations and skip entries with no registration + var registrations = iviewForInfo + .Where(static x => !string.IsNullOrWhiteSpace(x.SplatRegistrationType)) + .GroupBy(static x => (x.TargetInfo.TargetNamespaceWithNamespace, x.ViewModelTypeName, x.SplatRegistrationType)) + .Select(static g => g.First()) + .ToImmutableArray(); + + var sb = new StringBuilder(); + sb + .AppendLine("// ") + .AppendLine("#pragma warning disable") + .AppendLine("#nullable enable") + .AppendLine() + .AppendLine("using global::ReactiveUI;") + .AppendLine("using global::Splat;") + .AppendLine() + .AppendLine("namespace ReactiveUI") + .AppendLine("{") + .AppendLine(" /// ") + .AppendLine(" /// Source-generated registration extensions for ReactiveUI views.") + .AppendLine(" /// ") + .AppendLine(" internal static class ReactiveUISourceGeneratorsExtensions") + .AppendLine(" {") + .AppendLine($" [{AttributeDefinitions.GeneratedCode}(\"{GeneratorName}\", \"{GeneratorVersion}\")] ") + .AppendLine(" public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver)") + .AppendLine(" {") + .AppendLine(" if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));") + .AppendLine(); + + foreach (var item in registrations) + { + var vmType = item.ViewModelTypeName; + if (!string.IsNullOrEmpty(vmType) && !vmType.StartsWith("global::", System.StringComparison.Ordinal)) + { + vmType = "global::" + vmType; + } + + var serviceType = "global::ReactiveUI.IViewFor<" + vmType + ">"; + var viewType = item.TargetInfo.TargetNamespaceWithNamespace; // already fully-qualified + + // resolver.Register*/, View>(); + switch (item.SplatRegistrationType) + { + case "RegisterLazySingleton": + sb.AppendLine($" resolver.{item.SplatRegistrationType}<{serviceType}>(() => new {viewType}());"); + break; + case "Register": + sb.AppendLine($" resolver.{item.SplatRegistrationType}<{serviceType}, {viewType}>();"); + break; + case "RegisterConstant": + sb.AppendLine($" resolver.{item.SplatRegistrationType}<{serviceType}>(new {viewType}());"); + break; + } + } + + sb + .AppendLine(" }") + .AppendLine(" }") + .AppendLine("}") + .AppendLine("#nullable restore") + .AppendLine("#pragma warning restore"); + + return sb.ToString(); + } } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs index cb2970b..fef7d26 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.cs @@ -23,7 +23,7 @@ public sealed partial class IViewForGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { context.RegisterPostInitializationOutput(ctx => - ctx.AddSource($"{AttributeDefinitions.IViewForAttributeType}.g.cs", SourceText.From(AttributeDefinitions.IViewForAttribute, Encoding.UTF8))); + ctx.AddSource(AttributeDefinitions.IViewForAttributeType + ".g.cs", SourceText.From(AttributeDefinitions.IViewForAttribute, Encoding.UTF8))); // Gather info for all annotated IViewFor Classes var iViewForInfo = @@ -44,11 +44,20 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static info => info) .ToImmutableArray(); + const string fileName = "ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.cs"; + if (groupedPropertyInfo.Length == 0) { + // Even if there are no views, emit an empty extension to keep API stable. + var empty = GenerateRegistrationExtensions(ImmutableArray.Create()); + context.AddSource(fileName, SourceText.From(empty, Encoding.UTF8)); return; } + // Generate the IViewFor Splat Registration code for all classes in a single extension method here + var registrationSource = GenerateRegistrationExtensions(input); + context.AddSource(fileName, SourceText.From(registrationSource, Encoding.UTF8)); + foreach (var grouping in groupedPropertyInfo) { var items = grouping.ToImmutableArray(); @@ -59,7 +68,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } var source = GenerateSource(grouping.Key.TargetName, grouping.Key.TargetNamespace, grouping.Key.TargetVisibility, grouping.Key.TargetType, grouping.FirstOrDefault()); - context.AddSource($"{grouping.Key.FileHintName}.IViewFor.g.cs", source); + context.AddSource(grouping.Key.FileHintName + ".IViewFor.g.cs", source); } }); } diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForInfo.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForInfo.cs index cb6f51a..36a5485 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForInfo.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/Models/IViewForInfo.cs @@ -14,4 +14,5 @@ namespace ReactiveUI.SourceGenerators.Input.Models; internal sealed record IViewForInfo( TargetInfo TargetInfo, string ViewModelTypeName, - IViewForBaseType BaseType); + IViewForBaseType BaseType, + string SplatRegistrationType); From 1d8e6fdcea3e91d039599b715f7eb66a30f2bef9 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Tue, 2 Sep 2025 00:57:42 +0100 Subject: [PATCH 3/4] Reduce the use of string builder --- README.md | 2 + .../Program.cs | 2 +- .../IViewFor/IViewForGenerator.Execute.cs | 55 +++++++++---------- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 8f60e7e..a4c001e 100644 --- a/README.md +++ b/README.md @@ -567,6 +567,8 @@ public partial class MyReactiveControl : UserControl this will generate the following code to enable you register the marked Views as `IViewFor` with Splat: ```csharp +using ReactiveUI.SourceGenerators; + Splat.Locator.CurrentMutable.RegisterViewsForViewModelsSourceGenerated(); ``` diff --git a/src/ReactiveUI.SourceGenerators.Execute/Program.cs b/src/ReactiveUI.SourceGenerators.Execute/Program.cs index c216360..6db2c3a 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/Program.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/Program.cs @@ -3,7 +3,7 @@ // 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; +using ReactiveUI.SourceGenerators; using Splat; namespace SGReactiveUI.SourceGenerators.Test; diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs index f22899d..5c061fc 100644 --- a/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators.Roslyn/IViewFor/IViewForGenerator.Execute.cs @@ -323,27 +323,7 @@ private static string GenerateRegistrationExtensions(in ImmutableArray") - .AppendLine("#pragma warning disable") - .AppendLine("#nullable enable") - .AppendLine() - .AppendLine("using global::ReactiveUI;") - .AppendLine("using global::Splat;") - .AppendLine() - .AppendLine("namespace ReactiveUI") - .AppendLine("{") - .AppendLine(" /// ") - .AppendLine(" /// Source-generated registration extensions for ReactiveUI views.") - .AppendLine(" /// ") - .AppendLine(" internal static class ReactiveUISourceGeneratorsExtensions") - .AppendLine(" {") - .AppendLine($" [{AttributeDefinitions.GeneratedCode}(\"{GeneratorName}\", \"{GeneratorVersion}\")] ") - .AppendLine(" public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver)") - .AppendLine(" {") - .AppendLine(" if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));") - .AppendLine(); - + sb.AppendLine("if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver));"); foreach (var item in registrations) { var vmType = item.ViewModelTypeName; @@ -370,13 +350,32 @@ private static string GenerateRegistrationExtensions(in ImmutableArray +#pragma warning disable +#nullable enable + +using global::ReactiveUI; +using global::Splat; - return sb.ToString(); +namespace ReactiveUI.SourceGenerators +{ + /// + /// Source-generated registration extensions for ReactiveUI views. + /// + internal static class ReactiveUISourceGeneratorsExtensions + { + [global::System.CodeDom.Compiler.GeneratedCode("{{GeneratorName}}", "{{GeneratorVersion}}")] + public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver) + { + {{registrationsBody}} + } + } +} +#nullable restore +#pragma warning restore +"""; } } From d3602a5b4c46f80103dce61e0799e7a5855fcfcd Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Tue, 2 Sep 2025 01:41:10 +0100 Subject: [PATCH 4/4] Update IViewForGeneratorTests.FromIViewFor#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs --- ...veUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs index d6a012b..ee04db7 100644 --- a/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs +++ b/src/ReactiveUI.SourceGenerator.Tests/IVIEWFOR/IViewForGeneratorTests.FromIViewFor#ReactiveUI.ReactiveUISourceGeneratorsExtensions.g.verified.cs @@ -6,19 +6,18 @@ using global::ReactiveUI; using global::Splat; -namespace ReactiveUI +namespace ReactiveUI.SourceGenerators { /// /// Source-generated registration extensions for ReactiveUI views. /// - public static class ReactiveUISourceGeneratorsExtensions + internal static class ReactiveUISourceGeneratorsExtensions { public static void RegisterViewsForViewModelsSourceGenerated(this global::Splat.IMutableDependencyResolver resolver) { if (resolver is null) throw new global::System.ArgumentNullException(nameof(resolver)); - } } } #nullable restore -#pragma warning restore +#pragma warning restore \ No newline at end of file