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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,9 @@ public partial class MyReactiveClass : ReactiveObject
}
```

String values are automatically quoted, so `InitialValue = ""` produces an empty string initial value.
Non nullable `string` properties are initialized with `string.Empty` when no `InitialValue` is provided.

## Usage ReactiveCommand `[ReactiveCommand]`

### Usage ReactiveCommand without parameter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//HintName: ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.cs
// Copyright (c) 2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

// <auto-generated/>
#pragma warning disable
#nullable enable
namespace ReactiveUI.SourceGenerators;

/// <summary>
/// ObservableAsPropertyAttribute.
/// </summary>
/// <seealso cref="Attribute" />
[global::System.AttributeUsage(global::System.AttributeTargets.Field | global::System.AttributeTargets.Property | global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class ObservableAsPropertyAttribute : global::System.Attribute
{
/// <summary>
/// Gets the name of the property.
/// </summary>
/// <value>
/// The name of the property.
/// </value>
public string? PropertyName { get; init; }

/// <summary>
/// Gets the Readonly state of the OAPH property.
/// </summary>
/// <value>
/// The is read only of the OAPH property.
/// </value>
public bool ReadOnly { get; init; } = true;

/// <summary>
/// Gets the AccessModifier of the OAPH property.
/// </summary>
/// <value>
/// The AccessModifier of the OAPH property, protected if true.
/// </value>
public bool UseProtected { get; init; } = false;

/// <summary>
/// Gets the inheritance modifier of the generated property.
/// </summary>
public InheritanceModifier Inheritance { get; init; }

/// <summary>
/// Gets the Initial Value of the OAPH property.
/// This should be a string representation of the value. i.e. "42" or "DateTime.Now" or "new List&lt;string&gt;()".
/// String values are automatically quoted.
/// </summary>
public string? InitialValue { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//HintName: TestNs.TestVM.ObservableAsPropertyFromObservable.g.cs
// <auto-generated/>
using ReactiveUI;

#pragma warning disable
#nullable enable

namespace TestNs
{

public partial class TestVM
{
/// <inheritdoc cref="PLCActive"/>
private string _pLCActive = "";

/// <inheritdoc cref="_pLCActiveHelper"/>
private readonly ReactiveUI.ObservableAsPropertyHelper<string> _pLCActiveHelper;

/// <inheritdoc cref="_pLCActive"/>
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public partial string PLCActive { get => _pLCActive = (_pLCActiveHelper == null ? _pLCActive : _pLCActiveHelper.Value); }

[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
protected void InitializeOAPH()
{

}
}
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//HintName: ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.cs
// Copyright (c) 2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

// <auto-generated/>
#pragma warning disable
#nullable enable
namespace ReactiveUI.SourceGenerators;

/// <summary>
/// ObservableAsPropertyAttribute.
/// </summary>
/// <seealso cref="Attribute" />
[global::System.AttributeUsage(global::System.AttributeTargets.Field | global::System.AttributeTargets.Property | global::System.AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
internal sealed class ObservableAsPropertyAttribute : global::System.Attribute
{
/// <summary>
/// Gets the name of the property.
/// </summary>
/// <value>
/// The name of the property.
/// </value>
public string? PropertyName { get; init; }

/// <summary>
/// Gets the Readonly state of the OAPH property.
/// </summary>
/// <value>
/// The is read only of the OAPH property.
/// </value>
public bool ReadOnly { get; init; } = true;

/// <summary>
/// Gets the AccessModifier of the OAPH property.
/// </summary>
/// <value>
/// The AccessModifier of the OAPH property, protected if true.
/// </value>
public bool UseProtected { get; init; } = false;

/// <summary>
/// Gets the inheritance modifier of the generated property.
/// </summary>
public InheritanceModifier Inheritance { get; init; }

/// <summary>
/// Gets the Initial Value of the OAPH property.
/// This should be a string representation of the value. i.e. "42" or "DateTime.Now" or "new List&lt;string&gt;()".
/// String values are automatically quoted.
/// </summary>
public string? InitialValue { get; init; }
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//HintName: TestNs.TestVM.ObservableAsPropertyFromObservable.g.cs
// <auto-generated/>
using ReactiveUI;

#pragma warning disable
#nullable enable

namespace TestNs
{

public partial class TestVM
{
/// <inheritdoc cref="PLCActive"/>
private string _pLCActive = string.Empty;

/// <inheritdoc cref="_pLCActiveHelper"/>
private readonly ReactiveUI.ObservableAsPropertyHelper<string> _pLCActiveHelper;

/// <inheritdoc cref="_pLCActive"/>
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public partial string PLCActive { get => _pLCActive = (_pLCActiveHelper == null ? _pLCActive : _pLCActiveHelper.Value); }
/// <inheritdoc cref="PLCName"/>
private string? _pLCName;

/// <inheritdoc cref="_pLCNameHelper"/>
private readonly ReactiveUI.ObservableAsPropertyHelper<string?> _pLCNameHelper;

/// <inheritdoc cref="_pLCName"/>
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public partial string? PLCName { get => _pLCName = (_pLCNameHelper == null ? _pLCName : _pLCNameHelper.Value); }

[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
protected void InitializeOAPH()
{

}
}
}
#nullable restore
#pragma warning restore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public partial class Level1
public partial class Level2
{
/// <inheritdoc cref="Level2ObservableProperty"/>
private string _level2ObservableProperty;
private string _level2ObservableProperty = string.Empty;

/// <inheritdoc cref="_level2ObservablePropertyHelper"/>
private ReactiveUI.ObservableAsPropertyHelper<string>? _level2ObservablePropertyHelper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace TestNs
public partial class TestVM
{
/// <inheritdoc cref="InternalStateProperty"/>
private string _internalStateProperty;
private string _internalStateProperty = string.Empty;

/// <inheritdoc cref="_internalStatePropertyHelper"/>
private ReactiveUI.ObservableAsPropertyHelper<string>? _internalStatePropertyHelper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public partial class TestVM
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public bool HasItemsProperty { get => _hasItemsProperty = _hasItemsPropertyHelper?.Value ?? _hasItemsProperty; }
/// <inheritdoc cref="DisplayNameProperty"/>
private string _displayNameProperty;
private string _displayNameProperty = string.Empty;

/// <inheritdoc cref="_displayNamePropertyHelper"/>
private ReactiveUI.ObservableAsPropertyHelper<string>? _displayNamePropertyHelper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,79 @@ public TestVM()
// Act: Initialize the helper and run the generator. Assert: Verify the generated code.
return TestHelper.TestPass(sourceCode);
}

/// <summary>Tests that an empty string initial value initialises the generated backing field.</summary>
/// <returns>
/// A task to monitor the async.
/// </returns>
[Test]
public Task FromPartialPropertyWithEmptyStringInitialValue()
{
// Arrange: Setup the source code that matches the generator input expectations.
const string sourceCode = """
using System;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System.Reactive.Linq;
using System.Reactive.Subjects;

namespace TestNs;

public partial class TestVM : ReactiveObject
{
private readonly Subject<string> _testSubject = new();

public TestVM()
{
_pLCActiveHelper = _testSubject.ToProperty(this, nameof(PLCActive));
}

[ObservableAsProperty(InitialValue = "")]
public partial string PLCActive { get; }
}
""";

// Act: Initialize the helper and run the generator. Assert: Verify the generated code.
return TestHelper.TestPass(sourceCode);
}

/// <summary>Tests that a non nullable string without an initial value defaults to an empty string.</summary>
/// <returns>
/// A task to monitor the async.
/// </returns>
[Test]
public Task FromPartialPropertyWithoutInitialValue()
{
// Arrange: Setup the source code that matches the generator input expectations.
const string sourceCode = """
using System;
using ReactiveUI;
using ReactiveUI.SourceGenerators;
using System.Reactive.Linq;
using System.Reactive.Subjects;

namespace TestNs;

public partial class TestVM : ReactiveObject
{
private readonly Subject<string> _testSubject = new();
private readonly Subject<string?> _testNullableSubject = new();

public TestVM()
{
_pLCActiveHelper = _testSubject.ToProperty(this, nameof(PLCActive));
_pLCNameHelper = _testNullableSubject.ToProperty(this, nameof(PLCName));
}

[ObservableAsProperty]
public partial string PLCActive { get; }

[ObservableAsProperty]
public partial string? PLCName { get; }
}
""";

// Act: Initialize the helper and run the generator. Assert: Verify the generated code.
return TestHelper.TestPass(sourceCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ private ObservableAsPropertyHelper<int> CreateObservableAsPropertyFromPropertyHe
private ObservableAsPropertyHelper<string?> CreatePlcActiveHelper() =>
this.WhenAnyValue(x => x.PartialRequiredPropertyTest).ToProperty(this, nameof(PLCActive));

/// <summary>Creates the helper that projects the PLC status message.</summary>
/// <returns>The initialized observable property helper.</returns>
private ObservableAsPropertyHelper<string> CreatePlcStatusHelper() =>
this.WhenAnyValue(x => x.PLCActive).Select(static x => x ?? string.Empty).ToProperty(this, nameof(PLCStatus));

/// <summary>Creates the helper that projects the PLC port.</summary>
/// <returns>The initialized observable property helper.</returns>
private ObservableAsPropertyHelper<int> CreatePlcPortHelper() =>
Expand Down
7 changes: 7 additions & 0 deletions src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ public TestViewModel()
_observableAsPropertyTest3PropertyHelper = CreateObservableAsPropertyTest3PropertyHelper();
_observableAsPropertyFromPropertyHelper = CreateObservableAsPropertyFromPropertyHelper();
_pLCActiveHelper = CreatePlcActiveHelper();
_pLCStatusHelper = CreatePlcStatusHelper();
_pLCPortHelper = CreatePlcPortHelper();
_instanceOfPLCHelper = CreatePlcInstanceHelper();
_referenceTypeObservableProperty = default!;
Expand Down Expand Up @@ -254,6 +255,12 @@ public TestViewModel()
[ObservableAsProperty(InitialValue = "Not Connected")]
public partial string? PLCActive { get; }

/// <summary>
/// Gets the current PLC status message, initialized with an empty string.
/// </summary>
[ObservableAsProperty(InitialValue = "")]
public partial string PLCStatus { get; }

/// <summary>
/// Gets the TCP port number used to communicate with the PLC.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,16 +401,27 @@ private static string GetPropertySyntax(ObservableMethodInfo propertyInfo, strin
/// <returns>The field initializer suffix.</returns>
private static string GetInitialValueSyntax(string propertyType, string? initialValue)
{
if (initialValue is null || string.IsNullOrWhiteSpace(initialValue))
var isNullableStringProperty = propertyType is "string?"
|| propertyType.EndsWith("##string?", System.StringComparison.Ordinal);
var isStringProperty = isNullableStringProperty
|| propertyType is "string"
|| propertyType.EndsWith("##string", System.StringComparison.Ordinal);

if (isStringProperty)
{
return ";";
// A non nullable string field is initialised to an empty string when no initial value is
// supplied, so the generated field never holds null. Empty and whitespace values are valid
// string literals and are emitted as written.
if (initialValue is not null)
{
return $" = {Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatLiteral(initialValue, quote: true)};";
}

return isNullableStringProperty ? ";" : " = string.Empty;";
}

var isStringProperty = propertyType is "string" or "string?"
|| propertyType.EndsWith("##string", System.StringComparison.Ordinal)
|| propertyType.EndsWith("##string?", System.StringComparison.Ordinal);
return isStringProperty
? $" = {Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatLiteral(initialValue, quote: true)};"
return string.IsNullOrWhiteSpace(initialValue)
? ";"
: $" = {initialValue};";
}

Expand Down
Loading