diff --git a/README.md b/README.md index d45b0ed..c812874 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,12 @@ public partial class MyReactiveClass : ReactiveObject ## Usage ObservableAsPropertyHelper `[ObservableAsProperty]` +ObservableAsPropertyHelper is used to create a read-only property from an IObservable. The generated code will create a backing field and a property that returns the value of the backing field. The backing field is initialized with the value of the IObservable when the class is instantiated. + +A private field is created with the name of the property prefixed with an underscore. The field is initialized with the value of the IObservable when the class is instantiated. The property is created with the same name as the field without the underscore. The property returns the value of the field until initialized, then it returns the value of the IObservable. + +You can define the name of the property by using the PropertyName parameter. If you do not define the PropertyName, the property name will be the same as the field name without the underscore. + ### Usage ObservableAsPropertyHelper with Field ```csharp using ReactiveUI.SourceGenerators; @@ -112,7 +118,10 @@ using ReactiveUI.SourceGenerators; public partial class MyReactiveClass : ReactiveObject { public MyReactiveClass() - { + { + // default value for MyObservableProperty prior to initialization. + _myObservable = "Test Value Pre Init"; + // Initialize generated _myObservablePropertyHelper // for the generated MyObservableProperty InitializeOAPH(); @@ -130,7 +139,10 @@ using ReactiveUI.SourceGenerators; public partial class MyReactiveClass : ReactiveObject { public MyReactiveClass() - { + { + // default value for TestValueProperty prior to initialization. + _testValueProperty = "Test Value Pre Init"; + // Initialize generated _testValuePropertyHelper // for the generated TestValueProperty InitializeOAPH(); @@ -143,7 +155,7 @@ public partial class MyReactiveClass : ReactiveObject ### Usage ObservableAsPropertyHelper with Observable Method -NOTE: This does not support methods with parameters +NOTE: This does not currently support methods with parameters ```csharp using ReactiveUI.SourceGenerators; diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs index 245e4c2..4ee8126 100644 --- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs +++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs @@ -5,9 +5,9 @@ using System.Reactive; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Runtime.Serialization; using System.Text.Json.Serialization; -using System.Windows.Media.TextFormatting; using ReactiveUI; using ReactiveUI.SourceGenerators; @@ -17,25 +17,45 @@ namespace SGReactiveUI.SourceGenerators.Test; /// TestClass. /// [DataContract] -public partial class TestViewModel : ReactiveObject +public partial class TestViewModel : ReactiveObject, IDisposable { private readonly IObservable _observable = Observable.Return(true); + private readonly Subject _testSubject = new(); + private readonly Subject _testNonNullSubject = new(); [JsonInclude] [DataMember] [ObservableAsProperty] - private double _test2Property = 1.1d; + private double? _test2Property = 1.1d; [JsonInclude] [Reactive(SetModifier = AccessModifier.Protected)] [DataMember] private int _test1Property = 10; + private bool _disposedValue; /// /// Initializes a new instance of the class. /// public TestViewModel() { + Console.Out.WriteLine("MyReadOnlyProperty before init"); + + // only settable prior to init, after init it will be ignored. + _myReadOnlyProperty = -1.0; + Console.Out.WriteLine(MyReadOnlyProperty); + Console.Out.WriteLine(_myReadOnlyProperty); + + Console.Out.WriteLine("MyReadOnlyNonNullProperty before init"); + + // only settable prior to init, after init it will be ignored. + _myReadOnlyNonNullProperty = -5.0; + Console.Out.WriteLine(MyReadOnlyNonNullProperty); + Console.Out.WriteLine(_myReadOnlyNonNullProperty); + + _observableAsPropertyTest2Property = 11223344; + Console.Out.WriteLine(ObservableAsPropertyTest2Property); + Console.Out.WriteLine(_observableAsPropertyTest2Property); InitializeOAPH(); Console.Out.WriteLine(Test1Command); @@ -59,11 +79,49 @@ public TestViewModel() Console.Out.WriteLine($"Test2Property default Value: {Test2Property}"); _test2PropertyHelper = Test8ObservableCommand!.ToProperty(this, x => x.Test2Property); - Test8ObservableCommand?.Execute(100).Subscribe(Console.Out.WriteLine); + Test8ObservableCommand?.Execute(100).Subscribe(d => Console.Out.WriteLine(d)); Console.Out.WriteLine($"Test2Property Value: {Test2Property}"); Console.Out.WriteLine($"Test2Property underlying Value: {_test2Property}"); Console.Out.WriteLine(ObservableAsPropertyTest2Property); + + Console.Out.WriteLine("MyReadOnlyProperty After Init"); + + // setting this value should not update the _myReadOnlyPropertyHelper as the _testSubject has not been updated yet but the _myReadOnlyPropertyHelper should be updated with null upon init. + _myReadOnlyProperty = -2.0; + + // null value expected as the _testSubject has not been updated yet, ignoring the private variable. Console.Out.WriteLine(MyReadOnlyProperty); + Console.Out.WriteLine(_myReadOnlyProperty); + _testSubject.OnNext(10.0); + + // expected value 10 as the _testSubject has been updated. + Console.Out.WriteLine(MyReadOnlyProperty); + Console.Out.WriteLine(_myReadOnlyProperty); + _testSubject.OnNext(null); + + // expected value null as the _testSubject has been updated. + Console.Out.WriteLine(MyReadOnlyProperty); + Console.Out.WriteLine(_myReadOnlyProperty); + + Console.Out.WriteLine("MyReadOnlyNonNullProperty After Init"); + + // setting this value should not update the _myReadOnlyNonNullProperty as the _testNonNullSubject has not been updated yet but the _myReadOnlyNonNullPropertyHelper should be updated with null upon init. + _myReadOnlyNonNullProperty = -2.0; + + // 0 value expected as the _testNonNullSubject has not been updated yet, ignoring the private variable. + Console.Out.WriteLine(MyReadOnlyNonNullProperty); + Console.Out.WriteLine(_myReadOnlyNonNullProperty); + _testNonNullSubject.OnNext(11.0); + + // expected value 11 as the _testNonNullSubject has been updated. + Console.Out.WriteLine(MyReadOnlyNonNullProperty); + Console.Out.WriteLine(_myReadOnlyNonNullProperty); + _testNonNullSubject.OnNext(default); + + // expected value 0 as the _testNonNullSubject has been updated. + Console.Out.WriteLine(MyReadOnlyNonNullProperty); + Console.Out.WriteLine(_myReadOnlyNonNullProperty); + Test9AsyncCommand?.ThrownExceptions.Subscribe(Console.Out.WriteLine); var cancel = Test9AsyncCommand?.Execute().Subscribe(); Task.Delay(1000).Wait(); @@ -116,7 +174,42 @@ public TestViewModel() /// Observable of double. /// [ObservableAsProperty(PropertyName = "MyReadOnlyProperty")] - public IObservable ObservableAsPropertyTest() => Observable.Return(10.0); + public IObservable ObservableAsPropertyTest() => _testSubject; + + /// + /// Observables as property test non null. + /// + /// Observable of double. + [ObservableAsProperty(PropertyName = "MyReadOnlyNonNullProperty")] + public IObservable ObservableAsPropertyTestNonNull() => _testNonNullSubject; + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _testSubject.Dispose(); + _testNonNullSubject.Dispose(); + } + + _disposedValue = true; + } + } /// /// Test1s this instance. @@ -168,7 +261,7 @@ public TestViewModel() /// The i. /// An Observable of int. [ReactiveCommand] - private IObservable Test8Observable(int i) => Observable.Return(i + 10.0); + private IObservable Test8Observable(int i) => Observable.Return(i + 10.0); [ReactiveCommand] private async Task Test9Async(CancellationToken ct) => await Task.Delay(2000, ct); diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyFromObservableGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyFromObservableGenerator.Execute.cs index 04a232d..efccc15 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyFromObservableGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyFromObservableGenerator.Execute.cs @@ -29,7 +29,18 @@ internal static ImmutableArray GetPropertySyntax(Observ { var getterFieldIdentifierName = GetGeneratedFieldName(propertyInfo); - var getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName}Helper?.Value ?? default")); + // Get the property type syntax + TypeSyntax propertyType = IdentifierName(propertyInfo.GetObservableTypeText()); + + ArrowExpressionClauseSyntax getterArrowExpression; + if (propertyType.ToFullString().EndsWith("?")) + { + getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = ({getterFieldIdentifierName}Helper == null ? {getterFieldIdentifierName} : {getterFieldIdentifierName}Helper.Value)")); + } + else + { + getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = {getterFieldIdentifierName}Helper?.Value ?? {getterFieldIdentifierName}")); + } // Prepare the forwarded attributes, if any var forwardedAttributes = @@ -37,35 +48,44 @@ internal static ImmutableArray GetPropertySyntax(Observ .Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax()))) .ToImmutableArray(); - // Get the property type syntax - TypeSyntax propertyType = IdentifierName(propertyInfo.GetObservableTypeText()); return ImmutableArray.Create( - FieldDeclaration(VariableDeclaration(ParseTypeName($"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>?"))) - .AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName + "Helper")) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList()))) - .AddModifiers( - Token(SyntaxKind.PrivateKeyword)), - PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName)) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).FullName))), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).Assembly.GetName().Version.ToString())))))) - .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage))))) - .AddAttributeLists([.. forwardedAttributes]) - .AddModifiers(Token(SyntaxKind.PublicKeyword)) - .AddAccessorListAccessors( - AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) - .WithExpressionBody(getterArrowExpression) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)))); + FieldDeclaration(VariableDeclaration(propertyType)) + .AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName)) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) + .AddArgumentListArguments( + 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)), + FieldDeclaration(VariableDeclaration(ParseTypeName($"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>?"))) + .AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName + "Helper")) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).Assembly.GetName().Version.ToString())))))) + .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList()))) + .AddModifiers( + Token(SyntaxKind.PrivateKeyword)), + PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName)) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).Assembly.GetName().Version.ToString())))))) + .WithOpenBracketToken(Token(TriviaList(Comment($"/// ")), SyntaxKind.OpenBracketToken, TriviaList())), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage))))) + .AddAttributeLists([.. forwardedAttributes]) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithExpressionBody(getterArrowExpression) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)))); } internal static MethodDeclarationSyntax GetPropertyInitiliser(ObservableMethodInfo[] propertyInfos) diff --git a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs index 05617fa..77b5c67 100644 --- a/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs +++ b/src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyGenerator.Execute.cs @@ -62,7 +62,16 @@ internal static ImmutableArray GetPropertySyntax(Proper getterFieldIdentifierName = propertyInfo.FieldName; } - var getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = ({getterFieldIdentifierName}Helper?.Value ?? {getterFieldIdentifierName})")); + ArrowExpressionClauseSyntax getterArrowExpression; + + if (propertyInfo.TypeNameWithNullabilityAnnotations.EndsWith("?")) + { + getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = ({getterFieldIdentifierName}Helper == null ? {getterFieldIdentifierName} : {getterFieldIdentifierName}Helper.Value)")); + } + else + { + getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = {getterFieldIdentifierName}Helper?.Value ?? {getterFieldIdentifierName}")); + } // Prepare the forwarded attributes, if any var forwardedAttributes = @@ -83,32 +92,32 @@ internal static ImmutableArray GetPropertySyntax(Proper return ImmutableArray.Create( FieldDeclaration(VariableDeclaration(ParseTypeName($"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>"))) - .AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName + "Helper")) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - 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)), + .AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName + "Helper")) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) + .AddArgumentListArguments( + 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)), PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName)) - .AddAttributeLists( - AttributeList(SingletonSeparatedList( - Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) - .AddArgumentListArguments( - 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())), - AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage))))) - .AddAttributeLists([.. forwardedAttributes]) - .AddModifiers(Token(SyntaxKind.PublicKeyword)) - .AddAccessorListAccessors( - AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) - .WithExpressionBody(getterArrowExpression) - .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)))); + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName(AttributeDefinitions.GeneratedCode)) + .AddArgumentListArguments( + 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())), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage))))) + .AddAttributeLists([.. forwardedAttributes]) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .AddAccessorListAccessors( + AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) + .WithExpressionBody(getterArrowExpression) + .WithSemicolonToken(Token(SyntaxKind.SemicolonToken)))); } internal static bool GetFieldInfoFromClass(