diff --git a/README.md b/README.md
index e58bd48..d98ed34 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithEmptyStringInitialValue#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithEmptyStringInitialValue#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs
new file mode 100644
index 0000000..2ba75ce
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithEmptyStringInitialValue#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs
@@ -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.
+
+//
+#pragma warning disable
+#nullable enable
+namespace ReactiveUI.SourceGenerators;
+
+///
+/// ObservableAsPropertyAttribute.
+///
+///
+[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
+{
+ ///
+ /// Gets the name of the property.
+ ///
+ ///
+ /// 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;
+
+ ///
+ /// Gets the AccessModifier of the OAPH property.
+ ///
+ ///
+ /// The AccessModifier of the OAPH property, protected if true.
+ ///
+ public bool UseProtected { get; init; } = false;
+
+ ///
+ /// Gets the inheritance modifier of the generated property.
+ ///
+ public InheritanceModifier Inheritance { get; init; }
+
+ ///
+ /// 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<string>()".
+ /// String values are automatically quoted.
+ ///
+ public string? InitialValue { get; init; }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithEmptyStringInitialValue#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithEmptyStringInitialValue#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs
new file mode 100644
index 0000000..06b00e6
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithEmptyStringInitialValue#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs
@@ -0,0 +1,31 @@
+//HintName: TestNs.TestVM.ObservableAsPropertyFromObservable.g.cs
+//
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ private string _pLCActive = "";
+
+ ///
+ private readonly ReactiveUI.ObservableAsPropertyHelper _pLCActiveHelper;
+
+ ///
+ [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
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithoutInitialValue#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithoutInitialValue#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs
new file mode 100644
index 0000000..2ba75ce
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithoutInitialValue#ReactiveUI.SourceGenerators.ObservableAsPropertyAttribute.g.verified.cs
@@ -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.
+
+//
+#pragma warning disable
+#nullable enable
+namespace ReactiveUI.SourceGenerators;
+
+///
+/// ObservableAsPropertyAttribute.
+///
+///
+[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
+{
+ ///
+ /// Gets the name of the property.
+ ///
+ ///
+ /// 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;
+
+ ///
+ /// Gets the AccessModifier of the OAPH property.
+ ///
+ ///
+ /// The AccessModifier of the OAPH property, protected if true.
+ ///
+ public bool UseProtected { get; init; } = false;
+
+ ///
+ /// Gets the inheritance modifier of the generated property.
+ ///
+ public InheritanceModifier Inheritance { get; init; }
+
+ ///
+ /// 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<string>()".
+ /// String values are automatically quoted.
+ ///
+ public string? InitialValue { get; init; }
+}
+#nullable restore
+#pragma warning restore
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithoutInitialValue#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithoutInitialValue#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs
new file mode 100644
index 0000000..a640e04
--- /dev/null
+++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OAPFromObservableGeneratorTests.FromPartialPropertyWithoutInitialValue#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs
@@ -0,0 +1,40 @@
+//HintName: TestNs.TestVM.ObservableAsPropertyFromObservable.g.cs
+//
+using ReactiveUI;
+
+#pragma warning disable
+#nullable enable
+
+namespace TestNs
+{
+
+ public partial class TestVM
+ {
+ ///
+ private string _pLCActive = string.Empty;
+
+ ///
+ private readonly ReactiveUI.ObservableAsPropertyHelper _pLCActiveHelper;
+
+ ///
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ public partial string PLCActive { get => _pLCActive = (_pLCActiveHelper == null ? _pLCActive : _pLCActiveHelper.Value); }
+///
+ private string? _pLCName;
+
+ ///
+ private readonly ReactiveUI.ObservableAsPropertyHelper _pLCNameHelper;
+
+ ///
+ [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
\ No newline at end of file
diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromFieldInDeeplyNestedClass#TestNs.Level1+Level2.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromFieldInDeeplyNestedClass#TestNs.Level1+Level2.ObservableAsPropertyFromObservable.g.verified.cs
index 2839af9..a31f90b 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromFieldInDeeplyNestedClass#TestNs.Level1+Level2.ObservableAsPropertyFromObservable.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromFieldInDeeplyNestedClass#TestNs.Level1+Level2.ObservableAsPropertyFromObservable.g.verified.cs
@@ -13,7 +13,7 @@ public partial class Level1
public partial class Level2
{
///
- private string _level2ObservableProperty;
+ private string _level2ObservableProperty = string.Empty;
///
private ReactiveUI.ObservableAsPropertyHelper? _level2ObservablePropertyHelper;
diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromFieldWithAttributes#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromFieldWithAttributes#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs
index 74641e2..69c292f 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromFieldWithAttributes#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromFieldWithAttributes#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs
@@ -11,7 +11,7 @@ namespace TestNs
public partial class TestVM
{
///
- private string _internalStateProperty;
+ private string _internalStateProperty = string.Empty;
///
private ReactiveUI.ObservableAsPropertyHelper? _internalStatePropertyHelper;
diff --git a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromMultipleObservableAsProperties#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromMultipleObservableAsProperties#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs
index 6b9751b..56ee103 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromMultipleObservableAsProperties#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/OAPH/OapExtTests.FromMultipleObservableAsProperties#TestNs.TestVM.ObservableAsPropertyFromObservable.g.verified.cs
@@ -20,7 +20,7 @@ public partial class TestVM
[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
public bool HasItemsProperty { get => _hasItemsProperty = _hasItemsPropertyHelper?.Value ?? _hasItemsProperty; }
///
- private string _displayNameProperty;
+ private string _displayNameProperty = string.Empty;
///
private ReactiveUI.ObservableAsPropertyHelper? _displayNamePropertyHelper;
diff --git a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs
index 81bb71a..2f0f6e8 100644
--- a/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs
+++ b/src/ReactiveUI.SourceGenerator.Tests/UnitTests/OAPFromObservableGeneratorTests.cs
@@ -314,4 +314,79 @@ public TestVM()
// Act: Initialize the helper and run the generator. Assert: Verify the generated code.
return TestHelper.TestPass(sourceCode);
}
+
+ /// Tests that an empty string initial value initialises the generated backing field.
+ ///
+ /// A task to monitor the async.
+ ///
+ [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 _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);
+ }
+
+ /// Tests that a non nullable string without an initial value defaults to an empty string.
+ ///
+ /// A task to monitor the async.
+ ///
+ [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 _testSubject = new();
+ private readonly Subject _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);
+ }
}
diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.Initialization.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.Initialization.cs
index 280eabb..d049a2c 100644
--- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.Initialization.cs
+++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.Initialization.cs
@@ -44,6 +44,11 @@ private ObservableAsPropertyHelper CreateObservableAsPropertyFromPropertyHe
private ObservableAsPropertyHelper CreatePlcActiveHelper() =>
this.WhenAnyValue(x => x.PartialRequiredPropertyTest).ToProperty(this, nameof(PLCActive));
+ /// Creates the helper that projects the PLC status message.
+ /// The initialized observable property helper.
+ private ObservableAsPropertyHelper CreatePlcStatusHelper() =>
+ this.WhenAnyValue(x => x.PLCActive).Select(static x => x ?? string.Empty).ToProperty(this, nameof(PLCStatus));
+
/// Creates the helper that projects the PLC port.
/// The initialized observable property helper.
private ObservableAsPropertyHelper CreatePlcPortHelper() =>
diff --git a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs
index dd94cbe..a16c399 100644
--- a/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs
+++ b/src/ReactiveUI.SourceGenerators.Execute/TestViewModel.cs
@@ -150,6 +150,7 @@ public TestViewModel()
_observableAsPropertyTest3PropertyHelper = CreateObservableAsPropertyTest3PropertyHelper();
_observableAsPropertyFromPropertyHelper = CreateObservableAsPropertyFromPropertyHelper();
_pLCActiveHelper = CreatePlcActiveHelper();
+ _pLCStatusHelper = CreatePlcStatusHelper();
_pLCPortHelper = CreatePlcPortHelper();
_instanceOfPLCHelper = CreatePlcInstanceHelper();
_referenceTypeObservableProperty = default!;
@@ -254,6 +255,12 @@ public TestViewModel()
[ObservableAsProperty(InitialValue = "Not Connected")]
public partial string? PLCActive { get; }
+ ///
+ /// Gets the current PLC status message, initialized with an empty string.
+ ///
+ [ObservableAsProperty(InitialValue = "")]
+ public partial string PLCStatus { get; }
+
///
/// Gets the TCP port number used to communicate with the PLC.
///
diff --git a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs
index 49b1f06..367abe8 100644
--- a/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs
+++ b/src/ReactiveUI.SourceGenerators.Roslyn/ObservableAsProperty/ObservableAsPropertyGenerator{FromObservable}.Execute.cs
@@ -401,16 +401,27 @@ private static string GetPropertySyntax(ObservableMethodInfo propertyInfo, strin
/// The field initializer suffix.
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};";
}