Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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();
Expand All @@ -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;

Expand Down
105 changes: 99 additions & 6 deletions src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -17,25 +17,45 @@ namespace SGReactiveUI.SourceGenerators.Test;
/// TestClass.
/// </summary>
[DataContract]
public partial class TestViewModel : ReactiveObject
public partial class TestViewModel : ReactiveObject, IDisposable
{
private readonly IObservable<bool> _observable = Observable.Return(true);
private readonly Subject<double?> _testSubject = new();
private readonly Subject<double> _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;

/// <summary>
/// Initializes a new instance of the <see cref="TestViewModel"/> class.
/// </summary>
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);
Expand All @@ -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();
Expand Down Expand Up @@ -116,7 +174,42 @@ public TestViewModel()
/// Observable of double.
/// </returns>
[ObservableAsProperty(PropertyName = "MyReadOnlyProperty")]
public IObservable<double> ObservableAsPropertyTest() => Observable.Return(10.0);
public IObservable<double?> ObservableAsPropertyTest() => _testSubject;

/// <summary>
/// Observables as property test non null.
/// </summary>
/// <returns>Observable of double.</returns>
[ObservableAsProperty(PropertyName = "MyReadOnlyNonNullProperty")]
public IObservable<double> ObservableAsPropertyTestNonNull() => _testNonNullSubject;

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_testSubject.Dispose();
_testNonNullSubject.Dispose();
}

_disposedValue = true;
}
}

/// <summary>
/// Test1s this instance.
Expand Down Expand Up @@ -168,7 +261,7 @@ public TestViewModel()
/// <param name="i">The i.</param>
/// <returns>An Observable of int.</returns>
[ReactiveCommand]
private IObservable<double> Test8Observable(int i) => Observable.Return(i + 10.0);
private IObservable<double?> Test8Observable(int i) => Observable.Return<double?>(i + 10.0);

[ReactiveCommand]
private async Task Test9Async(CancellationToken ct) => await Task.Delay(2000, ct);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,43 +29,63 @@ internal static ImmutableArray<MemberDeclarationSyntax> 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 =
propertyInfo.ForwardedPropertyAttributes
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
.ToImmutableArray();

// Get the property type syntax
TypeSyntax propertyType = IdentifierName(propertyInfo.GetObservableTypeText());
return ImmutableArray.Create<MemberDeclarationSyntax>(
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($"/// <inheritdoc cref=\"{getterFieldIdentifierName + "Helper"}\"/>")), 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($"/// <inheritdoc cref=\"{getterFieldIdentifierName}\"/>")), 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($"/// <inheritdoc cref=\"{propertyInfo.PropertyName}\"/>")), 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($"/// <inheritdoc cref=\"{getterFieldIdentifierName + "Helper"}\"/>")), 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($"/// <inheritdoc cref=\"{getterFieldIdentifierName}\"/>")), 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)
Expand Down
Loading