Skip to content

Commit f9b1b61

Browse files
committed
Add IReactiveObject source generator and tests
Introduces the [IReactiveObject] attribute and a new source generator to implement IReactiveObject for classes that cannot inherit from ReactiveObject. Updates documentation, adds generator logic, supporting models, and unit tests to verify generated code. Also updates helper and project files to support the new generator.
1 parent 8aedecb commit f9b1b61

14 files changed

+366
-13
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ ReactiveUI Source Generators automatically generate ReactiveUI objects to stream
5151
- `[ViewModelControlHost("YourNameSpace.CustomControl")]`
5252
- `[BindableDerivedList]` Generates a derived list from a ReadOnlyObservableCollection backing field
5353
- `[ReactiveCollection]` Generates property changed notifications on add, remove, new actions on a ObservableCollection backing field
54+
- `[IReactiveObject]` Generates IReactiveObject implementation for classes not able to inherit from ReactiveObject
5455

5556
#### IViewFor Registration generator
5657

@@ -684,6 +685,19 @@ public partial class MyReactiveClass : ReactiveObject
684685
}
685686
```
686687

688+
### ReactiveObject implementation for classes not able to inherit from ReactiveObject
689+
```csharp
690+
using ReactiveUI;
691+
using ReactiveUI.SourceGenerators;
692+
693+
[IReactiveObject]
694+
public partial class MyReactiveClass
695+
{
696+
[Reactive]
697+
private string _myProperty;
698+
}
699+
```
700+
687701
### TODO:
688702
- Add ObservableAsProperty to generate from a IObservable method with parameters.
689703

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//HintName: ReactiveUI.SourceGenerators.IReactiveObjectAttribute.g.cs
2+
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
3+
// Licensed to the .NET Foundation under one or more agreements.
4+
// The .NET Foundation licenses this file to you under the MIT license.
5+
// See the LICENSE file in the project root for full license information.
6+
7+
// <auto-generated/>
8+
#pragma warning disable
9+
#nullable enable
10+
namespace ReactiveUI.SourceGenerators;
11+
12+
/// <summary>
13+
/// IReactiveObject Attribute.
14+
/// </summary>
15+
/// <seealso cref="System.Attribute" />
16+
[global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
17+
internal sealed class IReactiveObjectAttribute : global::System.Attribute;
18+
#nullable restore
19+
#pragma warning restore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//HintName: TestNs.TestVM.IReactiveObject.g.cs
2+
// <auto-generated/>
3+
#pragma warning disable
4+
#nullable enable
5+
using System;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.ComponentModel;
8+
using ReactiveUI;
9+
10+
namespace TestNs
11+
{
12+
/// <summary>
13+
/// Partial class for the TestVM which contains ReactiveUI IReactiveObject initialization.
14+
/// </summary>
15+
public partial class TestVM : IReactiveObject
16+
{
17+
private bool _propertyChangingEventsSubscribed;
18+
private bool _propertyChangedEventsSubscribed;
19+
20+
/// <inheritdoc/>
21+
public event PropertyChangingEventHandler? PropertyChanging
22+
{
23+
add
24+
{
25+
if (!_propertyChangingEventsSubscribed)
26+
{
27+
this.SubscribePropertyChangingEvents();
28+
_propertyChangingEventsSubscribed = true;
29+
}
30+
31+
PropertyChangingHandler += value;
32+
}
33+
remove => PropertyChangingHandler -= value;
34+
}
35+
36+
/// <inheritdoc/>
37+
public event PropertyChangedEventHandler? PropertyChanged
38+
{
39+
add
40+
{
41+
if (!_propertyChangedEventsSubscribed)
42+
{
43+
this.SubscribePropertyChangedEvents();
44+
_propertyChangedEventsSubscribed = true;
45+
}
46+
47+
PropertyChangedHandler += value;
48+
}
49+
remove => PropertyChangedHandler -= value;
50+
}
51+
52+
[SuppressMessage("Roslynator", "RCS1159:Use EventHandler<T>", Justification = "Long term design.")]
53+
private event PropertyChangingEventHandler? PropertyChangingHandler;
54+
55+
[SuppressMessage("Roslynator", "RCS1159:Use EventHandler<T>", Justification = "Long term design.")]
56+
private event PropertyChangedEventHandler? PropertyChangedHandler;
57+
58+
/// <inheritdoc/>
59+
void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) =>
60+
PropertyChangingHandler?.Invoke(this, args);
61+
62+
/// <inheritdoc/>
63+
void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) =>
64+
PropertyChangedHandler?.Invoke(this, args);
65+
}
66+
}
67+
#nullable restore
68+
#pragma warning restore

src/ReactiveUI.SourceGenerator.Tests/ReactiveUI.SourceGenerators.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<Folder Include="OAPH\" />
4141
<Folder Include="REACTIVECMD\" />
4242
<Folder Include="REACTIVE\" />
43+
<Folder Include="REACTIVEOBJ\" />
4344
<Folder Include="REACTIVECOLL\" />
4445
</ItemGroup>
4546

src/ReactiveUI.SourceGenerator.Tests/TestHelper.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public string VerifiedFilePath()
8787
nameof(ViewModelControlHostGenerator) => "CONTROLHOST",
8888
nameof(BindableDerivedListGenerator) => "DERIVEDLIST",
8989
nameof(ReactiveCollectionGenerator) => "REACTIVECOLL",
90+
nameof(ReactiveObjectGenerator) => "REACTIVEOBJ",
9091
_ => name,
9192
};
9293
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright (c) 2025 ReactiveUI and contributors. All rights reserved.
2+
// Licensed to the ReactiveUI and contributors under one or more agreements.
3+
// The ReactiveUI and contributors licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using ReactiveUI.SourceGenerators;
7+
8+
namespace ReactiveUI.SourceGenerator.Tests;
9+
10+
/// <summary>
11+
/// Unit tests for the Reactive generator.
12+
/// </summary>
13+
[TestFixture]
14+
public class ReactiveObjectGeneratorTests : TestBase<ReactiveObjectGenerator>
15+
{
16+
/// <summary>
17+
/// Froms the reactive object.
18+
/// </summary>
19+
/// <returns>A task to monitor the async.</returns>
20+
[Test]
21+
public Task FromReactiveObject()
22+
{
23+
// Arrange: Setup the source code that matches the generator input expectations.
24+
const string sourceCode = """
25+
using System;
26+
using ReactiveUI.SourceGenerators;
27+
using System.Reactive.Linq;
28+
namespace TestNs;
29+
30+
[IReactiveObject]
31+
public partial class TestVM
32+
{
33+
[Reactive]
34+
private int _test1 = 10;
35+
}
36+
""";
37+
38+
// Act: Initialize the helper and run the generator. Assert: Verify the generated code.
39+
return TestHelper.TestPass(sourceCode);
40+
}
41+
}

src/ReactiveUI.SourceGenerators.Execute/Person.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
// See the LICENSE file in the project root for full license information.
55

66
using System.Diagnostics.CodeAnalysis;
7-
using ReactiveUI;
87
using ReactiveUI.SourceGenerators;
98

109
namespace SGReactiveUI.SourceGenerators.Test;
@@ -14,7 +13,8 @@ namespace SGReactiveUI.SourceGenerators.Test;
1413
/// </summary>
1514
/// <seealso cref="ReactiveUI.ReactiveObject" />
1615
[ExcludeFromCodeCoverage]
17-
public partial class Person : ReactiveObject
16+
[IReactiveObject]
17+
public partial class Person
1818
{
1919
/// <summary>
2020
/// Gets or sets a value indicating whether this <see cref="Person"/> is deleted.
@@ -23,5 +23,5 @@ public partial class Person : ReactiveObject
2323
/// <c>true</c> if deleted; otherwise, <c>false</c>.
2424
/// </value>
2525
[Reactive]
26-
public bool Deleted { get; set; }
26+
public partial bool Deleted { get; set; }
2727
}

src/ReactiveUI.SourceGenerators.Roslyn/AttributeDefinitions.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ internal enum SplatRegistrationType
7777
#pragma warning restore
7878
""";
7979

80+
public const string ReactiveObjectAttributeType = "ReactiveUI.SourceGenerators.IReactiveObjectAttribute";
81+
8082
public static string ReactiveObjectAttribute => $$"""
8183
// Copyright (c) {{DateTime.Now.Year}} .NET Foundation and Contributors. All rights reserved.
8284
// Licensed to the .NET Foundation under one or more agreements.
@@ -89,12 +91,12 @@ internal enum SplatRegistrationType
8991
namespace ReactiveUI.SourceGenerators;
9092
9193
/// <summary>
92-
/// ReactiveObjectAttribute.
94+
/// IReactiveObject Attribute.
9395
/// </summary>
9496
/// <seealso cref="System.Attribute" />
9597
[global::System.CodeDom.Compiler.GeneratedCode("ReactiveUI.SourceGenerators.ReactiveObjectGenerator", "{{ReactiveGenerator.GeneratorVersion}}")]
9698
[global::System.AttributeUsage(global::System.AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
97-
internal sealed class ReactiveObjectAttribute : global::System.Attribute;
99+
internal sealed class IReactiveObjectAttribute : global::System.Attribute;
98100
#nullable restore
99101
#pragma warning restore
100102
""";

src/ReactiveUI.SourceGenerators.Roslyn/BindableDerivedList/BindableDerivedListGenerator.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
// The ReactiveUI and contributors licenses this file to you under the MIT license.
44
// See the LICENSE file in the project root for full license information.
55

6-
using System;
7-
using System.Collections.Generic;
86
using System.Collections.Immutable;
97
using System.Linq;
108
using System.Text;

src/ReactiveUI.SourceGenerators.Roslyn/Core/Extensions/FieldSyntaxExtensions.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System.Globalization;
77
using Microsoft.CodeAnalysis;
8+
using ReactiveUI.SourceGenerators.Helpers;
89

910
namespace ReactiveUI.SourceGenerators.Extensions;
1011

@@ -124,7 +125,7 @@ internal static bool IsTargetTypeValid(this IFieldSymbol fieldSymbol)
124125
{
125126
var isObservableObject = fieldSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject");
126127
var isIObservableObject = fieldSymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject");
127-
var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute");
128+
var hasObservableObjectAttribute = fieldSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveObjectAttributeType);
128129

129130
return isIObservableObject || isObservableObject || hasObservableObjectAttribute;
130131
}
@@ -138,7 +139,7 @@ internal static bool IsTargetTypeValid(this IPropertySymbol propertySymbol)
138139
{
139140
var isObservableObject = propertySymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject");
140141
var isIObservableObject = propertySymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject");
141-
var hasObservableObjectAttribute = propertySymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute");
142+
var hasObservableObjectAttribute = propertySymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveObjectAttributeType);
142143

143144
return isIObservableObject || isObservableObject || hasObservableObjectAttribute;
144145
}
@@ -152,7 +153,7 @@ internal static bool IsTargetTypeValid(this IMethodSymbol methodSymbol)
152153
{
153154
var isObservableObject = methodSymbol.ContainingType.InheritsFromFullyQualifiedMetadataName("ReactiveUI.ReactiveObject");
154155
var isIObservableObject = methodSymbol.ContainingType.ImplementsFullyQualifiedMetadataName("ReactiveUI.IReactiveObject");
155-
var hasObservableObjectAttribute = methodSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName("ReactiveUI.SourceGenerators.ReactiveObjectAttribute");
156+
var hasObservableObjectAttribute = methodSymbol.ContainingType.HasOrInheritsAttributeWithFullyQualifiedMetadataName(AttributeDefinitions.ReactiveObjectAttributeType);
156157

157158
return isIObservableObject || isObservableObject || hasObservableObjectAttribute;
158159
}

0 commit comments

Comments
 (0)