From b314c6a9005f488bc8ae51853099d213cdd59705 Mon Sep 17 00:00:00 2001 From: Bekzod Allaev Date: Sat, 22 Nov 2025 13:10:46 +0100 Subject: [PATCH 1/3] Adding ReactiveOwningComponentBase --- .../ReactiveOwningComponentBase.cs | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs diff --git a/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs b/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs new file mode 100644 index 0000000000..0ba20f8539 --- /dev/null +++ b/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs @@ -0,0 +1,137 @@ +// Copyright (c) 2025 .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. +using System.Runtime.CompilerServices; + +using Microsoft.AspNetCore.Components; + +namespace ReactiveUI.Blazor; + +/// +/// A base component for handling property changes and updating the blazer view appropriately. +/// +/// The type of view model. Must support INotifyPropertyChanged. +public class ReactiveOwningComponentBase : OwningComponentBase, IViewFor, INotifyPropertyChanged, ICanActivate + where T : class, INotifyPropertyChanged +{ + private readonly Subject _initSubject = new(); + [SuppressMessage("Design", "CA2213: Dispose object", Justification = "Used for deactivation.")] + private readonly Subject _deactivateSubject = new(); + private readonly CompositeDisposable _compositeDisposable = []; + + private bool _disposedValue; + private T? _viewModel; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + + /// + /// Gets or sets the view model associated with this component. + /// + [Parameter] + public T? ViewModel + { + get => _viewModel; + set + { + if (EqualityComparer.Default.Equals(_viewModel, value)) + { + return; + } + + _viewModel = value; + OnPropertyChanged(); + } + } + + /// + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (T?)value; + } + + /// + public IObservable Activated => _initSubject.AsObservable(); + + /// + public IObservable Deactivated => _deactivateSubject.AsObservable(); + + /// + protected override void OnInitialized() + { + if (ViewModel is IActivatableViewModel avm) + { + Activated.Subscribe(_ => avm.Activator.Activate()).DisposeWith(_compositeDisposable); + Deactivated.Subscribe(_ => avm.Activator.Deactivate()); + } + + _initSubject.OnNext(Unit.Default); + base.OnInitialized(); + } + + /// +#if NET6_0_OR_GREATER + [RequiresDynamicCode("OnAfterRender uses methods that require dynamic code generation")] + [RequiresUnreferencedCode("OnAfterRender uses methods that may require unreferenced code")] + [SuppressMessage("AOT", "IL3051:'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "ComponentBase is an external reference")] + [SuppressMessage("Trimming", "IL2046:'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides.", Justification = "ComponentBase is an external reference")] +#endif + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + // The following subscriptions are here because if they are done in OnInitialized, they conflict with certain JavaScript frameworks. + var viewModelChanged = + this.WhenAnyValue, T?>(nameof(ViewModel)) + .WhereNotNull() + .Publish() + .RefCount(2); + + viewModelChanged + .Subscribe(_ => InvokeAsync(StateHasChanged)) + .DisposeWith(_compositeDisposable); + + viewModelChanged + .Select(x => + Observable + .FromEvent( + eventHandler => + { + void Handler(object? sender, PropertyChangedEventArgs e) => eventHandler(Unit.Default); + return Handler; + }, + eh => x.PropertyChanged += eh, + eh => x.PropertyChanged -= eh)) + .Switch() + .Subscribe(_ => InvokeAsync(StateHasChanged)) + .DisposeWith(_compositeDisposable); + } + + base.OnAfterRender(firstRender); + } + + /// + /// Raises the event. + /// + /// The name of the changed property. + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + + /// + protected override void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _initSubject.Dispose(); + _compositeDisposable.Dispose(); + _deactivateSubject.OnNext(Unit.Default); + } + + _disposedValue = true; + } + } +} From 89ab71062a1140625d4e963cc45d2d7297978d7a Mon Sep 17 00:00:00 2001 From: Bekzod Allaev Date: Sat, 22 Nov 2025 22:35:57 +0100 Subject: [PATCH 2/3] - Add comments for _disposedValue - Use PropertyChangedEventHandler instead of PropertyChangedEventHandler? - Edit summary of OnPropertyChanged --- src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs b/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs index 0ba20f8539..53b1a1f2ee 100644 --- a/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs +++ b/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs @@ -20,7 +20,7 @@ public class ReactiveOwningComponentBase : OwningComponentBase, IViewFor _deactivateSubject = new(); private readonly CompositeDisposable _compositeDisposable = []; - private bool _disposedValue; + private bool _disposedValue; // To detect redundant calls private T? _viewModel; /// @@ -96,7 +96,7 @@ protected override void OnAfterRender(bool firstRender) viewModelChanged .Select(x => Observable - .FromEvent( + .FromEvent( eventHandler => { void Handler(object? sender, PropertyChangedEventArgs e) => eventHandler(Unit.Default); @@ -113,10 +113,10 @@ protected override void OnAfterRender(bool firstRender) } /// - /// Raises the event. + /// Invokes the property changed event. /// /// The name of the changed property. - protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) => + protected virtual void OnPropertyChanged([CallerMemberName]string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); /// From d93149718b1c2cfafa298f4a472fa53755342b6a Mon Sep 17 00:00:00 2001 From: Bekzod Allaev Date: Sat, 22 Nov 2025 22:50:03 +0100 Subject: [PATCH 3/3] Remove _disposedValue --- .../ReactiveOwningComponentBase.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs b/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs index 53b1a1f2ee..9789b89a65 100644 --- a/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs +++ b/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs @@ -20,7 +20,6 @@ public class ReactiveOwningComponentBase : OwningComponentBase, IViewFor _deactivateSubject = new(); private readonly CompositeDisposable _compositeDisposable = []; - private bool _disposedValue; // To detect redundant calls private T? _viewModel; /// @@ -116,22 +115,17 @@ protected override void OnAfterRender(bool firstRender) /// Invokes the property changed event. /// /// The name of the changed property. - protected virtual void OnPropertyChanged([CallerMemberName]string? propertyName = null) => + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); /// protected override void Dispose(bool disposing) { - if (!_disposedValue) + if (disposing) { - if (disposing) - { - _initSubject.Dispose(); - _compositeDisposable.Dispose(); - _deactivateSubject.OnNext(Unit.Default); - } - - _disposedValue = true; + _initSubject.Dispose(); + _compositeDisposable.Dispose(); + _deactivateSubject.OnNext(Unit.Default); } } }