From 1d6f43315a388fce2f49140cff6efac9a14e3932 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 31 Aug 2024 15:29:07 +0100 Subject: [PATCH 1/2] Feature Add RoutedControlHost for WinForms --- .../TestWinFormsRCHost.Designer.cs | 23 ++ .../TestWinFormsRCHost.cs | 14 + .../TestWinFormsRCHost.resx | 120 ++++++++ .../TestWinFormsVMCHost.cs | 21 +- .../Helpers/AttributeDefinitions.cs | 29 ++ .../Models/RoutedControlHostInfo.cs | 19 ++ .../RoutedControlHostGenerator.Execute.cs | 257 ++++++++++++++++++ .../RoutedControlHostGenerator.cs | 149 ++++++++++ 8 files changed, 621 insertions(+), 11 deletions(-) create mode 100644 src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.Designer.cs create mode 100644 src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.cs create mode 100644 src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.resx create mode 100644 src/ReactiveUI.SourceGenerators/RoutedControlHost/Models/RoutedControlHostInfo.cs create mode 100644 src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs create mode 100644 src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.cs diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.Designer.cs b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.Designer.cs new file mode 100644 index 0000000..89b5aae --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.Designer.cs @@ -0,0 +1,23 @@ +namespace SGReactiveUI.SourceGenerators.Test; + +partial class TestWinFormsRCHost +{ + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + components = new System.ComponentModel.Container(); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + } + + #endregion +} diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.cs b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.cs new file mode 100644 index 0000000..f9b0377 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2024 .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. + +using ReactiveUI.SourceGenerators.WinForms; + +namespace SGReactiveUI.SourceGenerators.Test; + +/// +/// TestWinFormsRCHost. +/// +[RoutedControlHost(nameof(UserControl))] +public partial class TestWinFormsRCHost; diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.resx b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsRCHost.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.cs b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.cs index 21025e9..d34e993 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestWinFormsVMCHost.cs @@ -5,14 +5,13 @@ using ReactiveUI.SourceGenerators.WinForms; -namespace SGReactiveUI.SourceGenerators.Test -{ - /// - /// TestWinFormsVMCHost. - /// - /// - /// - /// - [ViewModelControlHost(nameof(UserControl))] - public partial class TestWinFormsVMCHost; -} +namespace SGReactiveUI.SourceGenerators.Test; + +/// +/// TestWinFormsVMCHost. +/// +/// +/// +/// +[ViewModelControlHost(nameof(UserControl))] +public partial class TestWinFormsVMCHost; diff --git a/src/ReactiveUI.SourceGenerators/Helpers/AttributeDefinitions.cs b/src/ReactiveUI.SourceGenerators/Helpers/AttributeDefinitions.cs index 14a74a0..f7eadfc 100644 --- a/src/ReactiveUI.SourceGenerators/Helpers/AttributeDefinitions.cs +++ b/src/ReactiveUI.SourceGenerators/Helpers/AttributeDefinitions.cs @@ -184,5 +184,34 @@ namespace ReactiveUI.SourceGenerators.WinForms; public sealed class ViewModelControlHostAttribute(string? baseType) : Attribute; #nullable restore #pragma warning restore +"""; + + public const string RoutedControlHostAttributeType = "ReactiveUI.SourceGenerators.WinForms.RoutedControlHostAttribute"; + public const string RoutedControlHostAttribute = """ +// Copyright (c) 2024 .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. + +using System; + +// +#pragma warning disable +#nullable enable +namespace ReactiveUI.SourceGenerators.WinForms; + +/// +/// RoutedControlHostAttribute. +/// +/// +/// +/// Initializes a new instance of the class. +/// +/// Type of the view model. +[global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.RoutedControlHostGenerator", "1.1.0.0")] +[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] +public sealed class RoutedControlHostAttribute(string? baseType) : Attribute; +#nullable restore +#pragma warning restore """; } diff --git a/src/ReactiveUI.SourceGenerators/RoutedControlHost/Models/RoutedControlHostInfo.cs b/src/ReactiveUI.SourceGenerators/RoutedControlHost/Models/RoutedControlHostInfo.cs new file mode 100644 index 0000000..6041f95 --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/RoutedControlHost/Models/RoutedControlHostInfo.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2024 .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. + +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.Helpers; + +namespace ReactiveUI.SourceGenerators.Input.Models; + +/// +/// A model with gathered info on a given command method. +/// +internal sealed record RoutedControlHostInfo( + string ClassNamespace, + string ClassName, + string ViewModelTypeName, + TypeDeclarationSyntax DeclarationSyntax, + EquatableArray ForwardedAttributes); diff --git a/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs new file mode 100644 index 0000000..7710ceb --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs @@ -0,0 +1,257 @@ +// Copyright (c) 2024 .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. + +using System; +using System.CodeDom.Compiler; +using System.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using ReactiveUI.SourceGenerators.Input.Models; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace ReactiveUI.SourceGenerators.WinForms; + +/// +/// IViewForGenerator. +/// +/// +public partial class RoutedControlHostGenerator +{ + internal static class Execute + { + internal static CompilationUnitSyntax GetRoutedControlHost(RoutedControlHostInfo vmcInfo) + { + UsingDirectiveSyntax[] usings = + [ + UsingDirective(ParseName("ReactiveUI")) + .WithUsingKeyword( + Token( + TriviaList( + [ + Comment($"// Copyright (c) {DateTime.Now.Year} .NET Foundation and Contributors. All rights reserved."), + Comment("// Licensed to the .NET Foundation under one or more agreements."), + Comment("// The .NET Foundation licenses this file to you under the MIT license."), + Comment("// See the LICENSE file in the project root for full license information.") + ]), + SyntaxKind.UsingKeyword, + TriviaList())), + UsingDirective(ParseName("System.ComponentModel")), + UsingDirective(ParseName("System.Reactive.Disposables")), + UsingDirective(ParseName("System.Reactive.Linq")), + UsingDirective(ParseName("System.Windows.Forms")), + ]; + + var code = CompilationUnit() + .WithUsings(List(usings)) + .WithTrailingTrivia(TriviaList(CarriageReturnLineFeed)) + .WithMembers( + SingletonList( + NamespaceDeclaration(IdentifierName(vmcInfo.ClassNamespace)) + .WithLeadingTrivia(TriviaList( + Comment("// "), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)), + Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true)))) + .WithMembers( + SingletonList( + ClassDeclaration(vmcInfo.ClassName) + .WithAttributeLists( + SingletonList( + AttributeList( + SingletonSeparatedList( + Attribute(IdentifierName("DefaultProperty")) + .WithArgumentList( + AttributeArgumentList( + SingletonSeparatedList( + AttributeArgument( + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal("ViewModel")))))))))) + .WithModifiers( + TokenList( + [ + Token(SyntaxKind.PublicKeyword), + Token(SyntaxKind.PartialKeyword)])) + .WithBaseList( + BaseList( + SeparatedList( + new SyntaxNodeOrToken[] + { + SimpleBaseType(IdentifierName(vmcInfo.ViewModelTypeName)), + Token(SyntaxKind.CommaToken), + SimpleBaseType(IdentifierName("IReactiveObject")) + }))))))) + .NormalizeWhitespace().ToFullString(); + + // Remove the last 4 characters to remove the closing brackets + var baseCode = code.Remove(code.Length - 4); + + // Prepare all necessary type names with type arguments + using var stringStream = new StringWriter(); + using var writer = new IndentedTextWriter(stringStream, "\t"); + writer.WriteLine(baseCode); + writer.Indent++; + writer.Indent++; + + var body = """ + private readonly CompositeDisposable _disposables = []; + private RoutingState? _router; + private Control? _defaultContent; + private IObservable? _viewContractObservable; + + /// + /// Initializes a new instance of the class. + /// + public ####REPLACEME####() + { + InitializeComponent(); + + _disposables.Add(this.WhenAny(x => x.DefaultContent, x => x.Value).Subscribe(x => + { + if (x is not null && Controls.Count == 0) + { + Controls.Add(InitView(x)); + components?.Add(DefaultContent); + } + })); + + ViewContractObservable = Observable.Default; + + var vmAndContract = + this.WhenAnyObservable(x => x.Router!.CurrentViewModel!) + .CombineLatest( + this.WhenAnyObservable(x => x.ViewContractObservable!), + (vm, contract) => new { ViewModel = vm, Contract = contract }); + + Control? viewLastAdded = null; + _disposables.Add(vmAndContract.Subscribe( + x => + { + // clear all hosted controls (view or default content) + SuspendLayout(); + Controls.Clear(); + + viewLastAdded?.Dispose(); + + if (x.ViewModel is null) + { + if (DefaultContent is not null) + { + InitView(DefaultContent); + Controls.Add(DefaultContent); + } + + ResumeLayout(); + return; + } + + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(x.ViewModel, x.Contract); + if (view is not null) + { + view.ViewModel = x.ViewModel; + + viewLastAdded = InitView((Control)view); + } + + if (viewLastAdded is not null) + { + Controls.Add(viewLastAdded); + } + + ResumeLayout(); + }, + RxApp.DefaultExceptionHandler!.OnNext)); + } + + /// + public event PropertyChangingEventHandler? PropertyChanging; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Gets or sets the default content. + /// + /// + /// The default content. + /// + [Category("ReactiveUI")] + [Description("The default control when no viewmodel is specified")] + public Control? DefaultContent + { + get => _defaultContent; + set => this.RaiseAndSetIfChanged(ref _defaultContent, value); + } + + /// + /// Gets or sets the of the view model stack. + /// + [Category("ReactiveUI")] + [Description("The router.")] + public RoutingState? Router + { + get => _router; + set => this.RaiseAndSetIfChanged(ref _router, value); + } + + /// + /// Gets or sets the view contract observable. + /// + [Browsable(false)] + public IObservable? ViewContractObservable + { + get => _viewContractObservable; + set => this.RaiseAndSetIfChanged(ref _viewContractObservable, value); + } + + /// + /// Gets or sets the view locator. + /// + [Browsable(false)] + public IViewLocator? ViewLocator { get; set; } + + /// + void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args); + + /// + void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args); + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && components is not null) + { + components.Dispose(); + _disposables.Dispose(); + } + + base.Dispose(disposing); + } + + private static Control InitView(Control view) + { + view.Dock = DockStyle.Fill; + return view; + } + """.Replace("####REPLACEME####", vmcInfo.ClassName); + writer.WriteLine(body); + writer.Indent--; + writer.WriteLine(Token(SyntaxKind.CloseBraceToken)); + writer.Indent--; + writer.WriteLine(Token(SyntaxKind.CloseBraceToken)); + writer.WriteLine(TriviaList( + Trivia(NullableDirectiveTrivia(Token(SyntaxKind.RestoreKeyword), true)), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.RestoreKeyword), true))) + .NormalizeWhitespace()); + + var output = stringStream.ToString(); + return ParseCompilationUnit(output).NormalizeWhitespace(); + } + } +} diff --git a/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.cs b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.cs new file mode 100644 index 0000000..b3d8a9f --- /dev/null +++ b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.cs @@ -0,0 +1,149 @@ +// Copyright (c) 2024 .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. + +using System.CodeDom.Compiler; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using ReactiveUI.SourceGenerators.Extensions; +using ReactiveUI.SourceGenerators.Helpers; +using ReactiveUI.SourceGenerators.Input.Models; +using ReactiveUI.SourceGenerators.Models; + +namespace ReactiveUI.SourceGenerators.WinForms; + +/// +/// A source generator for generating reative properties. +/// +[Generator(LanguageNames.CSharp)] +public sealed partial class RoutedControlHostGenerator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(ctx => + ctx.AddSource($"{AttributeDefinitions.RoutedControlHostAttributeType}.g.cs", SourceText.From(AttributeDefinitions.RoutedControlHostAttribute, Encoding.UTF8))); + + // Gather info for all annotated IViewFor Classes + IncrementalValuesProvider<(HierarchyInfo Hierarchy, Result Info)> rchInfoWithErrors = + context.SyntaxProvider + .ForAttributeWithMetadataName( + AttributeDefinitions.RoutedControlHostAttributeType, + static (node, _) => node is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, + static (context, token) => + { + token.ThrowIfCancellationRequested(); + using var hierarchys = ImmutableArrayBuilder.Rent(); + RoutedControlHostInfo rchInfo = default!; + HierarchyInfo hierarchy = default!; + + if (context.TargetNode is ClassDeclarationSyntax declaredClass && declaredClass.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + token.ThrowIfCancellationRequested(); + var compilation = context.SemanticModel.Compilation; + var semanticModel = compilation.GetSemanticModel(context.SemanticModel.SyntaxTree); + var symbol = ModelExtensions.GetDeclaredSymbol(semanticModel, declaredClass, token)!; + if (symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.RoutedControlHostAttributeType, out var attributeData)) + { + token.ThrowIfCancellationRequested(); + var classSymbol = symbol as INamedTypeSymbol; + var classNamespace = classSymbol?.ContainingNamespace.ToString(); + var className = declaredClass.Identifier.ValueText; + var constructorArgument = attributeData.GetConstructorArguments().First(); + if (constructorArgument is string viewModelTypeName) + { + token.ThrowIfCancellationRequested(); + GatherForwardedAttributes(attributeData, semanticModel, declaredClass, token, out var classAttributesInfo); + token.ThrowIfCancellationRequested(); + + rchInfo = new RoutedControlHostInfo( + classNamespace!, + className, + viewModelTypeName!, + declaredClass, + classAttributesInfo); + + hierarchy = HierarchyInfo.From(classSymbol!); + } + } + } + + token.ThrowIfCancellationRequested(); + ImmutableArray diagnostics = default; + return (Hierarchy: hierarchy, new Result(rchInfo, diagnostics)); + }) + .Where(static item => item.Hierarchy is not null)!; + + ////// Output the diagnostics + ////context.ReportDiagnostics(iViewForInfoWithErrors.Select(static (item, _) => item.Info.Errors)); + + // Get the filtered sequence to enable caching + var rchInfo = + rchInfoWithErrors + .Where(static item => item.Info.Value is not null)!; + + // Generate the requested properties and methods for IViewFor + context.RegisterSourceOutput(rchInfo, static (context, item) => + context.AddSource($"{item.Hierarchy.FilenameHint}.RoutedControlHost.g.cs", Execute.GetRoutedControlHost(item.Info.Value))); + } + + private static void GatherForwardedAttributes( + AttributeData attributeData, + SemanticModel semanticModel, + ClassDeclarationSyntax classDeclaration, + CancellationToken token, + out ImmutableArray classAttributesInfo) + { + using var classAttributesInfoBuilder = ImmutableArrayBuilder.Rent(); + + static void GatherForwardedAttributes( + AttributeData attributeData, + SemanticModel semanticModel, + ClassDeclarationSyntax classDeclaration, + CancellationToken token, + ImmutableArrayBuilder classAttributesInfo) + { + // Gather explicit forwarded attributes info + foreach (var attributeList in classDeclaration.AttributeLists) + { + foreach (var attribute in attributeList.Attributes) + { + if (!semanticModel.GetSymbolInfo(attribute, token).TryGetAttributeTypeSymbol(out var attributeTypeSymbol)) + { + continue; + } + + var attributeArguments = attribute.ArgumentList?.Arguments ?? Enumerable.Empty(); + + // Try to extract the forwarded attribute + if (!AttributeInfo.TryCreate(attributeTypeSymbol, semanticModel, attributeArguments, token, out var attributeInfo)) + { + continue; + } + + var ignoreAttribute = attributeData.AttributeClass?.GetFullyQualifiedMetadataName(); + if (attributeInfo.TypeName.Contains(ignoreAttribute)) + { + continue; + } + + // Add the new attribute info to the right builder + classAttributesInfo.Add(attributeInfo); + } + } + } + + // If the method is not a partial definition/implementation, just gather attributes from the method with no modifications + GatherForwardedAttributes(attributeData, semanticModel, classDeclaration, token, classAttributesInfoBuilder); + + classAttributesInfo = classAttributesInfoBuilder.ToImmutable(); + } +} From 53d0249cd4988527b48722c0e44b1dd41090e9e3 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 31 Aug 2024 15:43:11 +0100 Subject: [PATCH 2/2] Use Default functionality --- .../RoutedControlHost/RoutedControlHostGenerator.Execute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs index 7710ceb..c1ff519 100644 --- a/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/RoutedControlHost/RoutedControlHostGenerator.Execute.cs @@ -117,7 +117,7 @@ internal static CompilationUnitSyntax GetRoutedControlHost(RoutedControlHostInfo } })); - ViewContractObservable = Observable.Default; + ViewContractObservable = Observable.Return(default(string)!); var vmAndContract = this.WhenAnyObservable(x => x.Router!.CurrentViewModel!)