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};"; }