From a2a4797bea63c44363c057f6eef0e61bdb5d9e19 Mon Sep 17 00:00:00 2001 From: Chris Pulman Date: Sat, 1 Feb 2025 23:11:31 +0000 Subject: [PATCH] Update ReactiveProperty for multiple subscribers (#3953) **What kind of change does this PR introduce?** fix for #3935 **What is the current behavior?** #3935 **What is the new behavior?** ReactiveProperty updated to handle multiple subscribers **What might this PR break?** None expected, core function still retained **Please check if the PR fulfills these requirements** - [x] Tests for the changes have been added (for bug fixes / features) - [ ] Docs have been added / updated (for bug fixes / features) **Other information**: --- src/Directory.Packages.props | 22 +- ...valTests.ReactiveUI.DotNet8_0.verified.txt | 20 +- ...valTests.ReactiveUI.DotNet9_0.verified.txt | 20 +- ...provalTests.ReactiveUI.Net4_7.verified.txt | 10 +- ...rovalTests.Winforms.DotNet8_0.verified.txt | 2 +- ...rovalTests.Winforms.DotNet9_0.verified.txt | 2 +- ...ApprovalTests.Winforms.Net4_7.verified.txt | 2 +- .../Mocks/SubcribeTestViewModel.cs | 86 ++ .../ReactiveProperty/ReactivePropertyTest.cs | 822 +++++++++--------- .../Command/CommandBinderImplementation.cs | 12 + .../Expression/ExpressionRewriter.cs | 12 + .../Mixins/DependencyResolverMixins.cs | 12 + .../Platforms/android/ControlFetcherMixin.cs | 8 + .../android/FlexibleCommandBinder.cs | 4 + .../Platforms/mac/AutoSuspendHelper.cs | 4 + .../ComponentModelTypeConverter.cs | 4 + .../net/ComponentModelTypeConverter.cs | 4 + .../uikit-common/AutoSuspendHelper.cs | 4 + .../ReactiveProperty/ReactiveProperty.cs | 50 +- .../ReactivePropertyMixins.cs | 4 + 20 files changed, 662 insertions(+), 442 deletions(-) create mode 100644 src/ReactiveUI.Tests/ReactiveProperty/Mocks/SubcribeTestViewModel.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index d734646c6d..751e37799a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -4,12 +4,14 @@ true - 15.2.22 + 15.3.1 + 1.13.1.4 + 2.8.4.1 - - + + @@ -20,12 +22,12 @@ - + - + - + @@ -38,11 +40,11 @@ - + - + @@ -55,8 +57,8 @@ - - + + diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt index 81f67c638f..2a283a3d7b 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt @@ -92,10 +92,14 @@ namespace ReactiveUI public class CommandBinderImplementation : Splat.IEnableLogger { public CommandBinderImplementation() { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public ReactiveUI.IReactiveBinding BindCommand(TViewModel? viewModel, TView view, System.Linq.Expressions.Expression> vmProperty, System.Linq.Expressions.Expression> controlProperty, System.IObservable withParameter, string? toEvent = null) where TView : class, ReactiveUI.IViewFor where TViewModel : class where TProp : System.Windows.Input.ICommand { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public ReactiveUI.IReactiveBinding BindCommand(TViewModel? viewModel, TView view, System.Linq.Expressions.Expression> vmProperty, System.Linq.Expressions.Expression> controlProperty, System.Linq.Expressions.Expression> withParameter, string? toEvent = null) where TView : class, ReactiveUI.IViewFor where TViewModel : class @@ -108,6 +112,8 @@ namespace ReactiveUI public static System.Collections.Generic.IComparer ThenByDescending(this System.Collections.Generic.IComparer? parent, System.Func selector) { } public static System.Collections.Generic.IComparer ThenByDescending(this System.Collections.Generic.IComparer? parent, System.Func selector, System.Collections.Generic.IComparer comparer) { } } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public class ComponentModelTypeConverter : ReactiveUI.IBindingTypeConverter, Splat.IEnableLogger { public ComponentModelTypeConverter() { } @@ -154,6 +160,8 @@ namespace ReactiveUI public static class DependencyResolverMixins { public static void InitializeReactiveUI(this Splat.IMutableDependencyResolver resolver, params ReactiveUI.RegistrationNamespace[] registrationNamespaces) { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public static void RegisterViewsForViewModels(this Splat.IMutableDependencyResolver resolver, System.Reflection.Assembly assembly) { } } public class DoubleToStringTypeConverter : ReactiveUI.IBindingTypeConverter, Splat.IEnableLogger @@ -766,6 +774,8 @@ namespace ReactiveUI } public static class ReactivePropertyMixins { + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public static ReactiveUI.ReactiveProperty AddValidation(this ReactiveUI.ReactiveProperty self, System.Linq.Expressions.Expression?>> selfSelector) { } public static System.IObservable ObserveValidationErrors(this ReactiveUI.ReactiveProperty self) { } } @@ -874,19 +884,19 @@ namespace ReactiveUI public RoutingState(System.Reactive.Concurrency.IScheduler? scheduler = null) { } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public System.IObservable CurrentViewModel { get; set; } + public System.IObservable CurrentViewModel { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public ReactiveUI.ReactiveCommand Navigate { get; set; } + public ReactiveUI.ReactiveCommand Navigate { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public ReactiveUI.ReactiveCommand NavigateAndReset { get; set; } + public ReactiveUI.ReactiveCommand NavigateAndReset { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public ReactiveUI.ReactiveCommand NavigateBack { get; set; } + public ReactiveUI.ReactiveCommand NavigateBack { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public System.IObservable> NavigationChanged { get; set; } + public System.IObservable> NavigationChanged { get; protected set; } [System.Runtime.Serialization.DataMember] [System.Text.Json.Serialization.JsonRequired] public System.Collections.ObjectModel.ObservableCollection NavigationStack { get; set; } diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet9_0.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet9_0.verified.txt index 40452cf552..9db5df94a4 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet9_0.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet9_0.verified.txt @@ -92,10 +92,14 @@ namespace ReactiveUI public class CommandBinderImplementation : Splat.IEnableLogger { public CommandBinderImplementation() { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public ReactiveUI.IReactiveBinding BindCommand(TViewModel? viewModel, TView view, System.Linq.Expressions.Expression> vmProperty, System.Linq.Expressions.Expression> controlProperty, System.IObservable withParameter, string? toEvent = null) where TView : class, ReactiveUI.IViewFor where TViewModel : class where TProp : System.Windows.Input.ICommand { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public ReactiveUI.IReactiveBinding BindCommand(TViewModel? viewModel, TView view, System.Linq.Expressions.Expression> vmProperty, System.Linq.Expressions.Expression> controlProperty, System.Linq.Expressions.Expression> withParameter, string? toEvent = null) where TView : class, ReactiveUI.IViewFor where TViewModel : class @@ -108,6 +112,8 @@ namespace ReactiveUI public static System.Collections.Generic.IComparer ThenByDescending(this System.Collections.Generic.IComparer? parent, System.Func selector) { } public static System.Collections.Generic.IComparer ThenByDescending(this System.Collections.Generic.IComparer? parent, System.Func selector, System.Collections.Generic.IComparer comparer) { } } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public class ComponentModelTypeConverter : ReactiveUI.IBindingTypeConverter, Splat.IEnableLogger { public ComponentModelTypeConverter() { } @@ -154,6 +160,8 @@ namespace ReactiveUI public static class DependencyResolverMixins { public static void InitializeReactiveUI(this Splat.IMutableDependencyResolver resolver, params ReactiveUI.RegistrationNamespace[] registrationNamespaces) { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public static void RegisterViewsForViewModels(this Splat.IMutableDependencyResolver resolver, System.Reflection.Assembly assembly) { } } public class DoubleToStringTypeConverter : ReactiveUI.IBindingTypeConverter, Splat.IEnableLogger @@ -766,6 +774,8 @@ namespace ReactiveUI } public static class ReactivePropertyMixins { + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] public static ReactiveUI.ReactiveProperty AddValidation(this ReactiveUI.ReactiveProperty self, System.Linq.Expressions.Expression?>> selfSelector) { } public static System.IObservable ObserveValidationErrors(this ReactiveUI.ReactiveProperty self) { } } @@ -874,19 +884,19 @@ namespace ReactiveUI public RoutingState(System.Reactive.Concurrency.IScheduler? scheduler = null) { } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public System.IObservable CurrentViewModel { get; set; } + public System.IObservable CurrentViewModel { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public ReactiveUI.ReactiveCommand Navigate { get; set; } + public ReactiveUI.ReactiveCommand Navigate { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public ReactiveUI.ReactiveCommand NavigateAndReset { get; set; } + public ReactiveUI.ReactiveCommand NavigateAndReset { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public ReactiveUI.ReactiveCommand NavigateBack { get; set; } + public ReactiveUI.ReactiveCommand NavigateBack { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public System.IObservable> NavigationChanged { get; set; } + public System.IObservable> NavigationChanged { get; protected set; } [System.Runtime.Serialization.DataMember] [System.Text.Json.Serialization.JsonRequired] public System.Collections.ObjectModel.ObservableCollection NavigationStack { get; set; } diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt index 901c49b221..62d6010bc0 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt @@ -856,19 +856,19 @@ namespace ReactiveUI public RoutingState(System.Reactive.Concurrency.IScheduler? scheduler = null) { } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public System.IObservable CurrentViewModel { get; set; } + public System.IObservable CurrentViewModel { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public ReactiveUI.ReactiveCommand Navigate { get; set; } + public ReactiveUI.ReactiveCommand Navigate { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public ReactiveUI.ReactiveCommand NavigateAndReset { get; set; } + public ReactiveUI.ReactiveCommand NavigateAndReset { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public ReactiveUI.ReactiveCommand NavigateBack { get; set; } + public ReactiveUI.ReactiveCommand NavigateBack { get; protected set; } [System.Runtime.Serialization.IgnoreDataMember] [System.Text.Json.Serialization.JsonIgnore] - public System.IObservable> NavigationChanged { get; set; } + public System.IObservable> NavigationChanged { get; protected set; } [System.Runtime.Serialization.DataMember] [System.Text.Json.Serialization.JsonRequired] public System.Collections.ObjectModel.ObservableCollection NavigationStack { get; set; } diff --git a/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.DotNet8_0.verified.txt b/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.DotNet8_0.verified.txt index ac9d46705e..30a2bcbcd5 100644 --- a/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.DotNet8_0.verified.txt +++ b/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.DotNet8_0.verified.txt @@ -94,7 +94,7 @@ namespace ReactiveUI.Winforms [System.ComponentModel.Category("ReactiveUI")] [System.ComponentModel.Description("The Current View")] [System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Content)] - public object? Content { get; set; } + public object? Content { get; protected set; } public System.Windows.Forms.Control? CurrentView { get; } [System.ComponentModel.Category("ReactiveUI")] [System.ComponentModel.Description("The default control when no viewmodel is specified")] diff --git a/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.DotNet9_0.verified.txt b/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.DotNet9_0.verified.txt index 3515a89dd8..0602a8f618 100644 --- a/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.DotNet9_0.verified.txt +++ b/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.DotNet9_0.verified.txt @@ -94,7 +94,7 @@ namespace ReactiveUI.Winforms [System.ComponentModel.Category("ReactiveUI")] [System.ComponentModel.Description("The Current View")] [System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Content)] - public object? Content { get; set; } + public object? Content { get; protected set; } public System.Windows.Forms.Control? CurrentView { get; } [System.ComponentModel.Category("ReactiveUI")] [System.ComponentModel.Description("The default control when no viewmodel is specified")] diff --git a/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.Net4_7.verified.txt b/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.Net4_7.verified.txt index f37123169a..675c50cb15 100644 --- a/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.Net4_7.verified.txt +++ b/src/ReactiveUI.Tests/Platforms/winforms/API/WinformsApiApprovalTests.Winforms.Net4_7.verified.txt @@ -92,7 +92,7 @@ namespace ReactiveUI.Winforms [System.ComponentModel.Category("ReactiveUI")] [System.ComponentModel.Description("The Current View")] [System.ComponentModel.DesignerSerializationVisibility(System.ComponentModel.DesignerSerializationVisibility.Content)] - public object? Content { get; set; } + public object? Content { get; protected set; } public System.Windows.Forms.Control? CurrentView { get; } [System.ComponentModel.Category("ReactiveUI")] [System.ComponentModel.Description("The default control when no viewmodel is specified")] diff --git a/src/ReactiveUI.Tests/ReactiveProperty/Mocks/SubcribeTestViewModel.cs b/src/ReactiveUI.Tests/ReactiveProperty/Mocks/SubcribeTestViewModel.cs new file mode 100644 index 0000000000..4981cec87a --- /dev/null +++ b/src/ReactiveUI.Tests/ReactiveProperty/Mocks/SubcribeTestViewModel.cs @@ -0,0 +1,86 @@ +// Copyright (c) 2024 .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.Diagnostics; + +namespace ReactiveUI.Tests.ReactiveProperty.Mocks; + +public class SubcribeTestViewModel : IDisposable +{ + private static readonly List _items = []; + private readonly List _cache = []; + private bool _disposedValue; + + /// + /// Initializes a new instance of the class. + /// + /// The count. + public SubcribeTestViewModel(int count) + { + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + for (var i = 0; i < count; i++) + { + _cache.Add(new BasicViewModel(Property)); + } + + stopwatch.Stop(); + + StartupTime = stopwatch.ElapsedMilliseconds; + SubscriberCount = _cache.Count; + SubscriberEvents = _items.Count; + } + + public ReactiveProperty Property { get; } = new(1); + + public int SubscriberCount { get; } + + public int SubscriberEvents { get; } + + public long StartupTime { get; } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue && disposing) + { + foreach (var item in _cache) + { + item.Dispose(); + } + + _disposedValue = true; + } + } + + private class BasicViewModel(IObservable observable) : IDisposable + { + private readonly IDisposable _subscription = observable.Subscribe(_items.Add); + private bool _disposedValue; + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue && disposing) + { + _subscription.Dispose(); + _disposedValue = true; + } + } + } +} diff --git a/src/ReactiveUI.Tests/ReactiveProperty/ReactivePropertyTest.cs b/src/ReactiveUI.Tests/ReactiveProperty/ReactivePropertyTest.cs index 0f1549bdcd..ab85f0284d 100644 --- a/src/ReactiveUI.Tests/ReactiveProperty/ReactivePropertyTest.cs +++ b/src/ReactiveUI.Tests/ReactiveProperty/ReactivePropertyTest.cs @@ -9,460 +9,470 @@ using ReactiveUI.Testing; using ReactiveUI.Tests.ReactiveProperty.Mocks; -namespace ReactiveUI.Tests.ReactiveProperty +namespace ReactiveUI.Tests.ReactiveProperty; + +public class ReactivePropertyTest : ReactiveTest { - public class ReactivePropertyTest : ReactiveTest + [Fact] + public void DefaultValueIsRaisedOnSubscribe() { - [Fact] - public void DefaultValueIsRaisedOnSubscribe() - { - var rp = new ReactiveProperty(); - rp.Value.Should().BeNull(); - rp.Subscribe(Assert.Null); - } + using var rp = new ReactiveProperty(); + rp.Value.Should().BeNull(); + rp.Subscribe(Assert.Null); + } - [Fact] - public void InitialValue() - { - var rp = new ReactiveProperty("ReactiveUI"); - Assert.Equal(rp.Value, "ReactiveUI"); - rp.Subscribe(x => Assert.Equal(x, "ReactiveUI")); - } + [Fact] + public void InitialValue() + { + using var rp = new ReactiveProperty("ReactiveUI"); + Assert.Equal(rp.Value, "ReactiveUI"); + rp.Subscribe(x => Assert.Equal(x, "ReactiveUI")); + } - [Fact] - public void InitialValueSkipCurrent() - { - var rp = new ReactiveProperty("ReactiveUI", true, false); - Assert.Equal(rp.Value, "ReactiveUI"); + [Fact] + public void InitialValueSkipCurrent() + { + using var rp = new ReactiveProperty("ReactiveUI", true, false); + Assert.Equal(rp.Value, "ReactiveUI"); - // current value should be skipped - rp.Subscribe(x => Assert.Equal(x, "ReactiveUI 2")); - rp.Value = "ReactiveUI 2"; - Assert.Equal(rp.Value, "ReactiveUI 2"); - } + // current value should be skipped + rp.Subscribe(x => Assert.Equal(x, "ReactiveUI 2")); + rp.Value = "ReactiveUI 2"; + Assert.Equal(rp.Value, "ReactiveUI 2"); + } - [Fact] - public void SetValueRaisesEvents() - { - var rp = new ReactiveProperty(); - rp.Value.Should().BeNull(); - rp.Value = "ReactiveUI"; - Assert.Equal(rp.Value, "ReactiveUI"); - rp.Subscribe(x => Assert.Equal(x, "ReactiveUI")); - } - - [Fact] - public void ValidationLengthIsCorrectlyHandled() - { - var target = new ReactivePropertyVM(); - IEnumerable? error = default; - target.LengthLessThanFiveProperty - .ObserveErrorChanged - .Subscribe(x => error = x); - - target.LengthLessThanFiveProperty.HasErrors.Should().BeTrue(); - Assert.Equal(error?.OfType().First(), "required"); - - target.LengthLessThanFiveProperty.Value = "a"; - target.LengthLessThanFiveProperty.HasErrors.Should().BeFalse(); - error.Should().BeNull(); - - target.LengthLessThanFiveProperty.Value = "aaaaaa"; - target.LengthLessThanFiveProperty.HasErrors.Should().BeTrue(); - error.Should().NotBeNull(); - Assert.Equal(error?.OfType().First(), "5over"); - - target.LengthLessThanFiveProperty.Value = null; - target.LengthLessThanFiveProperty.HasErrors.Should().BeTrue(); - Assert.Equal(error?.OfType().First(), "required"); - } - - [Fact] - public void ValidationIsRequiredIsCorrectlyHandled() - { - var target = new ReactivePropertyVM(); - var errors = new List(); - target.IsRequiredProperty - .ObserveErrorChanged - .Where(x => x != null) - .Subscribe(errors.Add); - - errors.Count.Should().Be(1); - errors[0]?.Cast().Should().Equal("error!"); - target.IsRequiredProperty.HasErrors.Should().BeTrue(); - - target.IsRequiredProperty.Value = "a"; - errors.Count.Should().Be(1); - target.IsRequiredProperty.HasErrors.Should().BeFalse(); - - target.IsRequiredProperty.Value = null; - errors.Count.Should().Be(2); - errors[1]?.Cast().Should().Equal("error!"); - target.IsRequiredProperty.HasErrors.Should().BeTrue(); - } - - [Fact] - public void ValidationTaskTest() - { - var target = new ReactivePropertyVM(); - var errors = new List(); - target.TaskValidationTestProperty - .ObserveErrorChanged - .Where(x => x != null) - .Subscribe(errors.Add); - errors.Count.Should().Be(1); - errors[0]?.OfType().Should().Equal("required"); - - target.TaskValidationTestProperty.Value = "a"; - target.TaskValidationTestProperty.HasErrors.Should().BeFalse(); - errors.Count.Should().Be(1); - - target.TaskValidationTestProperty.Value = null; - target.TaskValidationTestProperty.HasErrors.Should().BeTrue(); - errors.Count.Should().Be(2); - } - - [Fact] - public void ValidationWithCustomErrorMessage() - { - var target = new ReactivePropertyVM(); - target.CustomValidationErrorMessageProperty.Value = string.Empty; - var errorMessage = target? - .CustomValidationErrorMessageProperty? - .GetErrors(nameof(ReactivePropertyVM.CustomValidationErrorMessageProperty))! - .Cast() - .First(); - - Assert.Equal(errorMessage, "Custom validation error message for CustomValidationErrorMessageProperty"); - } - - [Fact] - public void ValidationWithCustomErrorMessageWithDisplayName() - { - var target = new ReactivePropertyVM(); - target.CustomValidationErrorMessageWithDisplayNameProperty.Value = string.Empty; - var errorMessage = target - .CustomValidationErrorMessageWithDisplayNameProperty? - .GetErrors(nameof(ReactivePropertyVM.CustomValidationErrorMessageWithDisplayNameProperty))! - .Cast() - .First(); - - Assert.Equal(errorMessage, "Custom validation error message for CustomName"); - } - - [Fact] - public void ValidationWithCustomErrorMessageWithResource() - { - var target = new ReactivePropertyVM(); - target.CustomValidationErrorMessageWithResourceProperty.Value = string.Empty; - var errorMessage = target - .CustomValidationErrorMessageWithResourceProperty? - .GetErrors(nameof(ReactivePropertyVM.CustomValidationErrorMessageWithResourceProperty))! - .Cast() - .First(); - - Assert.Equal(errorMessage, "Oops!? FromResource is required."); - } - - [Fact] - public async Task ValidationWithAsyncSuccessCase() - { - var tcs = new TaskCompletionSource(); - var rp = new ReactiveProperty().AddValidationError(_ => tcs.Task); + [Fact] + public void SetValueRaisesEvents() + { + using var rp = new ReactiveProperty(); + rp.Value.Should().BeNull(); + rp.Value = "ReactiveUI"; + Assert.Equal(rp.Value, "ReactiveUI"); + rp.Subscribe(x => Assert.Equal(x, "ReactiveUI")); + } - IEnumerable? error = null; - rp.ObserveErrorChanged.Subscribe(x => error = x); + [Fact] + public void ValidationLengthIsCorrectlyHandled() + { + var target = new ReactivePropertyVM(); + IEnumerable? error = default; + target.LengthLessThanFiveProperty + .ObserveErrorChanged + .Subscribe(x => error = x); + + target.LengthLessThanFiveProperty.HasErrors.Should().BeTrue(); + Assert.Equal(error?.OfType().First(), "required"); + + target.LengthLessThanFiveProperty.Value = "a"; + target.LengthLessThanFiveProperty.HasErrors.Should().BeFalse(); + error.Should().BeNull(); + + target.LengthLessThanFiveProperty.Value = "aaaaaa"; + target.LengthLessThanFiveProperty.HasErrors.Should().BeTrue(); + error.Should().NotBeNull(); + Assert.Equal(error?.OfType().First(), "5over"); + + target.LengthLessThanFiveProperty.Value = null; + target.LengthLessThanFiveProperty.HasErrors.Should().BeTrue(); + Assert.Equal(error?.OfType().First(), "required"); + } + + [Fact] + public void ValidationIsRequiredIsCorrectlyHandled() + { + var target = new ReactivePropertyVM(); + var errors = new List(); + target.IsRequiredProperty + .ObserveErrorChanged + .Where(x => x != null) + .Subscribe(errors.Add); + + errors.Count.Should().Be(1); + errors[0]?.Cast().Should().Equal("error!"); + target.IsRequiredProperty.HasErrors.Should().BeTrue(); + + target.IsRequiredProperty.Value = "a"; + errors.Count.Should().Be(1); + target.IsRequiredProperty.HasErrors.Should().BeFalse(); + + target.IsRequiredProperty.Value = null; + errors.Count.Should().Be(2); + errors[1]?.Cast().Should().Equal("error!"); + target.IsRequiredProperty.HasErrors.Should().BeTrue(); + } - rp.HasErrors.Should().BeFalse(); - error.Should().BeNull(); + [Fact] + public void ValidationTaskTest() + { + var target = new ReactivePropertyVM(); + var errors = new List(); + target.TaskValidationTestProperty + .ObserveErrorChanged + .Where(x => x != null) + .Subscribe(errors.Add); + errors.Count.Should().Be(1); + errors[0]?.OfType().Should().Equal("required"); + + target.TaskValidationTestProperty.Value = "a"; + target.TaskValidationTestProperty.HasErrors.Should().BeFalse(); + errors.Count.Should().Be(1); + + target.TaskValidationTestProperty.Value = null; + target.TaskValidationTestProperty.HasErrors.Should().BeTrue(); + errors.Count.Should().Be(2); + } - rp.Value = "dummy"; - tcs.SetResult(null); - await Task.Yield(); + [Fact] + public void ValidationWithCustomErrorMessage() + { + var target = new ReactivePropertyVM(); + target.CustomValidationErrorMessageProperty.Value = string.Empty; + var errorMessage = target? + .CustomValidationErrorMessageProperty? + .GetErrors(nameof(ReactivePropertyVM.CustomValidationErrorMessageProperty))! + .Cast() + .First(); + + Assert.Equal(errorMessage, "Custom validation error message for CustomValidationErrorMessageProperty"); + } - rp.HasErrors.Should().BeFalse(); - error.Should().BeNull(); - } + [Fact] + public void ValidationWithCustomErrorMessageWithDisplayName() + { + var target = new ReactivePropertyVM(); + target.CustomValidationErrorMessageWithDisplayNameProperty.Value = string.Empty; + var errorMessage = target + .CustomValidationErrorMessageWithDisplayNameProperty? + .GetErrors(nameof(ReactivePropertyVM.CustomValidationErrorMessageWithDisplayNameProperty))! + .Cast() + .First(); + + Assert.Equal(errorMessage, "Custom validation error message for CustomName"); + } - [Fact] - public async Task ValidationWithAsyncFailedCase() - { - var tcs = new TaskCompletionSource(); - var rp = new ReactiveProperty().AddValidationError(_ => tcs.Task); + [Fact] + public void ValidationWithCustomErrorMessageWithResource() + { + var target = new ReactivePropertyVM(); + target.CustomValidationErrorMessageWithResourceProperty.Value = string.Empty; + var errorMessage = target + .CustomValidationErrorMessageWithResourceProperty? + .GetErrors(nameof(ReactivePropertyVM.CustomValidationErrorMessageWithResourceProperty))! + .Cast() + .First(); + + Assert.Equal(errorMessage, "Oops!? FromResource is required."); + } - IEnumerable? error = null; - rp.ObserveErrorChanged.Subscribe(x => error = x); + [Fact] + public async Task ValidationWithAsyncSuccessCase() + { + var tcs = new TaskCompletionSource(); + using var rp = new ReactiveProperty().AddValidationError(_ => tcs.Task); - rp.HasErrors.Should().BeFalse(); - error.Should().BeNull(); + IEnumerable? error = null; + rp.ObserveErrorChanged.Subscribe(x => error = x); - var errorMessage = "error occured!!"; - rp.Value = "dummy"; //--- push value - tcs.SetResult(errorMessage); //--- validation error! - await Task.Delay(10); + rp.HasErrors.Should().BeFalse(); + error.Should().BeNull(); - rp.HasErrors.Should().BeTrue(); - error.Should().NotBeNull(); - error?.Cast().Should().Equal(errorMessage); - rp.GetErrors("Value")?.Cast().Should().Equal(errorMessage); - } + rp.Value = "dummy"; + tcs.SetResult(null); + await Task.Yield(); - [Fact] - public void ValidationWithAsyncThrottleTest() - { - var scheduler = new TestScheduler(); - var rp = new ReactiveProperty() - .AddValidationError(xs => xs - .Throttle(TimeSpan.FromSeconds(1), scheduler) - .Select(x => string.IsNullOrEmpty(x) ? "required" : null)); - - IEnumerable? error = null; - rp.ObserveErrorChanged.Subscribe(x => error = x); - - scheduler.AdvanceTo(TimeSpan.FromMilliseconds(0).Ticks); - rp.Value = string.Empty; - rp.HasErrors.Should().BeFalse(); - error.Should().BeNull(); - - scheduler.AdvanceTo(TimeSpan.FromMilliseconds(300).Ticks); - rp.Value = "a"; - rp.HasErrors.Should().BeFalse(); - error.Should().BeNull(); - - scheduler.AdvanceTo(TimeSpan.FromMilliseconds(700).Ticks); - rp.Value = "b"; - rp.HasErrors.Should().BeFalse(); - error.Should().BeNull(); - - scheduler.AdvanceTo(TimeSpan.FromMilliseconds(1100).Ticks); - rp.Value = string.Empty; - rp.HasErrors.Should().BeFalse(); - error.Should().BeNull(); - - scheduler.AdvanceTo(TimeSpan.FromMilliseconds(2500).Ticks); - rp.HasErrors.Should().BeTrue(); - error.Should().NotBeNull(); - error?.Cast().Should().Equal("required"); - } - - [Fact] - public void ValidationErrorChangedTest() - { - var errors = new List(); - var rprop = new ReactiveProperty() - .AddValidationError(x => string.IsNullOrWhiteSpace(x) ? "error" : null); + rp.HasErrors.Should().BeFalse(); + error.Should().BeNull(); + } - // old version behavior - rprop.ObserveErrorChanged.Skip(1).Subscribe(errors.Add); + [Fact] + public async Task ValidationWithAsyncFailedCase() + { + var tcs = new TaskCompletionSource(); + using var rp = new ReactiveProperty().AddValidationError(_ => tcs.Task); - errors.Count.Should().Be(0); + IEnumerable? error = null; + rp.ObserveErrorChanged.Subscribe(x => error = x); - rprop.Value = "OK"; - errors.Count.Should().Be(1); - errors.Last().Should().BeNull(); + rp.HasErrors.Should().BeFalse(); + error.Should().BeNull(); - rprop.Value = null; - errors.Count.Should().Be(2); - errors.Last()?.OfType().Should().Equal("error"); - } + var errorMessage = "error occured!!"; + rp.Value = "dummy"; //--- push value + tcs.SetResult(errorMessage); //--- validation error! + await Task.Delay(10); - [Fact] - public void ValidationIgnoreInitialErrorAndRefresh() - { - var rp = new ReactiveProperty() - .AddValidationError(x => string.IsNullOrEmpty(x) ? "error" : null, true); + rp.HasErrors.Should().BeTrue(); + error.Should().NotBeNull(); + error?.Cast().Should().Equal(errorMessage); + rp.GetErrors("Value")?.Cast().Should().Equal(errorMessage); + } - rp.HasErrors.Should().BeFalse(); - rp.Refresh(); - rp.HasErrors.Should().BeTrue(); - } + [Fact] + public void ValidationWithAsyncThrottleTest() + { + var scheduler = new TestScheduler(); + using var rp = new ReactiveProperty() + .AddValidationError(xs => xs + .Throttle(TimeSpan.FromSeconds(1), scheduler) + .Select(x => string.IsNullOrEmpty(x) ? "required" : null)); + + IEnumerable? error = null; + rp.ObserveErrorChanged.Subscribe(x => error = x); + + scheduler.AdvanceTo(TimeSpan.FromMilliseconds(0).Ticks); + rp.Value = string.Empty; + rp.HasErrors.Should().BeFalse(); + error.Should().BeNull(); + + scheduler.AdvanceTo(TimeSpan.FromMilliseconds(300).Ticks); + rp.Value = "a"; + rp.HasErrors.Should().BeFalse(); + error.Should().BeNull(); + + scheduler.AdvanceTo(TimeSpan.FromMilliseconds(700).Ticks); + rp.Value = "b"; + rp.HasErrors.Should().BeFalse(); + error.Should().BeNull(); + + scheduler.AdvanceTo(TimeSpan.FromMilliseconds(1100).Ticks); + rp.Value = string.Empty; + rp.HasErrors.Should().BeFalse(); + error.Should().BeNull(); + + scheduler.AdvanceTo(TimeSpan.FromMilliseconds(2500).Ticks); + rp.HasErrors.Should().BeTrue(); + error.Should().NotBeNull(); + error?.Cast().Should().Equal("required"); + } - [Fact] - public void IgnoreInitialErrorAndCheckValidation() - { - var rp = new ReactiveProperty() - .AddValidationError(x => string.IsNullOrEmpty(x) ? "error" : null, true); + [Fact] + public void ValidationErrorChangedTest() + { + var errors = new List(); + using var rprop = new ReactiveProperty() + .AddValidationError(x => string.IsNullOrWhiteSpace(x) ? "error" : null); - rp.HasErrors.Should().BeFalse(); - rp.CheckValidation(); - rp.HasErrors.Should().BeTrue(); - } + // old version behavior + rprop.ObserveErrorChanged.Skip(1).Subscribe(errors.Add); - [Fact] - public void IgnoreInitErrorAndUpdateValue() - { - var rp = new ReactiveProperty() - .AddValidationError(x => string.IsNullOrEmpty(x) ? "error" : null, true); + errors.Count.Should().Be(0); - rp.HasErrors.Should().BeFalse(); - rp.Value = string.Empty; - rp.HasErrors.Should().BeTrue(); - } + rprop.Value = "OK"; + errors.Count.Should().Be(1); + errors.Last().Should().BeNull(); - [Fact] - public void ObserveErrors() - { - var rp = new ReactiveProperty() - .AddValidationError(x => x == null ? "Error" : null); + rprop.Value = null; + errors.Count.Should().Be(2); + errors.Last()?.OfType().Should().Equal("error"); + } + + [Fact] + public void ValidationIgnoreInitialErrorAndRefresh() + { + using var rp = new ReactiveProperty() + .AddValidationError(x => string.IsNullOrEmpty(x) ? "error" : null, true); - var results = new List(); - rp.ObserveErrorChanged.Subscribe(results.Add); - rp.Value = "OK"; + rp.HasErrors.Should().BeFalse(); + rp.Refresh(); + rp.HasErrors.Should().BeTrue(); + } - results.Count.Should().Be(2); - results[0]?.OfType().Should().Equal("Error"); - results[1].Should().BeNull(); - } + [Fact] + public void IgnoreInitialErrorAndCheckValidation() + { + using var rp = new ReactiveProperty() + .AddValidationError(x => string.IsNullOrEmpty(x) ? "error" : null, true); - [Fact] - public void ObserveHasError() - { - var rp = new ReactiveProperty() - .AddValidationError(x => x == null ? "Error" : null); + rp.HasErrors.Should().BeFalse(); + rp.CheckValidation(); + rp.HasErrors.Should().BeTrue(); + } - var results = new List(); - rp.ObserveHasErrors.Subscribe(x => results.Add(x)); - rp.Value = "OK"; + [Fact] + public void IgnoreInitErrorAndUpdateValue() + { + using var rp = new ReactiveProperty() + .AddValidationError(x => string.IsNullOrEmpty(x) ? "error" : null, true); - results.Count.Should().Be(2); - results[0].Should().BeTrue(); - results[1].Should().BeFalse(); - } + rp.HasErrors.Should().BeFalse(); + rp.Value = string.Empty; + rp.HasErrors.Should().BeTrue(); + } - [Fact] - public void CheckValidation() - { - var minValue = 0; - var rp = new ReactiveProperty(0) - .AddValidationError(x => x < minValue ? "Error" : null); - rp.GetErrors("Value").Should().BeNull(); + [Fact] + public void ObserveErrors() + { + using var rp = new ReactiveProperty() + .AddValidationError(x => x == null ? "Error" : null); - minValue = 1; - rp.GetErrors("Value").Should().BeNull(); + var results = new List(); + rp.ObserveErrorChanged.Subscribe(results.Add); + rp.Value = "OK"; - rp.CheckValidation(); - rp.GetErrors("Value")?.OfType().Should().Equal("Error"); - } + results.Count.Should().Be(2); + results[0]?.OfType().Should().Equal("Error"); + results[1].Should().BeNull(); + } - [Fact] - public async Task ValueUpdatesMultipleTimesWithDifferentValues() + [Fact] + public void ObserveHasError() + { + using var rp = new ReactiveProperty() + .AddValidationError(x => x == null ? "Error" : null); + + var results = new List(); + rp.ObserveHasErrors.Subscribe(x => results.Add(x)); + rp.Value = "OK"; + + results.Count.Should().Be(2); + results[0].Should().BeTrue(); + results[1].Should().BeFalse(); + } + + [Fact] + public void CheckValidation() + { + var minValue = 0; + using var rp = new ReactiveProperty(0) + .AddValidationError(x => x < minValue ? "Error" : null); + rp.GetErrors("Value").Should().BeNull(); + + minValue = 1; + rp.GetErrors("Value").Should().BeNull(); + + rp.CheckValidation(); + rp.GetErrors("Value")?.OfType().Should().Equal("Error"); + } + + [Fact] + public async Task ValueUpdatesMultipleTimesWithDifferentValues() + { + using var testSequencer = new TestSequencer(); + using var rp = new ReactiveProperty(0); + var collector = new List(); + rp.Subscribe(async x => { - using var testSequencer = new TestSequencer(); - var rp = new ReactiveProperty(0); - var collector = new List(); - rp.Subscribe(async x => - { - collector.Add(x); - await testSequencer.AdvancePhaseAsync(); - }); - - rp.Value.Should().Be(0); + collector.Add(x); await testSequencer.AdvancePhaseAsync(); - collector.Should().Equal(0); - rp.Value = 1; - rp.Value.Should().Be(1); - await testSequencer.AdvancePhaseAsync(); - collector.Should().Equal(0, 1); - rp.Value = 2; - rp.Value.Should().Be(2); - await testSequencer.AdvancePhaseAsync(); - collector.Should().Equal(0, 1, 2); - rp.Value = 3; - rp.Value.Should().Be(3); - await testSequencer.AdvancePhaseAsync(); - collector.Should().Equal(0, 1, 2, 3); - } + }); + + rp.Value.Should().Be(0); + await testSequencer.AdvancePhaseAsync(); + collector.Should().Equal(0); + rp.Value = 1; + rp.Value.Should().Be(1); + await testSequencer.AdvancePhaseAsync(); + collector.Should().Equal(0, 1); + rp.Value = 2; + rp.Value.Should().Be(2); + await testSequencer.AdvancePhaseAsync(); + collector.Should().Equal(0, 1, 2); + rp.Value = 3; + rp.Value.Should().Be(3); + await testSequencer.AdvancePhaseAsync(); + collector.Should().Equal(0, 1, 2, 3); + } - [Fact] - public async Task Refresh() + [Fact] + public async Task Refresh() + { + using var testSequencer = new TestSequencer(); + using var rp = new ReactiveProperty(0); + var collector = new List(); + rp.Subscribe(async x => { - using var testSequencer = new TestSequencer(); - var rp = new ReactiveProperty(0); - var collector = new List(); - rp.Subscribe(async x => - { - collector.Add(x); - await testSequencer.AdvancePhaseAsync(); - }); - + collector.Add(x); await testSequencer.AdvancePhaseAsync(); - collector.Should().Equal(0); - rp.Refresh(); - await testSequencer.AdvancePhaseAsync(); - collector.Should().Equal(0, 0); - } + }); + + await testSequencer.AdvancePhaseAsync(); + collector.Should().Equal(0); + + // refresh should always produce a value even if it is the same and duplicates are not allowed + rp.Refresh(); + await testSequencer.AdvancePhaseAsync(); + collector.Should().Equal(0, 0); + } - [Fact] - public async Task ValueUpdatesMultipleTimesWithSameValues() + [Fact] + public async Task ValueUpdatesMultipleTimesWithSameValues() + { + using var testSequencer = new TestSequencer(); + using var rp = new ReactiveProperty(0, false, true); + var collector = new List(); + rp.Subscribe(async x => { - using var testSequencer = new TestSequencer(); - var rp = new ReactiveProperty(0, false, true); - var collector = new List(); - rp.Subscribe(async x => - { - collector.Add(x); - await testSequencer.AdvancePhaseAsync(); - }); - - rp.Value.Should().Be(0); - await testSequencer.AdvancePhaseAsync(); - collector.Should().Equal(0); - rp.Value = 0; - rp.Value.Should().Be(0); + collector.Add(x); await testSequencer.AdvancePhaseAsync(); - collector.Should().Equal(0, 0); - rp.Value = 0; - rp.Value.Should().Be(0); - await testSequencer.AdvancePhaseAsync(); - collector.Should().Equal(0, 0, 0); - rp.Value = 0; - rp.Value.Should().Be(0); - await testSequencer.AdvancePhaseAsync(); - collector.Should().Equal(0, 0, 0, 0); - } + }); + + rp.Value.Should().Be(0); + await testSequencer.AdvancePhaseAsync(); + collector.Should().Equal(0); + rp.Value = 0; + rp.Value.Should().Be(0); + await testSequencer.AdvancePhaseAsync(); + collector.Should().Equal(0, 0); + rp.Value = 0; + rp.Value.Should().Be(0); + await testSequencer.AdvancePhaseAsync(); + collector.Should().Equal(0, 0, 0); + rp.Value = 0; + rp.Value.Should().Be(0); + await testSequencer.AdvancePhaseAsync(); + collector.Should().Equal(0, 0, 0, 0); + } - [Fact] - public async Task MultipleSubscribersGetCurrentValue() + [Fact] + public async Task MultipleSubscribersGetCurrentValue() + { + using var testSequencer1 = new TestSequencer(); + using var testSequencer2 = new TestSequencer(); + using var rp = new ReactiveProperty(0); + var collector1 = new List(); + var collector2 = new List(); + rp.Subscribe(async x => { - using var testSequencer1 = new TestSequencer(); - using var testSequencer2 = new TestSequencer(); - var rp = new ReactiveProperty(0); - var collector1 = new List(); - var collector2 = new List(); - rp.Subscribe(async x => - { - collector1.Add(x); - await testSequencer1.AdvancePhaseAsync(); - }); - - rp.Value.Should().Be(0); - await testSequencer1.AdvancePhaseAsync(); - collector1.Should().Equal(0); - rp.Value = 1; - rp.Value.Should().Be(1); - await testSequencer1.AdvancePhaseAsync(); - collector1.Should().Equal(0, 1); - rp.Value = 2; - rp.Value.Should().Be(2); + collector1.Add(x); await testSequencer1.AdvancePhaseAsync(); - collector1.Should().Equal(0, 1, 2); - - // second subscriber - rp.Subscribe(async x => - { - collector2.Add(x); - await testSequencer2.AdvancePhaseAsync(); - }); - rp.Value.Should().Be(2); + }); + + rp.Value.Should().Be(0); + await testSequencer1.AdvancePhaseAsync(); + collector1.Should().Equal(0); + rp.Value = 1; + rp.Value.Should().Be(1); + await testSequencer1.AdvancePhaseAsync(); + collector1.Should().Equal(0, 1); + rp.Value = 2; + rp.Value.Should().Be(2); + await testSequencer1.AdvancePhaseAsync(); + collector1.Should().Equal(0, 1, 2); + + // second subscriber + rp.Subscribe(async x => + { + collector2.Add(x); await testSequencer2.AdvancePhaseAsync(); - collector2.Should().Equal(2); + }); + rp.Value.Should().Be(2); + await testSequencer2.AdvancePhaseAsync(); + collector2.Should().Equal(2); + + rp.Value = 3; + rp.Value.Should().Be(3); + await testSequencer1.AdvancePhaseAsync(); + collector1.Should().Equal(0, 1, 2, 3); + await testSequencer2.AdvancePhaseAsync(); + collector2.Should().Equal(2, 3); + } - rp.Value = 3; - rp.Value.Should().Be(3); - await testSequencer1.AdvancePhaseAsync(); - collector1.Should().Equal(0, 1, 2, 3); - await testSequencer2.AdvancePhaseAsync(); - collector2.Should().Equal(2, 3); - } + [Fact] + public void TestMultipleSubstribers() + { + using var vm = new SubcribeTestViewModel(1000); + vm.SubscriberCount.Should().Be(1000); + vm.StartupTime.Should().BeLessThan(2000); + vm.SubscriberEvents.Should().Be(1000); } } diff --git a/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs b/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs index 181c27ec79..2bb3e119c1 100644 --- a/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs +++ b/src/ReactiveUI/Bindings/Command/CommandBinderImplementation.cs @@ -38,6 +38,10 @@ public class CommandBinderImplementation : ICommandBinderImplementation /// nameof(vmProperty) /// or /// nameof(vmProperty). +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif public IReactiveBinding BindCommand( TViewModel? viewModel, TView view, @@ -89,6 +93,10 @@ public IReactiveBinding BindCommand +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif public IReactiveBinding BindCommand( TViewModel? viewModel, TView view, @@ -119,6 +127,10 @@ public IReactiveBinding BindCommand( IObservable source, TView view, diff --git a/src/ReactiveUI/Expression/ExpressionRewriter.cs b/src/ReactiveUI/Expression/ExpressionRewriter.cs index abf4132de7..76a9a6b91b 100644 --- a/src/ReactiveUI/Expression/ExpressionRewriter.cs +++ b/src/ReactiveUI/Expression/ExpressionRewriter.cs @@ -51,6 +51,10 @@ public override Expression Visit(Expression? node) } } +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif protected override Expression VisitBinary(BinaryExpression node) { if (node.Right is not ConstantExpression) @@ -65,6 +69,10 @@ protected override Expression VisitBinary(BinaryExpression node) return Expression.MakeIndex(left, left.Type.GetRuntimeProperty("Item"), [right]); } +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif protected override Expression VisitUnary(UnaryExpression node) { if (node.NodeType == ExpressionType.ArrayLength && node.Operand is not null) @@ -93,6 +101,10 @@ protected override Expression VisitUnary(UnaryExpression node) } } +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif protected override Expression VisitMethodCall(MethodCallExpression node) { // Rewrite a method call to an indexer as an index expression diff --git a/src/ReactiveUI/Mixins/DependencyResolverMixins.cs b/src/ReactiveUI/Mixins/DependencyResolverMixins.cs index 831fd4d549..65135d5750 100644 --- a/src/ReactiveUI/Mixins/DependencyResolverMixins.cs +++ b/src/ReactiveUI/Mixins/DependencyResolverMixins.cs @@ -71,6 +71,10 @@ public static void InitializeReactiveUI(this IMutableDependencyResolver resolver /// /// The dependency injection resolver to register the Views with. /// The assembly to search using reflection for IViewFor classes. +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif public static void RegisterViewsForViewModels(this IMutableDependencyResolver resolver, Assembly assembly) { resolver.ArgumentNullExceptionThrowIfNull(nameof(resolver)); @@ -108,6 +112,10 @@ private static void RegisterType(IMutableDependencyResolver resolver, TypeInfo t } } +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif private static Func TypeFactory(TypeInfo typeInfo) { var parameterlessConstructor = typeInfo.DeclaredConstructors.FirstOrDefault(ci => ci.IsPublic && ci.GetParameters().Length == 0); @@ -116,6 +124,10 @@ private static Func TypeFactory(TypeInfo typeInfo) : Expression.Lambda>(Expression.New(parameterlessConstructor)).Compile(); } +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif private static void ProcessRegistrationForNamespace(string namespaceName, AssemblyName assemblyName, IMutableDependencyResolver resolver) { var targetType = namespaceName + ".Registrations"; diff --git a/src/ReactiveUI/Platforms/android/ControlFetcherMixin.cs b/src/ReactiveUI/Platforms/android/ControlFetcherMixin.cs index a9549df826..c8bdf4bd33 100644 --- a/src/ReactiveUI/Platforms/android/ControlFetcherMixin.cs +++ b/src/ReactiveUI/Platforms/android/ControlFetcherMixin.cs @@ -156,6 +156,10 @@ public static void WireUpControls(this Activity activity, ResolveStrategy resolv } } +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif internal static IEnumerable GetWireUpMembers(this object @this, ResolveStrategy resolveStrategy) { var members = @this.GetType().GetRuntimeProperties(); @@ -197,6 +201,10 @@ internal static string GetResourceName(this PropertyInfo member) return ret; } +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif private static int GetControlIdByName(Assembly assembly, string? name) { ArgumentNullException.ThrowIfNull(name); diff --git a/src/ReactiveUI/Platforms/android/FlexibleCommandBinder.cs b/src/ReactiveUI/Platforms/android/FlexibleCommandBinder.cs index 2edc651b4f..5f117c04ee 100644 --- a/src/ReactiveUI/Platforms/android/FlexibleCommandBinder.cs +++ b/src/ReactiveUI/Platforms/android/FlexibleCommandBinder.cs @@ -70,6 +70,10 @@ public IDisposable BindCommandToObject(ICommand? command, object? ta /// Command parameter. /// Event name. /// Enabled property name. +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif protected static IDisposable ForEvent(ICommand? command, object? target, IObservable commandParameter, string eventName, PropertyInfo enabledProperty) { ArgumentNullException.ThrowIfNull(command); diff --git a/src/ReactiveUI/Platforms/mac/AutoSuspendHelper.cs b/src/ReactiveUI/Platforms/mac/AutoSuspendHelper.cs index 05ba94eb96..347212749f 100644 --- a/src/ReactiveUI/Platforms/mac/AutoSuspendHelper.cs +++ b/src/ReactiveUI/Platforms/mac/AutoSuspendHelper.cs @@ -29,6 +29,10 @@ public class AutoSuspendHelper : IEnableLogger, IDisposable /// Initializes a new instance of the class. /// /// The application delegate. +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif public AutoSuspendHelper(NSApplicationDelegate appDelegate) { Reflection.ThrowIfMethodsNotOverloaded( diff --git a/src/ReactiveUI/Platforms/mobile-common/ComponentModelTypeConverter.cs b/src/ReactiveUI/Platforms/mobile-common/ComponentModelTypeConverter.cs index 54fe4ab2c8..0fa49f0306 100644 --- a/src/ReactiveUI/Platforms/mobile-common/ComponentModelTypeConverter.cs +++ b/src/ReactiveUI/Platforms/mobile-common/ComponentModelTypeConverter.cs @@ -8,6 +8,10 @@ namespace ReactiveUI; /// /// The component model binding type converter. /// +#if NET6_0_OR_GREATER +[RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] +[RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif public class ComponentModelTypeConverter : IBindingTypeConverter { private readonly MemoizingMRUCache<(Type fromType, Type toType), TypeConverter?> _typeConverterCache = new( diff --git a/src/ReactiveUI/Platforms/net/ComponentModelTypeConverter.cs b/src/ReactiveUI/Platforms/net/ComponentModelTypeConverter.cs index 54a7939727..06bf920dd7 100644 --- a/src/ReactiveUI/Platforms/net/ComponentModelTypeConverter.cs +++ b/src/ReactiveUI/Platforms/net/ComponentModelTypeConverter.cs @@ -10,6 +10,10 @@ namespace ReactiveUI; /// /// Binding Type Converter for component model. /// +#if NET6_0_OR_GREATER +[RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] +[RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif public class ComponentModelTypeConverter : IBindingTypeConverter { private readonly MemoizingMRUCache<(Type fromType, Type toType), TypeConverter?> _typeConverterCache = new ( diff --git a/src/ReactiveUI/Platforms/uikit-common/AutoSuspendHelper.cs b/src/ReactiveUI/Platforms/uikit-common/AutoSuspendHelper.cs index 432cd5cc7c..b5daf8c7fa 100644 --- a/src/ReactiveUI/Platforms/uikit-common/AutoSuspendHelper.cs +++ b/src/ReactiveUI/Platforms/uikit-common/AutoSuspendHelper.cs @@ -31,6 +31,10 @@ public class AutoSuspendHelper : IEnableLogger, IDisposable /// Initializes a new instance of the class. /// /// The uiappdelegate. +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif public AutoSuspendHelper(UIApplicationDelegate appDelegate) { Reflection.ThrowIfMethodsNotOverloaded( diff --git a/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs b/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs index 415c265698..4f6b4266f6 100644 --- a/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs +++ b/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs @@ -26,8 +26,10 @@ public class ReactiveProperty : ReactiveObject, IReactiveProperty private readonly Lazy, IObservable>>> _validatorStore = new(() => []); private readonly int _skipCurrentValue; private readonly bool _isDistinctUntilChanged; + private IObservable? _observable; private T? _value; private IEnumerable? _currentErrors; + private bool _hasSubscribed; /// /// Initializes a new instance of the class. @@ -70,9 +72,10 @@ public ReactiveProperty(T? initialValue, IScheduler? scheduler, bool skipCurrent { _skipCurrentValue = skipCurrentValueOnSubscribe ? 1 : 0; _isDistinctUntilChanged = !allowDuplicateValues; - Value = initialValue; + _value = initialValue; _scheduler = scheduler ?? RxApp.TaskpoolScheduler; _errorChanged = new Lazy>(() => new BehaviorSubject(GetErrors(null))); + GetSubscription(); } /// @@ -290,13 +293,28 @@ public void Refresh() /// A reference to an interface that allows observers to stop receiving notifications before /// the provider has finished sending them. /// - public IDisposable Subscribe(IObserver observer) => - this.WhenAnyValue(vm => vm.Value) - .Merge(_valueRefereshed) - .Skip(_skipCurrentValue) - .ObserveOn(_scheduler) - .Subscribe(observer) - .DisposeWith(_disposables); + public IDisposable Subscribe(IObserver observer) + { + if (observer == null) + { + return Disposable.Empty; + } + + if (IsDisposed) + { + observer.OnCompleted(); + return Disposable.Empty; + } + + if (_hasSubscribed) + { + observer.OnNext(_value); + } + + _hasSubscribed = true; + + return _observable!.Subscribe(observer).DisposeWith(_disposables); + } /// /// Releases unmanaged and - optionally - managed resources. @@ -326,4 +344,20 @@ private void SetValue(T? value) _checkValidation.OnNext(value); } } + + private void GetSubscription() + { + _observable = this.WhenAnyValue(vm => vm.Value) + .Skip(_skipCurrentValue); + + if (_isDistinctUntilChanged) + { + _observable = _observable.DistinctUntilChanged(); + } + + _observable = _observable.Merge(_valueRefereshed) + .Publish() + .RefCount() + .ObserveOn(_scheduler); + } } diff --git a/src/ReactiveUI/ReactiveProperty/ReactivePropertyMixins.cs b/src/ReactiveUI/ReactiveProperty/ReactivePropertyMixins.cs index 0036f27925..73604bef57 100644 --- a/src/ReactiveUI/ReactiveProperty/ReactivePropertyMixins.cs +++ b/src/ReactiveUI/ReactiveProperty/ReactivePropertyMixins.cs @@ -27,6 +27,10 @@ public static class ReactivePropertyMixins /// or /// self. /// +#if NET6_0_OR_GREATER + [RequiresDynamicCode("The method uses reflection and will not work in AOT environments.")] + [RequiresUnreferencedCode("The method uses reflection and will not work in AOT environments.")] +#endif public static ReactiveProperty AddValidation(this ReactiveProperty self, Expression?>> selfSelector) { selfSelector.ArgumentNullExceptionThrowIfNull(nameof(selfSelector));