diff --git a/README.md b/README.md index 9dd066f6..53326e7e 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ Binding code: It's very simple. -ReactiveProperty doesn't provide base class by ViewModel, which means that ReactiveProperty can be used together with another MVVM library like Prism, MVVMLight, etc... +ReactiveProperty doesn't provide base class by ViewModel, which means that ReactiveProperty can be used together with another MVVM libraries such as Prism, Microsoft.Toolkit.Mvvm and etc. ## Documentation @@ -140,6 +140,7 @@ ReactiveProperty doesn't provide base class by ViewModel, which means that React |[ReactiveProperty](https://www.nuget.org/packages/ReactiveProperty/)|![](https://img.shields.io/nuget/v/ReactiveProperty.svg)![](https://img.shields.io/nuget/dt/ReactiveProperty.svg)|The package includes all core features, and the target platform is .NET Standard 2.0. It fits almost all situations.| |[ReactiveProperty.Core](https://www.nuget.org/packages/ReactiveProperty.Core/)|![](https://img.shields.io/nuget/v/ReactiveProperty.Core.svg)![](https://img.shields.io/nuget/dt/ReactiveProperty.Core.svg)|The package includes minimum classes such as `ReactivePropertySlim` and `ReadOnlyReactivePropertySlim`. And this doesn't have any dependency even System.Reactive. If you don't need Rx features, then it fits.| |[ReactiveProperty.WPF](https://www.nuget.org/packages/ReactiveProperty.WPF/)|![](https://img.shields.io/nuget/v/ReactiveProperty.WPF.svg)![](https://img.shields.io/nuget/dt/ReactiveProperty.WPF.svg)|The package includes EventToReactiveProperty and EventToReactiveCommand for WPF. This is for .NET Core 3.0 or later and .NET Framework 4.7.2 or later.| +|[ReactiveProperty.Blazor](https://www.nuget.org/packages/ReactiveProperty.Blazor/)|![](https://img.shields.io/nuget/v/ReactiveProperty.Blazor.svg)![](https://img.shields.io/nuget/dt/ReactiveProperty.Blazor.svg)|The package includes validation support for EditForm component of Blazor with ReactiveProperty validation feature. This is for .NET 6.0 or later. | |[ReactiveProperty.UWP](https://www.nuget.org/packages/ReactiveProperty.UWP/)|![](https://img.shields.io/nuget/v/ReactiveProperty.UWP.svg)![](https://img.shields.io/nuget/dt/ReactiveProperty.UWP.svg)|The package includes EventToReactiveProperty and EventToReactiveCommand for UWP.| |[ReactiveProperty.XamarinAndroid](https://www.nuget.org/packages/ReactiveProperty.XamarinAndroid/)|![](https://img.shields.io/nuget/v/ReactiveProperty.XamarinAndroid.svg)![](https://img.shields.io/nuget/dt/ReactiveProperty.XamarinAndroid.svg)|The package includes many extension methods to create IObservable from events for Xamarin.Android native.| |[ReactiveProperty.XamariniOS](https://www.nuget.org/packages/ReactiveProperty.XamariniOS/)|![](https://img.shields.io/nuget/v/ReactiveProperty.XamariniOS.svg)![](https://img.shields.io/nuget/dt/ReactiveProperty.XamariniOS.svg)|The package includes many extension methods to bind ReactiveProperty and ReactiveCommand to Xamarin.iOS native controls.| diff --git a/Samples/Blazor/BlazorSample.Shared/Pages/Index.razor b/Samples/Blazor/BlazorSample.Shared/Pages/Index.razor index 79d42307..cc8bc717 100644 --- a/Samples/Blazor/BlazorSample.Shared/Pages/Index.razor +++ b/Samples/Blazor/BlazorSample.Shared/Pages/Index.razor @@ -1,27 +1,43 @@ -@using Reactive.Bindings.Components +@using Reactive.Bindings +@using Reactive.Bindings.Extensions +@using Reactive.Bindings.Components +@using System.Reactive.Linq +@using System.Reactive.Disposables +@using System.Reactive.Concurrency @page "/" -@inject IndexViewModel _viewModel +@implements IDisposable +@inject ValidationViewModel _validationViewModel +@inject HelloWorldViewModel _helloWorldViewModel -

ReactivePropertiesValidator sample

+

ReactiveProperty samples

- +

Hello world

+ + +
+ @_helloWorldViewModel.Output.Value +
+ +

Validation sample

+ +
- - + +
- - + +
- Full name: @_viewModel.FullName.Value + Full name: @_validationViewModel.FullName.Value
@@ -35,6 +51,7 @@ } @code { + private readonly CompositeDisposable _disposable = new(); private string? _message; private void InvalidSubmit(EditContext context) @@ -46,4 +63,16 @@ { _message = "ValidSubmit was invoked."; } + + protected override void OnInitialized() + { + _validationViewModel.AddTo(_disposable); + _helloWorldViewModel.AddTo(_disposable); + + _helloWorldViewModel.Output + .Subscribe(_ => InvokeAsync(StateHasChanged)) + .AddTo(_disposable); + } + + public void Dispose() => _disposable.Dispose(); } diff --git a/Samples/Blazor/BlazorSample.Shared/ViewModels/HelloWorldViewModel.cs b/Samples/Blazor/BlazorSample.Shared/ViewModels/HelloWorldViewModel.cs new file mode 100644 index 00000000..c895aa53 --- /dev/null +++ b/Samples/Blazor/BlazorSample.Shared/ViewModels/HelloWorldViewModel.cs @@ -0,0 +1,26 @@ +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Reactive.Bindings; +using Reactive.Bindings.Extensions; + +namespace BlazorSample.Shared.ViewModels; + +public class HelloWorldViewModel : IDisposable +{ + private readonly CompositeDisposable _disposable = new(); + + public ReactivePropertySlim Input { get; } + public ReadOnlyReactivePropertySlim Output { get; } + + public HelloWorldViewModel() + { + Input = new ReactivePropertySlim("") + .AddTo(_disposable); + Output = Input + .Select(x => x.ToUpperInvariant()) + .ToReadOnlyReactivePropertySlim("") + .AddTo(_disposable); + } + + public void Dispose() => _disposable.Dispose(); +} diff --git a/Samples/Blazor/BlazorSample.Shared/ViewModels/IndexViewModel.cs b/Samples/Blazor/BlazorSample.Shared/ViewModels/ValidationViewModel.cs similarity index 94% rename from Samples/Blazor/BlazorSample.Shared/ViewModels/IndexViewModel.cs rename to Samples/Blazor/BlazorSample.Shared/ViewModels/ValidationViewModel.cs index 4c716626..2e0ecca0 100644 --- a/Samples/Blazor/BlazorSample.Shared/ViewModels/IndexViewModel.cs +++ b/Samples/Blazor/BlazorSample.Shared/ViewModels/ValidationViewModel.cs @@ -6,7 +6,7 @@ namespace BlazorSample.Shared.ViewModels; -public class IndexViewModel : IDisposable +public class ValidationViewModel : IDisposable { private readonly CompositeDisposable _disposables = new(); @@ -18,7 +18,7 @@ public class IndexViewModel : IDisposable public ReactiveProperty LastName { get; } public ReadOnlyReactivePropertySlim FullName { get; } - public IndexViewModel() + public ValidationViewModel() { var reactivePropertyMode = ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError; FirstName = new ReactiveProperty("", mode: reactivePropertyMode) diff --git a/Samples/Blazor/BlazorServerApp/App.razor b/Samples/Blazor/BlazorServerApp/App.razor index 5ad7119f..7ea6f775 100644 --- a/Samples/Blazor/BlazorServerApp/App.razor +++ b/Samples/Blazor/BlazorServerApp/App.razor @@ -1,5 +1,5 @@ @using BlazorSample.Shared.ViewModels - + diff --git a/Samples/Blazor/BlazorServerApp/Program.cs b/Samples/Blazor/BlazorServerApp/Program.cs index 77a08a6a..ae71739c 100644 --- a/Samples/Blazor/BlazorServerApp/Program.cs +++ b/Samples/Blazor/BlazorServerApp/Program.cs @@ -9,7 +9,8 @@ builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); builder.Services.AddSingleton(); -builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); var app = builder.Build(); diff --git a/Samples/Blazor/BlazorWasmApp/App.razor b/Samples/Blazor/BlazorWasmApp/App.razor index 5ad7119f..7ea6f775 100644 --- a/Samples/Blazor/BlazorWasmApp/App.razor +++ b/Samples/Blazor/BlazorWasmApp/App.razor @@ -1,5 +1,5 @@ @using BlazorSample.Shared.ViewModels - + diff --git a/Samples/Blazor/BlazorWasmApp/Program.cs b/Samples/Blazor/BlazorWasmApp/Program.cs index 3ba5bd4d..22be29d8 100644 --- a/Samples/Blazor/BlazorWasmApp/Program.cs +++ b/Samples/Blazor/BlazorWasmApp/Program.cs @@ -7,7 +7,8 @@ builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); -builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); await builder.Build().RunAsync(); diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index 5db16283..ff7dcdec 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -1,7 +1,7 @@ Reactive.Bindings - 8.1.0 + 8.1.2 neuecc xin9le okazuki https://github.com/runceel/ReactiveProperty rx mvvm async rx-main reactive diff --git a/Source/ReactiveProperty.NETStandard/Binding/RxBindingExtensions.cs b/Source/ReactiveProperty.NETStandard/Binding/RxBindingExtensions.cs index 43d0e51a..357a2e68 100644 --- a/Source/ReactiveProperty.NETStandard/Binding/RxBindingExtensions.cs +++ b/Source/ReactiveProperty.NETStandard/Binding/RxBindingExtensions.cs @@ -1,4 +1,6 @@ -using System; +// This namespace is not used almost all cases. low priority to add nullable annnotation. +#nullable disable +using System; using System.Diagnostics; using System.Linq.Expressions; using System.Reactive; @@ -34,11 +36,11 @@ public static IDisposable BindTo( TTarget target, Expression> propertySelector, BindingMode mode = BindingMode.OneWay, - Func? convert = null, - Func? convertBack = null, - IObservable? targetUpdateTrigger = null, - TProperty? propertyFallbackValue = default(TProperty), - T? sourceFallbackValue = default(T)) + Func convert = null, + Func convertBack = null, + IObservable targetUpdateTrigger = null, + TProperty propertyFallbackValue = default(TProperty), + T sourceFallbackValue = default(T)) { if (convert == null) { @@ -101,8 +103,8 @@ public static IDisposable BindTo( this ReadOnlyReactiveProperty self, TTarget target, Expression> propertySelector, - Func? convert = null, - TProperty? propertyFallbackValue = default(TProperty)) + Func convert = null, + TProperty propertyFallbackValue = default(TProperty)) { if (convert == null) { @@ -138,11 +140,11 @@ public static IDisposable BindTo( TTarget target, Expression> propertySelector, BindingMode mode = BindingMode.OneWay, - Func? convert = null, - Func? convertBack = null, - IObservable? targetUpdateTrigger = null, - TProperty? propertyFallbackValue = default(TProperty), - T? sourceFallbackValue = default(T)) + Func convert = null, + Func convertBack = null, + IObservable targetUpdateTrigger = null, + TProperty propertyFallbackValue = default(TProperty), + T sourceFallbackValue = default(T)) { if (convert == null) { @@ -205,8 +207,8 @@ public static IDisposable BindTo( this ReadOnlyReactivePropertySlim self, TTarget target, Expression> propertySelector, - Func? convert = null, - TProperty? propertyFallbackValue = default(TProperty)) + Func convert = null, + TProperty propertyFallbackValue = default(TProperty)) { if (convert == null) { diff --git a/Source/ReactiveProperty.NETStandard/Binding/RxCommandExtensions.cs b/Source/ReactiveProperty.NETStandard/Binding/RxCommandExtensions.cs index b6c24595..b0a9dd58 100644 --- a/Source/ReactiveProperty.NETStandard/Binding/RxCommandExtensions.cs +++ b/Source/ReactiveProperty.NETStandard/Binding/RxCommandExtensions.cs @@ -1,4 +1,6 @@ -using System; +// This namespace is not used almost all cases. low priority to add nullable annnotation. +#nullable disable +using System; namespace Reactive.Bindings.Binding; diff --git a/Source/ReactiveProperty.NETStandard/Extensions/INotifyCollectionChangedExtensions.cs b/Source/ReactiveProperty.NETStandard/Extensions/INotifyCollectionChangedExtensions.cs index 776d0150..2bd56876 100644 --- a/Source/ReactiveProperty.NETStandard/Extensions/INotifyCollectionChangedExtensions.cs +++ b/Source/ReactiveProperty.NETStandard/Extensions/INotifyCollectionChangedExtensions.cs @@ -12,6 +12,9 @@ namespace Reactive.Bindings.Extensions; +// TODO: remove this line later. +#nullable disable + /// /// INotify Collection Changed Extensions /// @@ -49,7 +52,7 @@ public static IObservable ObserveAddChangedItems(this INotifyCollectionC public static IObservable ObserveRemoveChanged(this INotifyCollectionChanged source) => source.CollectionChangedAsObservable() .Where(e => e.Action == NotifyCollectionChangedAction.Remove) - .Select(e => (T)e.OldItems[0]); + .Select(e => (T)e.OldItems[0]!); /// /// Observe CollectionChanged:Remove. diff --git a/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs b/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs index 459c57dd..400cfd6a 100644 --- a/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs +++ b/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs @@ -116,8 +116,7 @@ public static ReactiveProperty ToReactivePropertyAsSynchronized observer, x => x) - .StartWith(observer.GetPropertyPathValue()) - .ToReactiveProperty(raiseEventScheduler, mode: mode); + .ToReactiveProperty(raiseEventScheduler, initialValue: observer.GetPropertyPathValue(), mode: mode); result .Where(_ => !ignoreValidationErrorValue || !result.HasErrors) .Subscribe(x => observer.SetPropertyPathValue(x)); @@ -125,8 +124,9 @@ public static ReactiveProperty ToReactivePropertyAsSynchronized.LookupGet(propertySelector, out var _); + var result = subject.ObserveSimpleProperty(propertySelector, isPushCurrentValueAtFirst: false) + .ToReactiveProperty(raiseEventScheduler, initialValue: getter(subject), mode: mode); var setter = AccessorCache.LookupSet(propertySelector, out _); result .Where(_ => !ignoreValidationErrorValue || !result.HasErrors) @@ -244,7 +244,7 @@ public static ReactiveProperty ToReactivePropertyAsSynchronized observer, x => x) .StartWith(observer.GetPropertyPathValue())) - .ToReactiveProperty(raiseEventScheduler, mode: mode); + .ToReactiveProperty(raiseEventScheduler, mode: mode); convertBack(result.Where(_ => !ignoreValidationErrorValue || !result.HasErrors)) .Subscribe(x => observer.SetPropertyPathValue(x)); return result; @@ -253,7 +253,7 @@ public static ReactiveProperty ToReactivePropertyAsSynchronized.LookupSet(propertySelector, out _); var result = convert(subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true)) - .ToReactiveProperty(raiseEventScheduler, mode: mode); + .ToReactiveProperty(raiseEventScheduler, mode: mode); convertBack(result.Where(_ => !ignoreValidationErrorValue || !result.HasErrors)) .Subscribe(x => setter(subject, x)); return result; @@ -347,8 +347,7 @@ public static ReactivePropertySlim ToReactivePropertySlimAsSynchronized { var result = new ReactivePropertySlim(mode: mode); var observer = PropertyObservable.CreateFromPropertySelector(subject, propertySelector); - IDisposable disposable = null; - disposable = convert(subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true)) + var disposable = convert(subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true)) .Subscribe(x => result.Value = x); convertBack(result) .Subscribe(x => observer.SetPropertyPathValue(x), _ => disposable.Dispose(), () => disposable.Dispose()); @@ -358,8 +357,7 @@ public static ReactivePropertySlim ToReactivePropertySlimAsSynchronized { var setter = AccessorCache.LookupSet(propertySelector, out _); var result = new ReactivePropertySlim(mode: mode); - IDisposable disposable = null; - disposable = convert(subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true)) + var disposable = convert(subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true)) .Subscribe(x => result.Value = x); convertBack(result) .Subscribe(x => setter(subject, x), _ => disposable.Dispose(), () => disposable.Dispose()); diff --git a/Source/ReactiveProperty.NETStandard/Extensions/PairwiseObservableExtensions.cs b/Source/ReactiveProperty.NETStandard/Extensions/PairwiseObservableExtensions.cs index b808faad..d3576492 100644 --- a/Source/ReactiveProperty.NETStandard/Extensions/PairwiseObservableExtensions.cs +++ b/Source/ReactiveProperty.NETStandard/Extensions/PairwiseObservableExtensions.cs @@ -21,7 +21,7 @@ public static IObservable Pairwise(this IObservable source, Func(observer => { - var prev = default(T); + T prev = default!; var isFirst = true; return source.Subscribe(x => @@ -36,7 +36,7 @@ public static IObservable Pairwise(this IObservable source, Func OnErrorRetry( var empty = Observable.Empty(); var count = 0; - IObservable? self = null; + IObservable self = null!; self = source.Catch((TException ex) => { onError(ex); return (++count < retryCount) ? (dueTime == TimeSpan.Zero) - ? self!.SubscribeOn(Scheduler.CurrentThread) + ? self.SubscribeOn(Scheduler.CurrentThread) : empty.Delay(dueTime, delayScheduler).Concat(self).SubscribeOn(Scheduler.CurrentThread) : Observable.Throw(ex); }); diff --git a/Source/ReactiveProperty.NETStandard/Helpers/CollectionUtilities.cs b/Source/ReactiveProperty.NETStandard/Helpers/CollectionUtilities.cs index cc693276..01672d09 100644 --- a/Source/ReactiveProperty.NETStandard/Helpers/CollectionUtilities.cs +++ b/Source/ReactiveProperty.NETStandard/Helpers/CollectionUtilities.cs @@ -86,8 +86,9 @@ public static IObservable> ObserveElementObser if (!(propertySelector.Body is MemberExpression memberExpression)) { if (!(propertySelector.Body is UnaryExpression unaryExpression)) { throw new ArgumentException(nameof(propertySelector)); } - memberExpression = unaryExpression.Operand as MemberExpression; - if (memberExpression == null) { throw new ArgumentException(nameof(propertySelector)); } + var operand = unaryExpression.Operand as MemberExpression; + if (operand == null) { throw new ArgumentException(nameof(propertySelector)); } + memberExpression = operand; } var propertyInfo = memberExpression.Member as PropertyInfo; @@ -154,11 +155,11 @@ internal static IObservable ObserveElementCore(observer => { - //--- cache element property subscriptions - var subscriptionCache = new Dictionary(); + //--- cache element property subscriptions + var subscriptionCache = new Dictionary(); - //--- subscribe / unsubscribe property which all elements have - void subscribe(IEnumerable elements) + //--- subscribe / unsubscribe property which all elements have + void subscribe(IEnumerable elements) { foreach (var x in elements) { @@ -177,14 +178,14 @@ void unsubscribeAll() } subscribe(source); - //--- hook collection changed - var disposable = source.CollectionChangedAsObservable().Subscribe(x => + //--- hook collection changed + var disposable = source.CollectionChangedAsObservable().Subscribe(x => { if (x.Action == NotifyCollectionChangedAction.Remove || x.Action == NotifyCollectionChangedAction.Replace) { - //--- unsubscribe - var oldItems = x.OldItems.Cast(); + //--- unsubscribe + var oldItems = x.OldItems.Cast(); foreach (var y in oldItems) { subscriptionCache[y].Dispose(); @@ -206,8 +207,8 @@ void unsubscribeAll() } }); - //--- unsubscribe - return Disposable.Create(() => + //--- unsubscribe + return Disposable.Create(() => { disposable.Dispose(); unsubscribeAll(); diff --git a/Source/ReactiveProperty.NETStandard/Helpers/FilteredReadOnlyObservableCollection.cs b/Source/ReactiveProperty.NETStandard/Helpers/FilteredReadOnlyObservableCollection.cs index 221b4611..f2febb17 100644 --- a/Source/ReactiveProperty.NETStandard/Helpers/FilteredReadOnlyObservableCollection.cs +++ b/Source/ReactiveProperty.NETStandard/Helpers/FilteredReadOnlyObservableCollection.cs @@ -86,14 +86,14 @@ public FilteredReadOnlyObservableCollection(TCollection source, Func(source, (x, observer) => elementChangedFactory(x).Subscribe(_ => observer.OnNext(x))) .Subscribe(SourceElementChanged) .AddTo(Subscription); - - // collection changed(support single changed only) - source.CollectionChanged += Source_CollectionChanged; } private void SourceElementChanged(TElement x) diff --git a/Source/ReactiveProperty.NETStandard/Interactivity/IEventToReactiveConverter.cs b/Source/ReactiveProperty.NETStandard/Interactivity/IEventToReactiveConverter.cs index 14bbfe65..f3bc14c3 100644 --- a/Source/ReactiveProperty.NETStandard/Interactivity/IEventToReactiveConverter.cs +++ b/Source/ReactiveProperty.NETStandard/Interactivity/IEventToReactiveConverter.cs @@ -14,12 +14,12 @@ public interface IEventToReactiveConverter /// Gets or sets the associate object. /// /// The associate object. - object AssociateObject { get; set; } + object? AssociateObject { get; set; } /// /// Converts the specified source. /// /// The source. /// - IObservable Convert(IObservable source); + IObservable Convert(IObservable source); } diff --git a/Source/ReactiveProperty.NETStandard/Interactivity/ReactiveConverter.cs b/Source/ReactiveProperty.NETStandard/Interactivity/ReactiveConverter.cs index 5ac40a96..eb77fa81 100644 --- a/Source/ReactiveProperty.NETStandard/Interactivity/ReactiveConverter.cs +++ b/Source/ReactiveProperty.NETStandard/Interactivity/ReactiveConverter.cs @@ -17,19 +17,19 @@ public abstract class ReactiveConverter : IEventToReactiveConverter /// /// EventToReactiveCommand's AssociateObject /// - public object AssociateObject { get; set; } + public object? AssociateObject { get; set; } /// /// Converts the specified source. /// /// The source. /// - public IObservable Convert(IObservable source) => OnConvert(source.Cast()).Select(x => (object)x); + public IObservable Convert(IObservable source) => OnConvert(source.Select(x => (T?)x)).Select(x => (object?)x); /// /// Converts /// /// source /// dest - protected abstract IObservable OnConvert(IObservable source); + protected abstract IObservable OnConvert(IObservable source); } diff --git a/Source/ReactiveProperty.NETStandard/Internals/PropertyObservable.cs b/Source/ReactiveProperty.NETStandard/Internals/PropertyObservable.cs index 13709568..1ed30b28 100644 --- a/Source/ReactiveProperty.NETStandard/Internals/PropertyObservable.cs +++ b/Source/ReactiveProperty.NETStandard/Internals/PropertyObservable.cs @@ -7,7 +7,7 @@ namespace Reactive.Bindings.Internals; internal sealed class PropertyObservable : IObservable, IDisposable { - private PropertyPathNode RootNode { get; set; } + private PropertyPathNode? RootNode { get; set; } internal void SetRootNode(PropertyPathNode rootNode) { RootNode?.SetCallback(null); @@ -17,7 +17,7 @@ internal void SetRootNode(PropertyPathNode rootNode) public TProperty GetPropertyPathValue() { var value = RootNode?.GetPropertyPathValue(); - return value != null ? (TProperty)value : default; + return value != null ? (TProperty)value : default!; } public string Path => RootNode?.Path; diff --git a/Source/ReactiveProperty.NETStandard/Internals/PropertyPathNode.cs b/Source/ReactiveProperty.NETStandard/Internals/PropertyPathNode.cs index 6a5888b4..e5c66344 100644 --- a/Source/ReactiveProperty.NETStandard/Internals/PropertyPathNode.cs +++ b/Source/ReactiveProperty.NETStandard/Internals/PropertyPathNode.cs @@ -24,7 +24,7 @@ public PropertyPathNode(string propertyName) private Type? PrevSourceType { get; set; } public PropertyPathNode? Next { get; private set; } public PropertyPathNode? Prev { get; private set; } - public void SetCallback(Action callback) + public void SetCallback(Action? callback) { _callback = callback; Next?.SetCallback(callback); diff --git a/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs b/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs index b3b518f5..62983a5b 100644 --- a/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs +++ b/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs @@ -82,7 +82,7 @@ public class ReactiveProperty : IReactiveProperty, IObserverLinkedList private Lazy> ErrorsTrigger { get; } - private Lazy, IObservable>>> ValidatorStore { get; } = new Lazy, IObservable>>>(() => new List, IObservable>>()); + private Lazy, IObservable>>> ValidatorStore { get; } = new Lazy, IObservable>>>(() => new List, IObservable>>()); /// /// PropertyChanged raise on ReactivePropertyScheduler @@ -283,7 +283,7 @@ public override string ToString() => /// /// If success return IO<null>, failure return IO<IEnumerable>(Errors). /// Self. - public ReactiveProperty SetValidateNotifyError(Func, IObservable> validator) + public ReactiveProperty SetValidateNotifyError(Func, IObservable> validator) { ValidatorStore.Value.Add(validator); //--- cache validation functions var validators = ValidatorStore.Value @@ -304,12 +304,12 @@ public ReactiveProperty SetValidateNotifyError(Func, IObservab } var strings = xs - .OfType() - .Where(x => x != null); + .Where(x => x != null) + .OfType(); var others = xs - .Where(x => !(x is string)) + .Where(x => x is not string) .Where(x => x != null) - .SelectMany(x => x.Cast()); + .SelectMany(x => x!.Cast()); return strings.Concat(others); }) .Subscribe(x => @@ -339,15 +339,15 @@ public ReactiveProperty SetValidateNotifyError(Func, IObservab /// /// If success return IO<null>, failure return IO<IEnumerable>(Errors). /// Self. - public ReactiveProperty SetValidateNotifyError(Func, IObservable> validator) => - SetValidateNotifyError(xs => validator(xs).Cast()); + public ReactiveProperty SetValidateNotifyError(Func, IObservable> validator) => + SetValidateNotifyError(xs => validator(xs).Select(x => (IEnumerable?)x)); /// /// Set INotifyDataErrorInfo's asynchronous validation. /// /// Validation logic /// Self. - public ReactiveProperty SetValidateNotifyError(Func> validator) => + public ReactiveProperty SetValidateNotifyError(Func> validator) => SetValidateNotifyError(xs => xs.SelectMany(x => validator(x))); /// @@ -355,7 +355,7 @@ public ReactiveProperty SetValidateNotifyError(Func> val /// /// Validation logic /// Self. - public ReactiveProperty SetValidateNotifyError(Func> validator) => + public ReactiveProperty SetValidateNotifyError(Func> validator) => SetValidateNotifyError(xs => xs.SelectMany(x => validator(x))); /// @@ -363,7 +363,7 @@ public ReactiveProperty SetValidateNotifyError(Func> validato /// /// Validation logic /// Self. - public ReactiveProperty SetValidateNotifyError(Func validator) => + public ReactiveProperty SetValidateNotifyError(Func validator) => SetValidateNotifyError(xs => xs.Select(x => validator(x))); /// @@ -371,7 +371,7 @@ public ReactiveProperty SetValidateNotifyError(Func validator /// /// Validation logic /// Self. - public ReactiveProperty SetValidateNotifyError(Func validator) => + public ReactiveProperty SetValidateNotifyError(Func validator) => SetValidateNotifyError(xs => xs.Select(x => validator(x))); /// diff --git a/Source/ReactiveProperty.NETStandard/ReactivePropertyExtensions.cs b/Source/ReactiveProperty.NETStandard/ReactivePropertyExtensions.cs index a5bf0611..851f8b43 100644 --- a/Source/ReactiveProperty.NETStandard/ReactivePropertyExtensions.cs +++ b/Source/ReactiveProperty.NETStandard/ReactivePropertyExtensions.cs @@ -20,7 +20,7 @@ public static class ReactivePropertyExtensions /// Target ReactiveProperty /// Target property as expression /// Self - public static ReactiveProperty SetValidateAttribute(this ReactiveProperty self, Expression>> selfSelector) + public static ReactiveProperty SetValidateAttribute(this ReactiveProperty self, Expression?>> selfSelector) { var memberExpression = (MemberExpression)selfSelector.Body; var propertyInfo = (PropertyInfo)memberExpression.Member; @@ -54,7 +54,7 @@ public static ReactiveProperty SetValidateAttribute(this ReactiveProperty< /// /// Property type /// Target ReactiveProperty - public static IObservable ObserveValidationErrorMessage(this ReactiveProperty self) => + public static IObservable ObserveValidationErrorMessage(this ReactiveProperty self) => self.ObserveErrorChanged .Select(x => x?.OfType()?.FirstOrDefault()); } diff --git a/Source/ReactiveProperty.Platform.Android/ListAdapter.cs b/Source/ReactiveProperty.Platform.Android/ListAdapter.cs index 9ec0d224..827c9f6b 100644 --- a/Source/ReactiveProperty.Platform.Android/ListAdapter.cs +++ b/Source/ReactiveProperty.Platform.Android/ListAdapter.cs @@ -26,7 +26,7 @@ public class ListAdapter : BaseAdapter /// create view /// set row data /// get id - public ListAdapter(IList list, Func createRowView, Action setRowData, Func? getId = null) + public ListAdapter(IList list, Func createRowView, Action setRowData, Func getId = null) { List = list ?? throw new ArgumentNullException(nameof(list)); CreateRowView = createRowView ?? throw new ArgumentNullException(nameof(createRowView)); @@ -65,7 +65,7 @@ public ListAdapter(IList list, Func createRowView, ActionTo be added. /// To be added. /// To be added. - public override View GetView(int position, View? convertView, ViewGroup? parent) + public override View GetView(int position, View convertView, ViewGroup parent) { if (convertView == null) { @@ -91,6 +91,6 @@ public static class ListExtensions /// fill row data /// get id /// ListAdapter - public static ListAdapter ToAdapter(this IList self, Func createRowView, Action setRowData, Func? getId = null) => + public static ListAdapter ToAdapter(this IList self, Func createRowView, Action setRowData, Func getId = null) => new(self, createRowView, setRowData, getId); } diff --git a/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj b/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj index 3e5e9c81..219d3c5d 100644 --- a/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj +++ b/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj @@ -7,6 +7,7 @@ true key.snk ReactiveProperty.XamarinAndroid provides many useful extension methods for Xamarin.Android that can be used with ReactiveProperty. + disable diff --git a/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj b/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj index cd902632..f455f7ed 100644 --- a/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj +++ b/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj @@ -7,6 +7,7 @@ true key.snk ReactiveProperty.XamariniOS provides many useful extension methods for Xamarin.iOS that can be used with ReactiveProperty. + disable diff --git a/Test/ReactiveProperty.NETStandard.Tests/Helpers/FilteredReadOnlyObservableCollectionTest.cs b/Test/ReactiveProperty.NETStandard.Tests/Helpers/FilteredReadOnlyObservableCollectionTest.cs index 03420bc8..673eb502 100644 --- a/Test/ReactiveProperty.NETStandard.Tests/Helpers/FilteredReadOnlyObservableCollectionTest.cs +++ b/Test/ReactiveProperty.NETStandard.Tests/Helpers/FilteredReadOnlyObservableCollectionTest.cs @@ -462,6 +462,23 @@ public void NestedPropertyCase() filtered.Select(x => x.Name).Is("tanaka2", "tanaka3", "tanaka4", "tanaka5"); } + [TestMethod] + public void ElementStatusChangedFactoryWithObservePropertyTest() + { + // https://github.com/runceel/ReactiveProperty/issues/379 + var observableCollection = new ObservableCollection(); + + var filteredCollection = observableCollection.ToFilteredReadOnlyObservableCollection( + filter: x => x.Name == "test", + elementStatusChangedFactory: x => x.ObserveProperty(x => x.Name)); + + AssertEx.DoesNotThrow(() => + { + var element = new Person(); + observableCollection.Add(element); // Exception thrown here (Issue 379) + }); + } + private class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; diff --git a/docs/docs/.vuepress/config.js b/docs/docs/.vuepress/config.js index bd4eff9f..c4e14de6 100644 --- a/docs/docs/.vuepress/config.js +++ b/docs/docs/.vuepress/config.js @@ -14,6 +14,7 @@ module.exports = { { text: 'Xamarin.Forms', link: '/getting-started/xf.html' }, { text: 'Avalonia', link: '/getting-started/avalonia.html' }, { text: 'Uno Platform', link: '/getting-started/uno-platform.html' }, + { text: 'Blazor', link: '/getting-started/blazor.html' }, { text: 'Add code snippets', link: '/getting-started/add-snippets.html' }, ] }, diff --git a/docs/docs/getting-started/blazor.md b/docs/docs/getting-started/blazor.md new file mode 100644 index 00000000..e3633113 --- /dev/null +++ b/docs/docs/getting-started/blazor.md @@ -0,0 +1,126 @@ +# Getting start for Blazor + +Blazor is for development framework for Single Page Application using C#. + +See below: + +[ASP.NET Core Blazor](https://docs.microsoft.com/en-us/aspnet/core/blazor/) + +ReactiveProperty also works both of Blazor Server and Blazor WebAssembly. But Blazor WebAssembly doen't support all operations of Reactive Extensions. For example, `Delay` extension method doesn't work on Blazor WASM. +If you want to use ReactiveProperty on Blazor WASM, then please care to not use unsupported features. + +## Create a project + +- Create a Blazor Server or WebAssembly project. +- Install ReactiveProperty.Blazor package from NuGet. + +## Edit codes + +- Create a class what name is `IndexViewModel`. +- Edit the class like fillowing: + +```csharp +using Reactive.Bindings; +using Reactive.Bindings.Extensions; +using System.Reactive.Disposables; +using System.Reactive.Linq; + +namespace BlazorApp1; + +public class IndexViewModel : IDisposable +{ + private CompositeDisposable _disposable = new(); + + public ReactivePropertySlim Input { get; } + public ReadOnlyReactivePropertySlim Output { get; } + + public IndexViewModel() + { + Input = new ReactivePropertySlim("") + .AddTo(_disposable); + Output = Input + .Delay(TimeSpan.FromSeconds(2)) // Important! Delay method doesn't work on Blazor WASM. If you are working on WASM, then please remove this line. + .Select(x => x.ToUpperInvariant()) + .ToReadOnlyReactivePropertySlim("") + .AddTo(_disposable); + } + + public void Dispose() => _disposable.Dispose(); +} +``` + +- Edit Index.razor like below: + +```csharp +@page "/" +@using System.Reactive.Disposables +@using Reactive.Bindings.Extensions +@implements IDisposable + +Index + +

