diff --git a/README.md b/README.md index a42fac3..827acfb 100644 --- a/README.md +++ b/README.md @@ -111,24 +111,29 @@ public partial class MyReactiveClass : ReactiveObject } ``` +### Usage ObservableAsPropertyHelper with Field and non readonly nullable OAPH field +```csharp + ### Usage ObservableAsPropertyHelper with Observable Property ```csharp using ReactiveUI.SourceGenerators; -public partial class MyReactiveClass : ReactiveObject -{ +public partial class MyReactiveClass : ReactiveObject, IActivatableViewModel +{ + [ObservableAsProperty(ReadOnly = false)] + private string _myProperty = "Default Value"; + public MyReactiveClass() { - // default value for MyObservableProperty prior to initialization. - _myObservable = "Test Value Pre Init"; - - // Initialize generated _myObservablePropertyHelper - // for the generated MyObservableProperty - InitializeOAPH(); + this.WhenActivated(disposables => + { + _myPrpertyHelper = MyPropertyObservable() + .ToProperty(this, x => x.MyProperty) + .DisposeWith(disposables); + }); } - [ObservableAsProperty] - IObservable MyObservable => Observable.Return("Test Value"); + IObservable MyPropertyObservable() => Observable.Return("Test Value"); } ``` diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 0f739f1..b87d1e5 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -4,6 +4,7 @@ // See the LICENSE file in the project root for full license information. using System.Reactive; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; using System.Runtime.Serialization; @@ -16,8 +17,11 @@ namespace SGReactiveUI.SourceGenerators.Test; /// /// TestClass. /// +/// +/// +/// [DataContract] -public partial class TestViewModel : ReactiveObject, IDisposable +public partial class TestViewModel : ReactiveObject, IActivatableViewModel, IDisposable { private readonly IObservable _observable = Observable.Return(true); private readonly Subject _testSubject = new(); @@ -28,6 +32,12 @@ public partial class TestViewModel : ReactiveObject, IDisposable [ObservableAsProperty] private double? _test2Property = 1.1d; + [ObservableAsProperty(ReadOnly = false)] + private double? _test11Property = 11.1d; + + [Reactive] + private double? _test12Property = 12.1d; + [JsonInclude] [Reactive(SetModifier = AccessModifier.Protected)] [DataMember] @@ -39,6 +49,12 @@ public partial class TestViewModel : ReactiveObject, IDisposable /// public TestViewModel() { + this.WhenActivated(disposables => + { + Console.Out.WriteLine("Activated"); + _test11PropertyHelper = this.WhenAnyValue(x => x.Test12Property).ToProperty(this, x => x.Test11Property, out _).DisposeWith(disposables); + }); + Console.Out.WriteLine("MyReadOnlyProperty before init"); // only settable prior to init, after init it will be ignored. @@ -201,6 +217,11 @@ public TestViewModel() [ObservableAsProperty] public IObservable ObservableAsPropertyTest2 => Observable.Return(9); + /// + /// Gets the Activator which will be used by the View when Activation/Deactivation occurs. + /// + public ViewModelActivator Activator { get; } = new(); + /// /// Gets observables as property test. /// diff --git a/src/ReactiveUI.SourceGenerators/Core/Helpers/AttributeDefinitions.cs b/src/ReactiveUI.SourceGenerators/Core/Helpers/AttributeDefinitions.cs index 3e4692d..f77fa84 100644 --- a/src/ReactiveUI.SourceGenerators/Core/Helpers/AttributeDefinitions.cs +++ b/src/ReactiveUI.SourceGenerators/Core/Helpers/AttributeDefinitions.cs @@ -161,6 +161,14 @@ internal sealed class ObservableAsPropertyAttribute : Attribute /// The name of the property. /// public string? PropertyName { get; init; } + + /// + /// Gets the Readonly state of the OAPH property. + /// + /// + /// The is read only of the OAPH property. + /// + public bool ReadOnly { get; init; } = true; } #nullable restore #pragma warning restore diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs index 77b5c67..d82583b 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs @@ -3,6 +3,7 @@ // 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.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -79,6 +80,19 @@ internal static ImmutableArray GetPropertySyntax(Proper .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) .ToImmutableArray(); + var modifiers = new List(); + var helperTypeName = $"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>"; + if (propertyInfo.AccessModifier == "readonly") + { + modifiers.Add(Token(SyntaxKind.PrivateKeyword)); + modifiers.Add(Token(SyntaxKind.ReadOnlyKeyword)); + } + else + { + helperTypeName = $"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>?"; + modifiers.Add(Token(SyntaxKind.PrivateKeyword)); + } + // Construct the generated property as follows: // // /// @@ -91,7 +105,7 @@ internal static ImmutableArray GetPropertySyntax(Proper // } return ImmutableArray.Create( - FieldDeclaration(VariableDeclaration(ParseTypeName($"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>"))) + FieldDeclaration(VariableDeclaration(ParseTypeName(helperTypeName))) .AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName + "Helper")) .AddAttributeLists( AttributeList(SingletonSeparatedList( @@ -100,9 +114,7 @@ internal static ImmutableArray GetPropertySyntax(Proper AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).FullName))), AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString())))))) .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList()))) - .AddModifiers( - Token(SyntaxKind.PrivateKeyword), - Token(SyntaxKind.ReadOnlyKeyword)), + .AddModifiers([.. modifiers]), PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName)) .AddAttributeLists( AttributeList(SingletonSeparatedList( @@ -124,6 +136,7 @@ internal static bool GetFieldInfoFromClass( FieldDeclarationSyntax fieldSyntax, IFieldSymbol fieldSymbol, SemanticModel semanticModel, + bool? isReadonly, CancellationToken token, [NotNullWhen(true)] out PropertyInfo? propertyInfo, out ImmutableArray diagnostics) @@ -282,7 +295,7 @@ internal static bool GetFieldInfoFromClass( isReferenceTypeOrUnconstraindTypeParameter, includeMemberNotNullOnSetAccessor, forwardedAttributes.ToImmutable(), - "public"); + isReadonly == false ? string.Empty : "readonly"); diagnostics = builder.ToImmutable(); diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.cs index 0e239e0..6a3a02d 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.cs @@ -35,6 +35,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context) static (node, _) => node is VariableDeclaratorSyntax { Parent: VariableDeclarationSyntax { Parent: FieldDeclarationSyntax { Parent: ClassDeclarationSyntax or RecordDeclarationSyntax, AttributeLists.Count: > 0 } } }, static (context, token) => { + var symbol = ModelExtensions.GetDeclaredSymbol(context.SemanticModel, context.TargetNode, token)!; + token.ThrowIfCancellationRequested(); + + // Skip symbols without the target attribute + if (!symbol.TryGetAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ObservableAsPropertyAttributeType, out var attributeData)) + { + return default; + } + + // Get the can PropertyName member, if any + attributeData.TryGetNamedArgument("ReadOnly", out bool? isReadonly); + var fieldDeclaration = (FieldDeclarationSyntax)context.TargetNode.Parent!.Parent!; var fieldSymbol = (IFieldSymbol)context.TargetSymbol; @@ -43,7 +55,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) token.ThrowIfCancellationRequested(); - Execute.GetFieldInfoFromClass(fieldDeclaration, fieldSymbol, context.SemanticModel, token, out var propertyInfo, out var diagnostics); + Execute.GetFieldInfoFromClass(fieldDeclaration, fieldSymbol, context.SemanticModel, isReadonly, token, out var propertyInfo, out var diagnostics); token.ThrowIfCancellationRequested(); return (Hierarchy: hierarchy, new Result(propertyInfo, diagnostics));