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)]