diff --git a/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs b/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs new file mode 100644 index 0000000000..9789b89a65 --- /dev/null +++ b/src/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs @@ -0,0 +1,131 @@ +// 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 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); + } + + /// + /// Invokes the property changed 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 (disposing) + { + _initSubject.Dispose(); + _compositeDisposable.Dispose(); + _deactivateSubject.OnNext(Unit.Default); + } + } +}