diff --git a/src/ReactiveUI.Blazor/ReactiveInjectableComponentBase.cs b/src/ReactiveUI.Blazor/ReactiveInjectableComponentBase.cs new file mode 100644 index 0000000000..a1d1eb96a3 --- /dev/null +++ b/src/ReactiveUI.Blazor/ReactiveInjectableComponentBase.cs @@ -0,0 +1,147 @@ +// Copyright (c) 2019 .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; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +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 ReactiveInjectableComponentBase : ComponentBase, IViewFor, INotifyPropertyChanged, ICanActivate, IDisposable + where T : class, INotifyPropertyChanged + { + private readonly Subject _initSubject = new Subject(); + [SuppressMessage("Design", "CA2213: Dispose object", Justification = "Used for deactivation.")] + private readonly Subject _deactivateSubject = new Subject(); + private readonly CompositeDisposable _compositeDisposable = new CompositeDisposable(); + + private T _viewModel; + + private bool _disposedValue; // To detect redundant calls + + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + [Inject] + 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(); + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in Dispose(bool disposing) below. + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + protected override void OnInitialized() + { + _initSubject.OnNext(Unit.Default); + base.OnInitialized(); + } + + /// + 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(x => x.ViewModel) + .Where(x => x != null) + .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 property. + protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + /// + /// Cleans up the managed resources of the object. + /// + /// If it is getting called by the Dispose() method rather than a finalizer. + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _initSubject?.Dispose(); + _compositeDisposable?.Dispose(); + _deactivateSubject.OnNext(Unit.Default); + } + + _disposedValue = true; + } + } + } +}