diff --git a/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelGenerator.cs b/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelGenerator.cs index ca92cf7839..7cdce7de52 100644 --- a/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelGenerator.cs +++ b/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelGenerator.cs @@ -2,37 +2,60 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Uno.Extensions.Generators; +using Uno.Extensions.Reactive.Config; using Uno.RoslynHelpers; using static Microsoft.CodeAnalysis.Accessibility; namespace Uno.Extensions.Reactive.Generator; -internal class BindableViewModelGenerator +internal class BindableViewModelGenerator : ICodeGenTool { - private const string _version = "1"; + /// + public string Version => "1"; private readonly BindableGenerationContext _ctx; private readonly BindableGenerator _bindables; private readonly BindableViewModelMappingGenerator _viewModelsMapping; + private readonly IAssemblySymbol _assembly; public BindableViewModelGenerator(BindableGenerationContext ctx) { _ctx = ctx; _bindables = new BindableGenerator(ctx); _viewModelsMapping = new BindableViewModelMappingGenerator(ctx); + _assembly = ctx.Context.Compilation.Assembly; } private bool IsSupported(INamedTypeSymbol? type) - => type is not null - && (_ctx.IsGenerationEnabled(type) - ?? type.Name.EndsWith("ViewModel", StringComparison.Ordinal) && type.IsPartial()); + { + if (type is null) + { + return false; + } + + if (_ctx.IsGenerationEnabled(type) is {} isEnabled) + { + // If the attribute is set, we don't check for the `partial`: the build as to fail if not + return isEnabled; + } - public IEnumerable<(string fileName, string code)> Generate(IAssemblySymbol assembly) + if (type.IsPartial() + && (type.ContainingAssembly.FindAttribute() ?? new ()) is { IsEnabled: true } @implicit // Note: the type might be from another assembly than current + && @implicit.Patterns.Any(pattern => Regex.IsMatch(type.ToString(), pattern))) + { + return true; + } + + return false; + } + + public IEnumerable<(string fileName, string code)> Generate() { - var viewModels = from module in assembly.Modules + var viewModels = from module in _assembly.Modules from type in module.GetNamespaceTypes() where IsSupported(type) select type; @@ -50,23 +73,35 @@ where IsSupported(type) yield return _viewModelsMapping.Generate(); } - private string Generate(INamedTypeSymbol vm) + private static string GetViewModelName(INamedTypeSymbol type) + { + // Note: the type might be from another assembly than current + var isLegacyMode = type.ContainingAssembly.FindAttribute() is { Patterns.Length: 1 } config + && config.Patterns[0] is ImplicitBindablesAttribute.LegacyPattern; + + return !isLegacyMode && type.Name.IndexOf("ViewModel", StringComparison.OrdinalIgnoreCase) < 0 + ? $"{type.Name}ViewModel" + : $"Bindable{type.Name}"; + } + + private string Generate(INamedTypeSymbol model) { - var hasBaseType = IsSupported(vm.BaseType); + var vmName = GetViewModelName(model); + var hasBaseType = IsSupported(model.BaseType); var baseType = hasBaseType - ? $"{vm.BaseType}.Bindable{vm.BaseType!.Name}" + ? $"{model.BaseType}.{GetViewModelName(model.BaseType!)}" : $"{NS.Bindings}.BindableViewModelBase"; List inputs; IEnumerable inputsErrors; - if (hasBaseType || vm.IsAbstract) + if (hasBaseType || model.IsAbstract) { inputs = new List(0); inputsErrors = Enumerable.Empty(); } else { - inputs = GetInputs(vm).ToList(); + inputs = GetInputs(model).ToList(); inputsErrors = inputs .GroupBy(input => input.Parameter.Name) .Where(group => group.Distinct().Count() > 1) @@ -82,7 +117,7 @@ private string Generate(INamedTypeSymbol vm) inputs = inputs.Distinct().ToList(); } - var mappedMembers = GetMembers(vm).ToList(); + var mappedMembers = GetMembers(model).ToList(); var mappedMembersConflictingWithInputs = mappedMembers .Where(member => inputs.Any(input => input.Property?.Name.Equals(member.Name, StringComparison.Ordinal) ?? false)) .ToList(); @@ -92,16 +127,16 @@ private string Generate(INamedTypeSymbol vm) mappedMembers = mappedMembers.Except(mappedMembersConflictingWithInputs).ToList(); var bindableVmCode = $@" - [global::System.CodeDom.Compiler.GeneratedCode(""{nameof(BindableViewModelGenerator)}"", ""{_version}"")] - public partial class Bindable{vm.Name} : {baseType} + {this.GetCodeGenAttribute()} + public partial class {vmName} : {baseType} {{ {inputsErrors.Align(4)} {inputs.Select(input => input.GetBackingField()).Align(4)} {mappedMembers.Select(member => member.GetBackingField()).Align(4)} - {vm + {model .Constructors - .Where(ctor => !ctor.IsCloneCtor(vm) + .Where(ctor => !ctor.IsCloneCtor(model) // we do not support inheritance of ctor, inheritance always goes through the same BindableVM(vm) ctor. && ctor.DeclaredAccessibility is not Protected and not ProtectedAndFriend and not ProtectedAndInternal) .Where(_ctx.IsGenerationNotDisable) @@ -115,8 +150,8 @@ public partial class Bindable{vm.Name} : {baseType} .ToList(); return $@" - {GetCtorAccessibility(ctor)} Bindable{vm.Name}({parameters.Select(p => p.GetCtorParameter().code).JoinBy(", ")}) - : base(new {vm}({parameters.Select(p => p.GetVMCtorParameter()).JoinBy(", ")})) + {GetCtorAccessibility(ctor)} {vmName}({parameters.Select(p => p.GetCtorParameter().code).JoinBy(", ")}) + : base(new {model}({parameters.Select(p => p.GetVMCtorParameter()).JoinBy(", ")})) {{ var {N.Ctor.Model} = {N.Model}; var {N.Ctor.Ctx} = {NS.Core}.SourceContext.GetOrCreate({N.Ctor.Model}); @@ -142,11 +177,11 @@ public partial class Bindable{vm.Name} : {baseType} .JoinBy(", "); return $@" - {GetCtorAccessibility(ctor)} Bindable{vm.Name}({bindableVmParameters}) + {GetCtorAccessibility(ctor)} {vmName}({bindableVmParameters}) {{ {inputs.Select(input => input.GetCtorInit(parameters.Contains(input))).Align(9)} - var {N.Ctor.Model} = new {vm}({vmParameters}); + var {N.Ctor.Model} = new {model}({vmParameters}); var {N.Ctor.Ctx} = {NS.Core}.SourceContext.GetOrCreate({N.Ctor.Model}); {NS.Core}.SourceContext.Set(this, {N.Ctor.Ctx}); base.RegisterDisposable({N.Ctor.Model}); @@ -159,7 +194,7 @@ public partial class Bindable{vm.Name} : {baseType} }) .Align(4)} - protected Bindable{vm.Name}({vm} {N.Ctor.Model}){(hasBaseType ? $" : base({N.Ctor.Model})" : "")} + protected {vmName}({model} {N.Ctor.Model}){(hasBaseType ? $" : base({N.Ctor.Model})" : "")} {{ var {N.Ctor.Ctx} = {NS.Core}.SourceContext.GetOrCreate({N.Ctor.Model}); {(hasBaseType ? "" : $"{NS.Core}.SourceContext.Set(this, {N.Ctor.Ctx});")} @@ -167,7 +202,7 @@ public partial class Bindable{vm.Name} : {baseType} {mappedMembers.Select(member => member.GetInitialization()).Align(5)} }} - public {(hasBaseType ? $"new {vm} {N.Model} => ({vm}) base.{N.Model};" : $"{vm} {N.Model} {{ get; }}")} + public {(hasBaseType ? $"new {model} {N.Model} => ({model}) base.{N.Model};" : $"{model} {N.Model} {{ get; }}")} {inputs.Select(input => input.Property?.ToString()).Align(4)} @@ -175,45 +210,24 @@ public partial class Bindable{vm.Name} : {baseType} {mappedMembers.Select(member => member.GetDeclaration()).Align(4)} }}"; - // We make the bindbale VM a nested class of the VM itself - var vmCode = $@"partial {(vm.IsRecord ? "record" : "class")} {vm.Name} : global::System.IAsyncDisposable, {NS.Core}.ISourceContextAware - {{ + var fileCode = this.AsPartialOf( + model, + $"global::System.IAsyncDisposable, {NS.Core}.ISourceContextAware", + $@" {bindableVmCode.Align(4)} /// - [global::System.CodeDom.Compiler.GeneratedCode(""{nameof(BindableViewModelGenerator)}"", ""{_version}"")] + {this.GetCodeGenAttribute()} public global::System.Threading.Tasks.ValueTask DisposeAsync() => {NS.Core}.SourceContext.Find(this)?.DisposeAsync() ?? default; - }}"; - - // Inject usings and declare the full namespace, including class nesting - var fileCode = $@"//---------------------- - // - // Generated by the {nameof(BindableViewModelGenerator)} v{_version}. DO NOT EDIT! - // Manual changes to this file will be overwritten if the code is regenerated. - // - //---------------------- - #nullable enable - #pragma warning disable - - using System; - using System.Linq; - using System.Threading.Tasks; - - namespace {vm.ContainingNamespace} - {{ - {vm.GetContainingTypes().Select(type => $"partial {(type.IsRecord ? "record" : "class")} {type.Name}\r\n{{").Align(4)} - {vmCode.Align(4).Indent(vm.GetContainingTypes().Count())} - {vm.GetContainingTypes().Select(_ => "}").Align(4)} - }} - "; + "); // If type is at least internally accessible, add it to a mapping from the VM type to it's bindable counterpart to ease usage in navigation engine. // (Private types are almost only a test case which is not supported by nav anyway) - if (vm.DeclaredAccessibility is not Accessibility.Private - && vm.GetContainingTypes().All(type => type.DeclaredAccessibility is not Accessibility.Private)) + if (model.DeclaredAccessibility is not Accessibility.Private + && model.GetContainingTypes().All(type => type.DeclaredAccessibility is not Accessibility.Private)) { - _viewModelsMapping.Register(vm, $"{vm.ContainingNamespace}.{vm.GetContainingTypes().Select(type => type.Name + '.').JoinBy("")}{vm.Name}.Bindable{vm.Name}"); + _viewModelsMapping.Register(model, $"{model.ContainingNamespace}.{model.GetContainingTypes().Select(type => type.Name + '.').JoinBy("")}{model.Name}.{vmName}"); } return fileCode.Align(0); diff --git a/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelMappingGenerator.cs b/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelMappingGenerator.cs index e6f222e7b2..c71c55905e 100644 --- a/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelMappingGenerator.cs +++ b/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelMappingGenerator.cs @@ -6,9 +6,10 @@ namespace Uno.Extensions.Reactive.Generator; -internal class BindableViewModelMappingGenerator +internal class BindableViewModelMappingGenerator : ICodeGenTool { - private const string _version = "1"; + /// + public string Version => "1"; #pragma warning disable RS1024 // Compare symbols correctly => False positive private readonly Dictionary _bindableVMs = new(SymbolEqualityComparer.Default); @@ -25,18 +26,11 @@ public void Register(INamedTypeSymbol viewModelType, string bindableViewModelTyp public (string file, string code) Generate() { - var code = $@"//---------------------- - // - // Generated by the {nameof(BindableViewModelMappingGenerator)} v{_version}. DO NOT EDIT! - // Manual changes to this file will be overwritten if the code is regenerated. - // - //---------------------- - #nullable enable - #pragma warning disable + var code = $@"{this.GetFileHeader(3)} namespace {_ctx.Context.Compilation.Assembly.Name} {{ - [global::System.CodeDom.Compiler.GeneratedCode(""{nameof(BindableViewModelMappingGenerator)}"", ""{_version}"")] + {this.GetCodeGenAttribute()} public static partial class ReactiveViewModelMappings {{ /// diff --git a/src/Uno.Extensions.Reactive.Generator/FeedsGenerator.cs b/src/Uno.Extensions.Reactive.Generator/FeedsGenerator.cs index 2adc5100e4..7d05339297 100644 --- a/src/Uno.Extensions.Reactive.Generator/FeedsGenerator.cs +++ b/src/Uno.Extensions.Reactive.Generator/FeedsGenerator.cs @@ -29,7 +29,7 @@ public void Execute(GeneratorExecutionContext context) if (GenerationContext.TryGet(context, out var error) is {} bindableContext) { - foreach (var generated in new BindableViewModelGenerator(bindableContext).Generate(context.Compilation.Assembly)) + foreach (var generated in new BindableViewModelGenerator(bindableContext).Generate()) { context.AddSource(PathHelper.SanitizeFileName(generated.fileName) + ".g.cs", generated.code); } diff --git a/src/Uno.Extensions.Reactive.Tests/Generator/Given_ViewModel_Then_GenerateBindable.cs b/src/Uno.Extensions.Reactive.Tests/Generator/Given_ViewModel_Then_GenerateBindable.cs index 9e56131fa4..e768fad9d6 100644 --- a/src/Uno.Extensions.Reactive.Tests/Generator/Given_ViewModel_Then_GenerateBindable.cs +++ b/src/Uno.Extensions.Reactive.Tests/Generator/Given_ViewModel_Then_GenerateBindable.cs @@ -96,7 +96,7 @@ public void NestedInheritingViewModel() => Assert.IsNotNull(GetBindable(typeof(NestedSubViewModel))); private Type? GetBindable(Type vmType) - => vmType.GetNestedType($"Bindable{vmType.Name}"); + => vmType.GetNestedType(vmType.Name.Contains("ViewModel") ? $"Bindable{vmType.Name}" : $"{vmType.Name}ViewModel"); private Type? GetBindable(string vmType) { diff --git a/src/Uno.Extensions.Reactive.UI/Config/ImplicitBindablesAttribute.cs b/src/Uno.Extensions.Reactive.UI/Config/ImplicitBindablesAttribute.cs new file mode 100644 index 0000000000..44a1dad5d1 --- /dev/null +++ b/src/Uno.Extensions.Reactive.UI/Config/ImplicitBindablesAttribute.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; + +namespace Uno.Extensions.Reactive.Config; + +/// +/// Indicates if types that matches defined suffixes should be automatically exposed as bindable friendly view models. +/// +/// If disabled, you can still generates bindable friendly view model by flagging a class with the . +[AttributeUsage(AttributeTargets.Assembly)] +public class ImplicitBindablesAttribute : Attribute +{ + /// + /// Gets the legacy pattern that was used in versions prior to 2.3. + /// + public const string LegacyPattern = "ViewModel$"; + + /// + /// Gets or sets a bool which indicates if the generation of view models based on class names is enabled of not. + /// + public bool IsEnabled { get; init; } = true; + + /// + /// The patterns that the class FullName has to match to implicitly trigger view model generation. + /// + public string[] Patterns { get; } = { "Model$" }; + + /// + /// Create a new instance using default values. + /// + public ImplicitBindablesAttribute() + { + } + + /// + /// Creates a new instance specifying the . + /// + /// The patterns that the class FullName has to match to implicitly trigger view model generation. + public ImplicitBindablesAttribute(params string[] patterns) + { + Patterns = patterns; + } +} diff --git a/src/Uno.Extensions.Reactive.UI/Presentation/Bindings/ValueAttribute.cs b/src/Uno.Extensions.Reactive.UI/Presentation/Bindings/ValueAttribute.cs index 899df798b4..1216e6a29d 100644 --- a/src/Uno.Extensions.Reactive.UI/Presentation/Bindings/ValueAttribute.cs +++ b/src/Uno.Extensions.Reactive.UI/Presentation/Bindings/ValueAttribute.cs @@ -4,7 +4,7 @@ namespace Uno.Extensions.Reactive; /// -/// Indicates that the input should be considered as a simple value. It can be get/set/ through bindings, but it won't be de-normalized. +/// Indicates that the input should be considered as a simple value. It can be get/set through bindings, but it won't be de-normalized. /// /// This should be used for records inputs for which we want to disable the default de-normalization. [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property)]