Skip to content

Commit f2f9022

Browse files
authored
Adding ReactiveOwningComponentBase (#4205)
<!-- Please be sure to read the [Contribute](https://github.com/reactiveui/reactiveui#contribute) section of the README --> **What kind of change does this PR introduce?** This PR adds a `ReactiveOwningComponentBase<T>` to the ReactiveUI.Blazor project, as requested in #3001. **What is the current behavior?** There is currently no reactive base component that combines `OwningComponentBase<T>` with ReactiveUI’s `IViewFor<T>` / activation support in `ReactiveUI.Blazor`. **What is the new behavior?** Blazor components can derive from `ReactiveOwningComponentBase<T>` to: - use an owning DI scope via `OwningComponentBase<T>`, - expose a reactive `ViewModel` property, - participate in activation (`ICanActivate`) and automatically call `StateHasChanged` on `INotifyPropertyChanged` changes. **What might this PR break?** No breaking changes are expected. The PR only introduces a new base class and does not modify existing public APIs or behavior. **Please check if the PR fulfills these requirements** - [ ] Tests for the changes have been added (for bug fixes / features) - [ ] Docs have been added / updated (for bug fixes / features) **Other information**: Fixes #3001.
1 parent cae2b65 commit f2f9022

File tree

1 file changed

+131
-0
lines changed

1 file changed

+131
-0
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright (c) 2025 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
using System.Runtime.CompilerServices;
6+
7+
using Microsoft.AspNetCore.Components;
8+
9+
namespace ReactiveUI.Blazor;
10+
11+
/// <summary>
12+
/// A base component for handling property changes and updating the blazer view appropriately.
13+
/// </summary>
14+
/// <typeparam name="T">The type of view model. Must support INotifyPropertyChanged.</typeparam>
15+
public class ReactiveOwningComponentBase<T> : OwningComponentBase<T>, IViewFor<T>, INotifyPropertyChanged, ICanActivate
16+
where T : class, INotifyPropertyChanged
17+
{
18+
private readonly Subject<Unit> _initSubject = new();
19+
[SuppressMessage("Design", "CA2213: Dispose object", Justification = "Used for deactivation.")]
20+
private readonly Subject<Unit> _deactivateSubject = new();
21+
private readonly CompositeDisposable _compositeDisposable = [];
22+
23+
private T? _viewModel;
24+
25+
/// <inheritdoc />
26+
public event PropertyChangedEventHandler? PropertyChanged;
27+
28+
/// <summary>
29+
/// Gets or sets the view model associated with this component.
30+
/// </summary>
31+
[Parameter]
32+
public T? ViewModel
33+
{
34+
get => _viewModel;
35+
set
36+
{
37+
if (EqualityComparer<T?>.Default.Equals(_viewModel, value))
38+
{
39+
return;
40+
}
41+
42+
_viewModel = value;
43+
OnPropertyChanged();
44+
}
45+
}
46+
47+
/// <inheritdoc />
48+
object? IViewFor.ViewModel
49+
{
50+
get => ViewModel;
51+
set => ViewModel = (T?)value;
52+
}
53+
54+
/// <inheritdoc />
55+
public IObservable<Unit> Activated => _initSubject.AsObservable();
56+
57+
/// <inheritdoc />
58+
public IObservable<Unit> Deactivated => _deactivateSubject.AsObservable();
59+
60+
/// <inheritdoc />
61+
protected override void OnInitialized()
62+
{
63+
if (ViewModel is IActivatableViewModel avm)
64+
{
65+
Activated.Subscribe(_ => avm.Activator.Activate()).DisposeWith(_compositeDisposable);
66+
Deactivated.Subscribe(_ => avm.Activator.Deactivate());
67+
}
68+
69+
_initSubject.OnNext(Unit.Default);
70+
base.OnInitialized();
71+
}
72+
73+
/// <inheritdoc/>
74+
#if NET6_0_OR_GREATER
75+
[RequiresDynamicCode("OnAfterRender uses methods that require dynamic code generation")]
76+
[RequiresUnreferencedCode("OnAfterRender uses methods that may require unreferenced code")]
77+
[SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "ComponentBase is an external reference")]
78+
[SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "ComponentBase is an external reference")]
79+
#endif
80+
protected override void OnAfterRender(bool firstRender)
81+
{
82+
if (firstRender)
83+
{
84+
// The following subscriptions are here because if they are done in OnInitialized, they conflict with certain JavaScript frameworks.
85+
var viewModelChanged =
86+
this.WhenAnyValue<ReactiveOwningComponentBase<T>, T?>(nameof(ViewModel))
87+
.WhereNotNull()
88+
.Publish()
89+
.RefCount(2);
90+
91+
viewModelChanged
92+
.Subscribe(_ => InvokeAsync(StateHasChanged))
93+
.DisposeWith(_compositeDisposable);
94+
95+
viewModelChanged
96+
.Select(x =>
97+
Observable
98+
.FromEvent<PropertyChangedEventHandler, Unit>(
99+
eventHandler =>
100+
{
101+
void Handler(object? sender, PropertyChangedEventArgs e) => eventHandler(Unit.Default);
102+
return Handler;
103+
},
104+
eh => x.PropertyChanged += eh,
105+
eh => x.PropertyChanged -= eh))
106+
.Switch()
107+
.Subscribe(_ => InvokeAsync(StateHasChanged))
108+
.DisposeWith(_compositeDisposable);
109+
}
110+
111+
base.OnAfterRender(firstRender);
112+
}
113+
114+
/// <summary>
115+
/// Invokes the property changed event.
116+
/// </summary>
117+
/// <param name="propertyName">The name of the changed property.</param>
118+
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
119+
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
120+
121+
/// <inheritdoc />
122+
protected override void Dispose(bool disposing)
123+
{
124+
if (disposing)
125+
{
126+
_initSubject.Dispose();
127+
_compositeDisposable.Dispose();
128+
_deactivateSubject.OnNext(Unit.Default);
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)