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 950d6c32..1afc9849 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 f9986d60..8f60e7ee 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 457513ae..de877790 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 00000000..d6a012ba --- /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 0ad03a75..11d67f64 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 2f7525dc..0e3ac886 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 2f7525dc..0e3ac886 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 2f7525dc..0e3ac886 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 2f7525dc..0e3ac886 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 2f7525dc..0e3ac886 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 2f7525dc..0e3ac886 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 2f7525dc..0e3ac886 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 2f7525dc..0e3ac886 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 2f7525dc..0e3ac886 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 df652e78..c216360b 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 2bdda74a..bd130b1c 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 92e87dc7..e73c3d03 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 10458262..1047ca11 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 b7f4fc74..5015e976 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 1afc9849..13d2207b 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 2043645d..f22899d9 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 cb2970be..fef7d267 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 cb6f51a2..36a54850 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 8f60e7ee..a4c001e5 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 c216360b..6db2c3a6 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 f22899d9..5c061fcd 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 d6a012ba..ee04db7b 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