Hello, world!

+ + +
+@_viewModel.Output.Value +
+ +@code { + private readonly CompositeDisposable _disposable = new(); + private IndexViewModel _viewModel = default!; + + protected override void OnInitialized() + { + _viewModel = new IndexViewModel() + .AddTo(_disposable); + + // Observe changing Output property, and call StateHasChanged on UI thread. + _viewModel.Output + .Subscribe(x => InvokeAsync(StateHasChanged)) + .AddTo(_disposable); + } + + public void Dispose() => _disposable.Dispose(); +} +``` + +- Launch this app + +You can see below result: + +![Launch the app](./images/blazor-helloworld.png) + +## Other topics for Blazor + +### Dependency Injection + +If you want to inject ViewModels to page, then please register ViewModels to DI container on Program.cs like below: + +```csharp +builder.Services.AddTransient(); +``` + +And inject to page like `@inject IndexViewModel _viewModel`. + + +### Integrate validation feature + +If you want to use validation feature of ReactiveProperty with Blazor's EditForm component, then you can use `Reactive.Bindings.Components.ReactivePropertiesValidator` component. + +`ReactivePropertiesValidator` can be used same as `DataAnnotationsValidator` component like below: + +```csharp + + @* needs @using Reactive.Bindings.Components *@ + + + +
+ + + +
+``` + +Please see Blazor's sample app under Samples/Blazor folder to know more details. The page using it is Pages/Index.razor page of BlazorSample.Shared project. diff --git a/docs/docs/getting-started/images/blazor-helloworld.png b/docs/docs/getting-started/images/blazor-helloworld.png new file mode 100644 index 00000000..ba2df8f6 Binary files /dev/null and b/docs/docs/getting-started/images/blazor-helloworld.png differ diff --git a/docs/package-lock.json b/docs/package-lock.json index bc34ef09..cabf966d 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -1975,9 +1975,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" @@ -4247,9 +4247,9 @@ "dev": true }, "eventsource": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", - "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.1.tgz", + "integrity": "sha512-qV5ZC0h7jYIAOhArFJgSfdyz6rALJyb270714o7ZtNnw2WSJ+eexhKtE0O8LYPRsHZHf2osHKZBxGPvm3kPkCA==", "dev": true, "requires": { "original": "^1.0.0"