Skip to content

Commit 61b910b

Browse files
authored
Fix For OAPH nullable values not set null after initial value (#69)
Fix For OAPH null values when set null after initial value closes #63
1 parent 0910397 commit 61b910b

File tree

4 files changed

+198
-64
lines changed

4 files changed

+198
-64
lines changed

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ public partial class MyReactiveClass : ReactiveObject
8686

8787
## Usage ObservableAsPropertyHelper `[ObservableAsProperty]`
8888

89+
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.
90+
91+
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.
92+
93+
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.
94+
8995
### Usage ObservableAsPropertyHelper with Field
9096
```csharp
9197
using ReactiveUI.SourceGenerators;
@@ -112,7 +118,10 @@ using ReactiveUI.SourceGenerators;
112118
public partial class MyReactiveClass : ReactiveObject
113119
{
114120
public MyReactiveClass()
115-
{
121+
{
122+
// default value for MyObservableProperty prior to initialization.
123+
_myObservable = "Test Value Pre Init";
124+
116125
// Initialize generated _myObservablePropertyHelper
117126
// for the generated MyObservableProperty
118127
InitializeOAPH();
@@ -130,7 +139,10 @@ using ReactiveUI.SourceGenerators;
130139
public partial class MyReactiveClass : ReactiveObject
131140
{
132141
public MyReactiveClass()
133-
{
142+
{
143+
// default value for TestValueProperty prior to initialization.
144+
_testValueProperty = "Test Value Pre Init";
145+
134146
// Initialize generated _testValuePropertyHelper
135147
// for the generated TestValueProperty
136148
InitializeOAPH();
@@ -143,7 +155,7 @@ public partial class MyReactiveClass : ReactiveObject
143155

144156
### Usage ObservableAsPropertyHelper with Observable Method
145157

146-
NOTE: This does not support methods with parameters
158+
NOTE: This does not currently support methods with parameters
147159
```csharp
148160
using ReactiveUI.SourceGenerators;
149161

src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55

66
using System.Reactive;
77
using System.Reactive.Linq;
8+
using System.Reactive.Subjects;
89
using System.Runtime.Serialization;
910
using System.Text.Json.Serialization;
10-
using System.Windows.Media.TextFormatting;
1111
using ReactiveUI;
1212
using ReactiveUI.SourceGenerators;
1313

@@ -17,25 +17,45 @@ namespace SGReactiveUI.SourceGenerators.Test;
1717
/// TestClass.
1818
/// </summary>
1919
[DataContract]
20-
public partial class TestViewModel : ReactiveObject
20+
public partial class TestViewModel : ReactiveObject, IDisposable
2121
{
2222
private readonly IObservable<bool> _observable = Observable.Return(true);
23+
private readonly Subject<double?> _testSubject = new();
24+
private readonly Subject<double> _testNonNullSubject = new();
2325

2426
[JsonInclude]
2527
[DataMember]
2628
[ObservableAsProperty]
27-
private double _test2Property = 1.1d;
29+
private double? _test2Property = 1.1d;
2830

2931
[JsonInclude]
3032
[Reactive(SetModifier = AccessModifier.Protected)]
3133
[DataMember]
3234
private int _test1Property = 10;
35+
private bool _disposedValue;
3336

3437
/// <summary>
3538
/// Initializes a new instance of the <see cref="TestViewModel"/> class.
3639
/// </summary>
3740
public TestViewModel()
3841
{
42+
Console.Out.WriteLine("MyReadOnlyProperty before init");
43+
44+
// only settable prior to init, after init it will be ignored.
45+
_myReadOnlyProperty = -1.0;
46+
Console.Out.WriteLine(MyReadOnlyProperty);
47+
Console.Out.WriteLine(_myReadOnlyProperty);
48+
49+
Console.Out.WriteLine("MyReadOnlyNonNullProperty before init");
50+
51+
// only settable prior to init, after init it will be ignored.
52+
_myReadOnlyNonNullProperty = -5.0;
53+
Console.Out.WriteLine(MyReadOnlyNonNullProperty);
54+
Console.Out.WriteLine(_myReadOnlyNonNullProperty);
55+
56+
_observableAsPropertyTest2Property = 11223344;
57+
Console.Out.WriteLine(ObservableAsPropertyTest2Property);
58+
Console.Out.WriteLine(_observableAsPropertyTest2Property);
3959
InitializeOAPH();
4060

4161
Console.Out.WriteLine(Test1Command);
@@ -59,11 +79,49 @@ public TestViewModel()
5979
Console.Out.WriteLine($"Test2Property default Value: {Test2Property}");
6080
_test2PropertyHelper = Test8ObservableCommand!.ToProperty(this, x => x.Test2Property);
6181

62-
Test8ObservableCommand?.Execute(100).Subscribe(Console.Out.WriteLine);
82+
Test8ObservableCommand?.Execute(100).Subscribe(d => Console.Out.WriteLine(d));
6383
Console.Out.WriteLine($"Test2Property Value: {Test2Property}");
6484
Console.Out.WriteLine($"Test2Property underlying Value: {_test2Property}");
6585
Console.Out.WriteLine(ObservableAsPropertyTest2Property);
86+
87+
Console.Out.WriteLine("MyReadOnlyProperty After Init");
88+
89+
// 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.
90+
_myReadOnlyProperty = -2.0;
91+
92+
// null value expected as the _testSubject has not been updated yet, ignoring the private variable.
6693
Console.Out.WriteLine(MyReadOnlyProperty);
94+
Console.Out.WriteLine(_myReadOnlyProperty);
95+
_testSubject.OnNext(10.0);
96+
97+
// expected value 10 as the _testSubject has been updated.
98+
Console.Out.WriteLine(MyReadOnlyProperty);
99+
Console.Out.WriteLine(_myReadOnlyProperty);
100+
_testSubject.OnNext(null);
101+
102+
// expected value null as the _testSubject has been updated.
103+
Console.Out.WriteLine(MyReadOnlyProperty);
104+
Console.Out.WriteLine(_myReadOnlyProperty);
105+
106+
Console.Out.WriteLine("MyReadOnlyNonNullProperty After Init");
107+
108+
// 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.
109+
_myReadOnlyNonNullProperty = -2.0;
110+
111+
// 0 value expected as the _testNonNullSubject has not been updated yet, ignoring the private variable.
112+
Console.Out.WriteLine(MyReadOnlyNonNullProperty);
113+
Console.Out.WriteLine(_myReadOnlyNonNullProperty);
114+
_testNonNullSubject.OnNext(11.0);
115+
116+
// expected value 11 as the _testNonNullSubject has been updated.
117+
Console.Out.WriteLine(MyReadOnlyNonNullProperty);
118+
Console.Out.WriteLine(_myReadOnlyNonNullProperty);
119+
_testNonNullSubject.OnNext(default);
120+
121+
// expected value 0 as the _testNonNullSubject has been updated.
122+
Console.Out.WriteLine(MyReadOnlyNonNullProperty);
123+
Console.Out.WriteLine(_myReadOnlyNonNullProperty);
124+
67125
Test9AsyncCommand?.ThrownExceptions.Subscribe(Console.Out.WriteLine);
68126
var cancel = Test9AsyncCommand?.Execute().Subscribe();
69127
Task.Delay(1000).Wait();
@@ -116,7 +174,42 @@ public TestViewModel()
116174
/// Observable of double.
117175
/// </returns>
118176
[ObservableAsProperty(PropertyName = "MyReadOnlyProperty")]
119-
public IObservable<double> ObservableAsPropertyTest() => Observable.Return(10.0);
177+
public IObservable<double?> ObservableAsPropertyTest() => _testSubject;
178+
179+
/// <summary>
180+
/// Observables as property test non null.
181+
/// </summary>
182+
/// <returns>Observable of double.</returns>
183+
[ObservableAsProperty(PropertyName = "MyReadOnlyNonNullProperty")]
184+
public IObservable<double> ObservableAsPropertyTestNonNull() => _testNonNullSubject;
185+
186+
/// <summary>
187+
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
188+
/// </summary>
189+
public void Dispose()
190+
{
191+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
192+
Dispose(disposing: true);
193+
GC.SuppressFinalize(this);
194+
}
195+
196+
/// <summary>
197+
/// Releases unmanaged and - optionally - managed resources.
198+
/// </summary>
199+
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
200+
protected virtual void Dispose(bool disposing)
201+
{
202+
if (!_disposedValue)
203+
{
204+
if (disposing)
205+
{
206+
_testSubject.Dispose();
207+
_testNonNullSubject.Dispose();
208+
}
209+
210+
_disposedValue = true;
211+
}
212+
}
120213

121214
/// <summary>
122215
/// Test1s this instance.
@@ -168,7 +261,7 @@ public TestViewModel()
168261
/// <param name="i">The i.</param>
169262
/// <returns>An Observable of int.</returns>
170263
[ReactiveCommand]
171-
private IObservable<double> Test8Observable(int i) => Observable.Return(i + 10.0);
264+
private IObservable<double?> Test8Observable(int i) => Observable.Return<double?>(i + 10.0);
172265

173266
[ReactiveCommand]
174267
private async Task Test9Async(CancellationToken ct) => await Task.Delay(2000, ct);

src/ReactiveUI.SourceGenerators/ObservableAsProperty/ObservableAsPropertyFromObservableGenerator.Execute.cs

Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -29,43 +29,63 @@ internal static ImmutableArray<MemberDeclarationSyntax> GetPropertySyntax(Observ
2929
{
3030
var getterFieldIdentifierName = GetGeneratedFieldName(propertyInfo);
3131

32-
var getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName}Helper?.Value ?? default"));
32+
// Get the property type syntax
33+
TypeSyntax propertyType = IdentifierName(propertyInfo.GetObservableTypeText());
34+
35+
ArrowExpressionClauseSyntax getterArrowExpression;
36+
if (propertyType.ToFullString().EndsWith("?"))
37+
{
38+
getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = ({getterFieldIdentifierName}Helper == null ? {getterFieldIdentifierName} : {getterFieldIdentifierName}Helper.Value)"));
39+
}
40+
else
41+
{
42+
getterArrowExpression = ArrowExpressionClause(ParseExpression($"{getterFieldIdentifierName} = {getterFieldIdentifierName}Helper?.Value ?? {getterFieldIdentifierName}"));
43+
}
3344

3445
// Prepare the forwarded attributes, if any
3546
var forwardedAttributes =
3647
propertyInfo.ForwardedPropertyAttributes
3748
.Select(static a => AttributeList(SingletonSeparatedList(a.GetSyntax())))
3849
.ToImmutableArray();
3950

40-
// Get the property type syntax
41-
TypeSyntax propertyType = IdentifierName(propertyInfo.GetObservableTypeText());
4251
return ImmutableArray.Create<MemberDeclarationSyntax>(
43-
FieldDeclaration(VariableDeclaration(ParseTypeName($"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>?")))
44-
.AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName + "Helper"))
45-
.AddAttributeLists(
46-
AttributeList(SingletonSeparatedList(
47-
Attribute(IdentifierName(AttributeDefinitions.GeneratedCode))
48-
.AddArgumentListArguments(
49-
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).FullName))),
50-
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).Assembly.GetName().Version.ToString()))))))
51-
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <inheritdoc cref=\"{getterFieldIdentifierName + "Helper"}\"/>")), SyntaxKind.OpenBracketToken, TriviaList())))
52-
.AddModifiers(
53-
Token(SyntaxKind.PrivateKeyword)),
54-
PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName))
55-
.AddAttributeLists(
56-
AttributeList(SingletonSeparatedList(
57-
Attribute(IdentifierName(AttributeDefinitions.GeneratedCode))
58-
.AddArgumentListArguments(
59-
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).FullName))),
60-
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).Assembly.GetName().Version.ToString()))))))
61-
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <inheritdoc cref=\"{getterFieldIdentifierName}\"/>")), SyntaxKind.OpenBracketToken, TriviaList())),
62-
AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage)))))
63-
.AddAttributeLists([.. forwardedAttributes])
64-
.AddModifiers(Token(SyntaxKind.PublicKeyword))
65-
.AddAccessorListAccessors(
66-
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
67-
.WithExpressionBody(getterArrowExpression)
68-
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))));
52+
FieldDeclaration(VariableDeclaration(propertyType))
53+
.AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName))
54+
.AddAttributeLists(
55+
AttributeList(SingletonSeparatedList(
56+
Attribute(IdentifierName(AttributeDefinitions.GeneratedCode))
57+
.AddArgumentListArguments(
58+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).FullName))),
59+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyGenerator).Assembly.GetName().Version.ToString()))))))
60+
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <inheritdoc cref=\"{propertyInfo.PropertyName}\"/>")), SyntaxKind.OpenBracketToken, TriviaList())))
61+
.AddModifiers(
62+
Token(SyntaxKind.PrivateKeyword)),
63+
FieldDeclaration(VariableDeclaration(ParseTypeName($"ReactiveUI.ObservableAsPropertyHelper<{propertyType}>?")))
64+
.AddDeclarationVariables(VariableDeclarator(getterFieldIdentifierName + "Helper"))
65+
.AddAttributeLists(
66+
AttributeList(SingletonSeparatedList(
67+
Attribute(IdentifierName(AttributeDefinitions.GeneratedCode))
68+
.AddArgumentListArguments(
69+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).FullName))),
70+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).Assembly.GetName().Version.ToString()))))))
71+
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <inheritdoc cref=\"{getterFieldIdentifierName + "Helper"}\"/>")), SyntaxKind.OpenBracketToken, TriviaList())))
72+
.AddModifiers(
73+
Token(SyntaxKind.PrivateKeyword)),
74+
PropertyDeclaration(propertyType, Identifier(propertyInfo.PropertyName))
75+
.AddAttributeLists(
76+
AttributeList(SingletonSeparatedList(
77+
Attribute(IdentifierName(AttributeDefinitions.GeneratedCode))
78+
.AddArgumentListArguments(
79+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).FullName))),
80+
AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(ObservableAsPropertyFromObservableGenerator).Assembly.GetName().Version.ToString()))))))
81+
.WithOpenBracketToken(Token(TriviaList(Comment($"/// <inheritdoc cref=\"{getterFieldIdentifierName}\"/>")), SyntaxKind.OpenBracketToken, TriviaList())),
82+
AttributeList(SingletonSeparatedList(Attribute(IdentifierName(AttributeDefinitions.ExcludeFromCodeCoverage)))))
83+
.AddAttributeLists([.. forwardedAttributes])
84+
.AddModifiers(Token(SyntaxKind.PublicKeyword))
85+
.AddAccessorListAccessors(
86+
AccessorDeclaration(SyntaxKind.GetAccessorDeclaration)
87+
.WithExpressionBody(getterArrowExpression)
88+
.WithSemicolonToken(Token(SyntaxKind.SemicolonToken))));
6989
}
7090

7191
internal static MethodDeclarationSyntax GetPropertyInitiliser(ObservableMethodInfo[] propertyInfos)

0 commit comments

Comments
 (0)