From 9ec11c42e968769268ae8cef337716cada2d0aef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Jul 2022 06:14:58 +0000 Subject: [PATCH 01/32] Bump terser from 4.8.0 to 4.8.1 in /docs Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1. - [Release notes](https://github.com/terser/terser/releases) - [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md) - [Commits](https://github.com/terser/terser/commits) --- updated-dependencies: - dependency-name: terser dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index cabf966d..2c9f3bae 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9225,9 +9225,9 @@ "dev": true }, "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", + "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", "dev": true, "requires": { "commander": "^2.20.0", From 15e0c0cf344394df44f17ec3ab4fda80c4b42f8d Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 10 Nov 2022 21:36:55 +0900 Subject: [PATCH 02/32] update targetframeworks --- Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj | 2 +- .../ReactiveProperty.NETStandard.csproj | 2 +- .../ReactiveProperty.Platform.Android.csproj | 2 +- .../ReactiveProperty.Platform.Blazor.csproj | 2 +- .../ReactiveProperty.Platform.WPF.csproj | 2 +- .../ReactiveProperty.Platform.iOS.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj b/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj index 9d30cb35..0f06730c 100644 --- a/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj +++ b/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj @@ -2,7 +2,7 @@ ReactiveProperty.Core - netstandard2.0;netcoreapp3.1;net6.0;net472 + netstandard2.0;net6.0;net7.0;net472 ReactiveProperty.Core true key.snk diff --git a/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj b/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj index d0b53828..3dc09e75 100644 --- a/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj +++ b/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj @@ -2,7 +2,7 @@ ReactiveProperty - netstandard2.0;netcoreapp3.1;net6.0;net472 + netstandard2.0;net6.0;net7.0;net472 ReactiveProperty true key.snk diff --git a/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj b/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj index 219d3c5d..3b390135 100644 --- a/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj +++ b/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj @@ -1,7 +1,7 @@  - net6.0-android + net7.0-android ReactiveProperty.XamarinAndroid ReactiveProperty.XamarinAndroid true diff --git a/Source/ReactiveProperty.Platform.Blazor/ReactiveProperty.Platform.Blazor.csproj b/Source/ReactiveProperty.Platform.Blazor/ReactiveProperty.Platform.Blazor.csproj index 8d280b77..84f32bf9 100644 --- a/Source/ReactiveProperty.Platform.Blazor/ReactiveProperty.Platform.Blazor.csproj +++ b/Source/ReactiveProperty.Platform.Blazor/ReactiveProperty.Platform.Blazor.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net7.0 enable enable ReactiveProperty.Blazor diff --git a/Source/ReactiveProperty.Platform.WPF/ReactiveProperty.Platform.WPF.csproj b/Source/ReactiveProperty.Platform.WPF/ReactiveProperty.Platform.WPF.csproj index ab59a58b..940cc80e 100644 --- a/Source/ReactiveProperty.Platform.WPF/ReactiveProperty.Platform.WPF.csproj +++ b/Source/ReactiveProperty.Platform.WPF/ReactiveProperty.Platform.WPF.csproj @@ -1,7 +1,7 @@  - net6.0-windows;netcoreapp3.1;net472 + net6.0-windows;net7.0-windows;net472 ReactiveProperty.WPF true true diff --git a/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj b/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj index f455f7ed..c563d036 100644 --- a/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj +++ b/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj @@ -1,7 +1,7 @@  - net6.0-ios + net7.0-ios ReactiveProperty.XamariniOS ReactiveProperty.XamariniOS true From f9445577145ca16c7d590978300e2a710620fe41 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 10 Nov 2022 21:59:39 +0900 Subject: [PATCH 03/32] update targetframework for sample projects --- .../Blazor/BlazorSample.Shared/BlazorSample.Shared.csproj | 2 +- Samples/Blazor/BlazorServerApp/BlazorServerApp.csproj | 2 +- Samples/Blazor/BlazorWasmApp/BlazorWasmApp.csproj | 6 +++--- Samples/MultiUIThreadApp/MultiUIThreadApp.csproj | 2 +- Samples/Reactive.Todo.Main/Reactive.Todo.Main.csproj | 6 +++--- Samples/Reactive.Todo/Reactive.Todo.csproj | 6 +++--- .../ReactivePropertySamples.WPF.csproj | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Samples/Blazor/BlazorSample.Shared/BlazorSample.Shared.csproj b/Samples/Blazor/BlazorSample.Shared/BlazorSample.Shared.csproj index 7b4045aa..b9e52d80 100644 --- a/Samples/Blazor/BlazorSample.Shared/BlazorSample.Shared.csproj +++ b/Samples/Blazor/BlazorSample.Shared/BlazorSample.Shared.csproj @@ -1,6 +1,6 @@ - net6.0 + net7.0 enable enable diff --git a/Samples/Blazor/BlazorServerApp/BlazorServerApp.csproj b/Samples/Blazor/BlazorServerApp/BlazorServerApp.csproj index 6578083d..ed55e48f 100644 --- a/Samples/Blazor/BlazorServerApp/BlazorServerApp.csproj +++ b/Samples/Blazor/BlazorServerApp/BlazorServerApp.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 enable enable diff --git a/Samples/Blazor/BlazorWasmApp/BlazorWasmApp.csproj b/Samples/Blazor/BlazorWasmApp/BlazorWasmApp.csproj index a493f41c..a87dec2e 100644 --- a/Samples/Blazor/BlazorWasmApp/BlazorWasmApp.csproj +++ b/Samples/Blazor/BlazorWasmApp/BlazorWasmApp.csproj @@ -1,14 +1,14 @@ - net6.0 + net7.0 enable enable - - + + diff --git a/Samples/MultiUIThreadApp/MultiUIThreadApp.csproj b/Samples/MultiUIThreadApp/MultiUIThreadApp.csproj index 24f72655..96e4de23 100644 --- a/Samples/MultiUIThreadApp/MultiUIThreadApp.csproj +++ b/Samples/MultiUIThreadApp/MultiUIThreadApp.csproj @@ -2,7 +2,7 @@ WinExe - net6.0-windows + net7.0-windows true diff --git a/Samples/Reactive.Todo.Main/Reactive.Todo.Main.csproj b/Samples/Reactive.Todo.Main/Reactive.Todo.Main.csproj index d8b2b71f..9b9ea64b 100644 --- a/Samples/Reactive.Todo.Main/Reactive.Todo.Main.csproj +++ b/Samples/Reactive.Todo.Main/Reactive.Todo.Main.csproj @@ -1,12 +1,12 @@  - net6.0-windows + net7.0-windows true Reactive.Todo.Main - - + + diff --git a/Samples/Reactive.Todo/Reactive.Todo.csproj b/Samples/Reactive.Todo/Reactive.Todo.csproj index 159744aa..f7965e85 100644 --- a/Samples/Reactive.Todo/Reactive.Todo.csproj +++ b/Samples/Reactive.Todo/Reactive.Todo.csproj @@ -1,13 +1,13 @@  WinExe - net6.0-windows + net7.0-windows true Reactive.Todo - - + + diff --git a/Samples/ReactivePropertySamples.WPF/ReactivePropertySamples.WPF.csproj b/Samples/ReactivePropertySamples.WPF/ReactivePropertySamples.WPF.csproj index 91269e4e..2b71df44 100644 --- a/Samples/ReactivePropertySamples.WPF/ReactivePropertySamples.WPF.csproj +++ b/Samples/ReactivePropertySamples.WPF/ReactivePropertySamples.WPF.csproj @@ -2,7 +2,7 @@ WinExe - net6.0-windows + net7.0-windows true From 942bbc4ef6677bbca3eb01bd9b4e416afb2db0f4 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 10 Nov 2022 22:21:34 +0900 Subject: [PATCH 04/32] update targetframework for benchmark projects --- Benchmark/Benchmark.Current/Benchmark.Current.csproj | 4 ++-- Benchmark/Benchmark.V6/BasicUsages.cs | 6 ++++-- Benchmark/Benchmark.V6/Benchmark.V6.csproj | 4 ++-- Benchmark/Benchmark.V7/Benchmark.V7.csproj | 4 ++-- .../FromEventBenchmark.Rp/FromEventBenchmark.Rp.csproj | 4 ++-- .../FromEventBenchmark.Rx/FromEventBenchmark.Rx.csproj | 4 ++-- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Benchmark/Benchmark.Current/Benchmark.Current.csproj b/Benchmark/Benchmark.Current/Benchmark.Current.csproj index 4af32f7d..04d87a5c 100644 --- a/Benchmark/Benchmark.Current/Benchmark.Current.csproj +++ b/Benchmark/Benchmark.Current/Benchmark.Current.csproj @@ -2,11 +2,11 @@ Exe - net6.0 + net7.0 - + diff --git a/Benchmark/Benchmark.V6/BasicUsages.cs b/Benchmark/Benchmark.V6/BasicUsages.cs index 1019794a..2722515c 100644 --- a/Benchmark/Benchmark.V6/BasicUsages.cs +++ b/Benchmark/Benchmark.V6/BasicUsages.cs @@ -18,23 +18,25 @@ public class BasicUsages public ReactivePropertySlim CreateReactivePropertySlimInstance() => new ReactivePropertySlim(); [Benchmark] - public void BasicForReactiveProperty() + public ReadOnlyReactiveProperty BasicForReactiveProperty() { var rp = new ReactiveProperty(); var rrp = rp.ToReadOnlyReactiveProperty(); rp.Value = "xxxx"; rp.Dispose(); + return rrp; } [Benchmark] - public void BasicForReactivePropertySlim() + public ReadOnlyReactivePropertySlim BasicForReactivePropertySlim() { var rp = new ReactivePropertySlim(); var rrp = rp.ToReadOnlyReactivePropertySlim(); rp.Value = "xxxx"; rp.Dispose(); + return rrp; } [Benchmark] diff --git a/Benchmark/Benchmark.V6/Benchmark.V6.csproj b/Benchmark/Benchmark.V6/Benchmark.V6.csproj index 6f8dc8c1..2bd60c2b 100644 --- a/Benchmark/Benchmark.V6/Benchmark.V6.csproj +++ b/Benchmark/Benchmark.V6/Benchmark.V6.csproj @@ -2,12 +2,12 @@ Exe - net6.0-windows + net7.0-windows ReactivePropertyBenchmark - + diff --git a/Benchmark/Benchmark.V7/Benchmark.V7.csproj b/Benchmark/Benchmark.V7/Benchmark.V7.csproj index 31115839..fea718cb 100644 --- a/Benchmark/Benchmark.V7/Benchmark.V7.csproj +++ b/Benchmark/Benchmark.V7/Benchmark.V7.csproj @@ -2,12 +2,12 @@ Exe - net6.0 + net7.0 - + diff --git a/Benchmark/FromEventBenchmark.Rp/FromEventBenchmark.Rp.csproj b/Benchmark/FromEventBenchmark.Rp/FromEventBenchmark.Rp.csproj index 35994e51..5ff0d97a 100644 --- a/Benchmark/FromEventBenchmark.Rp/FromEventBenchmark.Rp.csproj +++ b/Benchmark/FromEventBenchmark.Rp/FromEventBenchmark.Rp.csproj @@ -2,12 +2,12 @@ Exe - net5.0 + net7.0 FromEventBenchmark - + diff --git a/Benchmark/FromEventBenchmark.Rx/FromEventBenchmark.Rx.csproj b/Benchmark/FromEventBenchmark.Rx/FromEventBenchmark.Rx.csproj index e5dae1d8..fa976643 100644 --- a/Benchmark/FromEventBenchmark.Rx/FromEventBenchmark.Rx.csproj +++ b/Benchmark/FromEventBenchmark.Rx/FromEventBenchmark.Rx.csproj @@ -2,12 +2,12 @@ Exe - net5.0 + net7.0 FromEventBenchmark - + From 63f5689e251ca7a2dc04fce0f140b7f339768b92 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 10 Nov 2022 22:34:42 +0900 Subject: [PATCH 05/32] update lang version --- Source/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index ff7dcdec..2a07cff8 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -11,7 +11,7 @@ ReactivePropertyIcon_100x100.png https://github.com/runceel/ReactiveProperty git - 10.0 + 11.0 true true true From 9bad83d29f2e5e35486083ea28398c8ee9ff17d8 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 10 Nov 2022 22:34:57 +0900 Subject: [PATCH 06/32] increment version number --- Source/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index 2a07cff8..90f52967 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -1,7 +1,7 @@ Reactive.Bindings - 8.1.2 + 8.2.0 neuecc xin9le okazuki https://github.com/runceel/ReactiveProperty rx mvvm async rx-main reactive From cd3be5920e5eeec355fcc0be77130c5c6c8db74d Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 10 Nov 2022 22:50:12 +0900 Subject: [PATCH 07/32] update to .NET 7 for github actions --- .github/workflows/build-and-publish.yml | 2 +- .github/workflows/dotnet-core-unit-testing.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 4b0efee2..a3d28e49 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -7,7 +7,7 @@ on: - 'docs/**' env: - DOTNET_VERSION: 6.0.x + DOTNET_VERSION: 7.0.x jobs: build: diff --git a/.github/workflows/dotnet-core-unit-testing.yml b/.github/workflows/dotnet-core-unit-testing.yml index 870cbb84..dc56f82d 100644 --- a/.github/workflows/dotnet-core-unit-testing.yml +++ b/.github/workflows/dotnet-core-unit-testing.yml @@ -5,7 +5,7 @@ on: branches: [ main ] env: - DOTNET_VERSION: 6.0.x + DOTNET_VERSION: 7.0.x jobs: build: From 5876435f97c4d1cc6414394567cac8e6e5147eea Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 10 Nov 2022 23:13:07 +0900 Subject: [PATCH 08/32] update to .net 7.0 for test projects --- .../ReactiveProperty.Blazor.Tests.csproj | 15 +++++++++------ .../ReactiveProperty.NETStandard.Tests.csproj | 10 +++++----- .../ReactiveProperty.WPF.Tests.csproj | 10 +++++----- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Test/ReactiveProperty.Blazor.Tests/ReactiveProperty.Blazor.Tests.csproj b/Test/ReactiveProperty.Blazor.Tests/ReactiveProperty.Blazor.Tests.csproj index 11fdd174..abc2b30d 100644 --- a/Test/ReactiveProperty.Blazor.Tests/ReactiveProperty.Blazor.Tests.csproj +++ b/Test/ReactiveProperty.Blazor.Tests/ReactiveProperty.Blazor.Tests.csproj @@ -1,18 +1,21 @@ - net6.0 + net7.0 enable false - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Test/ReactiveProperty.NETStandard.Tests/ReactiveProperty.NETStandard.Tests.csproj b/Test/ReactiveProperty.NETStandard.Tests/ReactiveProperty.NETStandard.Tests.csproj index 02de5986..5ae68440 100644 --- a/Test/ReactiveProperty.NETStandard.Tests/ReactiveProperty.NETStandard.Tests.csproj +++ b/Test/ReactiveProperty.NETStandard.Tests/ReactiveProperty.NETStandard.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 ReactiveProperty.Tests ReactiveProperty.Tests true @@ -11,17 +11,17 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + diff --git a/Test/ReactiveProperty.WPF.Tests/ReactiveProperty.WPF.Tests.csproj b/Test/ReactiveProperty.WPF.Tests/ReactiveProperty.WPF.Tests.csproj index 1b7b077d..e880cab5 100644 --- a/Test/ReactiveProperty.WPF.Tests/ReactiveProperty.WPF.Tests.csproj +++ b/Test/ReactiveProperty.WPF.Tests/ReactiveProperty.WPF.Tests.csproj @@ -1,7 +1,7 @@  - net6.0-windows + net7.0-windows false true ReactiveProperty.Tests @@ -10,13 +10,13 @@ - + - - - + + + From c36c02a14c1f36e4cca1cd201f79c3d32316bd6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 14:19:34 +0000 Subject: [PATCH 09/32] Bump minimatch from 3.0.4 to 3.1.2 in /docs Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2. - [Release notes](https://github.com/isaacs/minimatch/releases) - [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2) --- updated-dependencies: - dependency-name: minimatch dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 2c9f3bae..15f7cebb 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -6343,9 +6343,9 @@ "dev": true }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" From 728bcf07d4cdc187f1b1a0d32876136f0be9fc36 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Fri, 11 Nov 2022 11:28:15 +0900 Subject: [PATCH 10/32] Update README.md --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 53326e7e..252e0c94 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # ReactiveProperty -ReactiveProperty provides MVVM and asynchronous support features under Reactive Extensions. Target framework is .NET 6.0, .NET Core 3.1, .NET Framework 4.7.2 and .NET Standard 2.0. +ReactiveProperty provides MVVM and asynchronous support features under Reactive Extensions. Target framework is .NET 6.0+, .NET Framework 4.7.2 and .NET Standard 2.0. ![](https://img.shields.io/nuget/v/ReactiveProperty.svg) ![](https://img.shields.io/nuget/dt/ReactiveProperty.svg) @@ -137,10 +137,15 @@ ReactiveProperty doesn't provide base class by ViewModel, which means that React |Package Id|Version and downloads|Description| |----|----|----| -|[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](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.| |[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.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 6 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. | + +Following packages are maitanance phase. + +|Package Id|Version and downloads|Description| +|----|----|----| |[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.| From e28a25b7550af9322f3303d86407689aa15ba5ad Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Fri, 11 Nov 2022 11:29:36 +0900 Subject: [PATCH 11/32] Update ReactiveProperty.Platform.Android.csproj --- .../ReactiveProperty.Platform.Android.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj b/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj index 3b390135..26e23091 100644 --- a/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj +++ b/Source/ReactiveProperty.Platform.Android/ReactiveProperty.Platform.Android.csproj @@ -6,7 +6,7 @@ ReactiveProperty.XamarinAndroid true key.snk - ReactiveProperty.XamarinAndroid provides many useful extension methods for Xamarin.Android that can be used with ReactiveProperty. + ReactiveProperty.XamarinAndroid provides many useful extension methods for .NET for Android that can be used with ReactiveProperty. disable From e2bc914e53a3e6a02ca5457bf79f40141d8101fb Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Fri, 11 Nov 2022 11:30:13 +0900 Subject: [PATCH 12/32] Update ReactiveProperty.Platform.iOS.csproj --- .../ReactiveProperty.Platform.iOS.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj b/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj index c563d036..10d21256 100644 --- a/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj +++ b/Source/ReactiveProperty.Platform.iOS/ReactiveProperty.Platform.iOS.csproj @@ -6,7 +6,7 @@ ReactiveProperty.XamariniOS true key.snk - ReactiveProperty.XamariniOS provides many useful extension methods for Xamarin.iOS that can be used with ReactiveProperty. + ReactiveProperty.XamariniOS provides many useful extension methods for .NET for iOS that can be used with ReactiveProperty. disable From b9ca19a0029ae52f0e3ad4eeb9b22c53f10a9c3c Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Fri, 11 Nov 2022 11:30:48 +0900 Subject: [PATCH 13/32] Update ReactiveProperty.NETStandard.csproj --- .../ReactiveProperty.NETStandard.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj b/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj index 3dc09e75..fef111d7 100644 --- a/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj +++ b/Source/ReactiveProperty.NETStandard/ReactiveProperty.NETStandard.csproj @@ -6,7 +6,7 @@ ReactiveProperty true key.snk - ReactiveProperty is MVVM and Asynchronous Extensions for Reactive Extensions(System.Reactive). Target platform is .NET Standard 2.0. + ReactiveProperty is MVVM and Asynchronous Extensions for Reactive Extensions(System.Reactive). @@ -18,4 +18,4 @@ - \ No newline at end of file + From d0576e0b34c81dbbc40f8d1708ff222e552b0fee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Dec 2022 20:15:17 +0000 Subject: [PATCH 14/32] Bump decode-uri-component from 0.2.0 to 0.2.2 in /docs Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2. - [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases) - [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2) --- updated-dependencies: - dependency-name: decode-uri-component dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 15f7cebb..08a36110 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -3665,9 +3665,9 @@ "dev": true }, "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", "dev": true }, "decompress-response": { From f45081b9c7d5dd66e89fa0cd71b84f2dc1e535eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Dec 2022 22:07:36 +0000 Subject: [PATCH 15/32] Bump qs from 6.5.2 to 6.5.3 in /docs Bumps [qs](https://github.com/ljharb/qs) from 6.5.2 to 6.5.3. - [Release notes](https://github.com/ljharb/qs/releases) - [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md) - [Commits](https://github.com/ljharb/qs/compare/v6.5.2...v6.5.3) --- updated-dependencies: - dependency-name: qs dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 08a36110..1b5bb037 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8156,9 +8156,9 @@ }, "dependencies": { "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", "dev": true } } From 2eb930fc1cbb97aa4891db8e85019c80cf0ac4e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Dec 2022 22:07:36 +0000 Subject: [PATCH 16/32] Bump express from 4.17.1 to 4.18.2 in /docs Bumps [express](https://github.com/expressjs/express) from 4.17.1 to 4.18.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.17.1...4.18.2) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/package-lock.json | 558 ++++++++++++++++++++++------------------- 1 file changed, 297 insertions(+), 261 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 08a36110..3e874771 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -2181,47 +2181,6 @@ "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", "dev": true }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" - }, - "dependencies": { - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, "bonjour": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", @@ -2708,6 +2667,16 @@ } } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "call-me-maybe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", @@ -3135,15 +3104,6 @@ "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", "dev": true }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dev": true, - "requires": { - "safe-buffer": "5.1.2" - } - }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", @@ -3159,12 +3119,6 @@ "safe-buffer": "~5.1.1" } }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true - }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -3831,12 +3785,6 @@ "minimalistic-assert": "^1.0.0" } }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, "detect-node": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", @@ -4331,47 +4279,99 @@ } }, "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", "dev": true, "requires": { - "accepts": "~1.3.7", + "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.4.0", + "cookie": "0.5.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "~1.1.2", + "depd": "2.0.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "~1.1.2", + "finalhandler": "1.2.0", "fresh": "0.5.2", + "http-errors": "2.0.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "dependencies": { + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "dev": true }, "debug": { @@ -4383,10 +4383,188 @@ "ms": "2.0.0" } }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true } } @@ -4592,38 +4770,6 @@ } } }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, "find-cache-dir": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", @@ -4689,12 +4835,6 @@ "mime-types": "^2.1.12" } }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true - }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -4778,6 +4918,25 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "dependencies": { + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + } + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -5194,27 +5353,6 @@ "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", "dev": true }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } - } - }, "http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -6724,15 +6862,6 @@ "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, "on-headers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", @@ -7724,16 +7853,6 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, - "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", - "dev": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" - } - }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -7828,12 +7947,6 @@ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", "dev": true }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true - }, "query-string": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", @@ -7888,26 +8001,6 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dev": true, - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true - } - } - }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -8377,58 +8470,6 @@ } } }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } - } - }, "serialize-javascript": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", @@ -8494,18 +8535,6 @@ } } }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" - } - }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -8541,12 +8570,6 @@ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", "dev": true }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true - }, "sha.js": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", @@ -8572,6 +8595,25 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "dependencies": { + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + } + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -9394,12 +9436,6 @@ "repeat-string": "^1.6.1" } }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true - }, "toml": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", From 15385b86927c0d5ced70aedbb90dde50c23f6447 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Dec 2022 02:00:20 +0000 Subject: [PATCH 17/32] Bump ansi-html and webpack-dev-server in /docs Removes [ansi-html](https://github.com/Tjatse/ansi-html). It's no longer used after updating ancestor dependency [webpack-dev-server](https://github.com/webpack/webpack-dev-server). These dependencies need to be updated together. Removes `ansi-html` Updates `webpack-dev-server` from 3.11.0 to 3.11.3 - [Release notes](https://github.com/webpack/webpack-dev-server/releases) - [Changelog](https://github.com/webpack/webpack-dev-server/blob/v3.11.3/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-server/compare/v3.11.0...v3.11.3) --- updated-dependencies: - dependency-name: ansi-html dependency-type: indirect - dependency-name: webpack-dev-server dependency-type: indirect ... Signed-off-by: dependabot[bot] --- docs/package-lock.json | 214 +++++++++++++++++++---------------------- 1 file changed, 99 insertions(+), 115 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 88071c7e..ae82ffb2 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -1804,10 +1804,10 @@ "type-fest": "^0.11.0" } }, - "ansi-html": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", - "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", "dev": true }, "ansi-regex": { @@ -4194,15 +4194,6 @@ "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", "dev": true }, - "eventsource": { - "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" - } - }, "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -4693,15 +4684,6 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -5353,6 +5335,12 @@ "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", "dev": true }, + "http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, "http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -5970,12 +5958,6 @@ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, - "json3": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", - "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==", - "dev": true - }, "json5": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", @@ -6902,15 +6884,6 @@ "last-call-webpack-plugin": "^3.0.0" } }, - "original": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", - "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", - "dev": true, - "requires": { - "url-parse": "^1.4.3" - } - }, "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -8430,23 +8403,6 @@ "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", "dev": true }, - "selfsigned": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", - "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", - "dev": true, - "requires": { - "node-forge": "0.9.0" - }, - "dependencies": { - "node-forge": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", - "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", - "dev": true - } - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -8771,51 +8727,6 @@ } } }, - "sockjs": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", - "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", - "dev": true, - "requires": { - "faye-websocket": "^0.10.0", - "uuid": "^3.4.0", - "websocket-driver": "0.6.5" - } - }, - "sockjs-client": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", - "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", - "dev": true, - "requires": { - "debug": "^3.2.5", - "eventsource": "^1.0.7", - "faye-websocket": "~0.11.1", - "inherits": "^2.0.3", - "json3": "^3.3.2", - "url-parse": "^1.4.3" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - } - } - }, "sort-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", @@ -10354,12 +10265,12 @@ } }, "webpack-dev-server": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", - "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.3.tgz", + "integrity": "sha512-3x31rjbEQWKMNzacUZRE6wXvUFuGpH7vr0lIEbYpMAG9BOxi0928QU1BBswOAP3kg3H1O4hiS+sq4YyAn6ANnA==", "dev": true, "requires": { - "ansi-html": "0.0.7", + "ansi-html-community": "0.0.8", "bonjour": "^3.5.0", "chokidar": "^2.1.8", "compression": "^1.7.4", @@ -10379,11 +10290,11 @@ "p-retry": "^3.0.1", "portfinder": "^1.0.26", "schema-utils": "^1.0.0", - "selfsigned": "^1.10.7", + "selfsigned": "^1.10.8", "semver": "^6.3.0", "serve-index": "^1.9.1", - "sockjs": "0.3.20", - "sockjs-client": "1.4.0", + "sockjs": "^0.3.21", + "sockjs-client": "^1.5.0", "spdy": "^4.0.2", "strip-ansi": "^3.0.1", "supports-color": "^6.1.0", @@ -10394,12 +10305,33 @@ "yargs": "^13.3.2" }, "dependencies": { + "eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "dev": true + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, "is-absolute-url": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", "dev": true }, + "node-forge": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "dev": true + }, "schema-utils": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", @@ -10411,12 +10343,56 @@ "ajv-keywords": "^3.1.0" } }, + "selfsigned": { + "version": "1.10.14", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.14.tgz", + "integrity": "sha512-lkjaiAye+wBZDCBsu5BGi0XiLRxeUlsGod5ZP924CRSEoGuZAw/f7y9RKu28rwTfiHVhdavhB0qH0INV6P1lEA==", + "dev": true, + "requires": { + "node-forge": "^0.10.0" + } + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "sockjs-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", + "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, "supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", @@ -10425,6 +10401,23 @@ "requires": { "has-flag": "^3.0.0" } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } } } }, @@ -10481,15 +10474,6 @@ "wrap-ansi": "^5.1.0" } }, - "websocket-driver": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", - "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", - "dev": true, - "requires": { - "websocket-extensions": ">=0.1.1" - } - }, "websocket-extensions": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", From b969949d00cdc8b76376b9484a57a7fd0138e33d Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Wed, 4 Jan 2023 16:19:09 +0900 Subject: [PATCH 18/32] move AsyncReactiveCommand to Core project --- .../AsyncReactiveCommand.cs | 102 ++++++++++-------- .../Internals/ImmutableList.cs | 59 ++++++++++ .../Notifiers/MessageBroker.cs | 57 +--------- 3 files changed, 120 insertions(+), 98 deletions(-) rename Source/{ReactiveProperty.NETStandard => ReactiveProperty.Core}/AsyncReactiveCommand.cs (79%) create mode 100644 Source/ReactiveProperty.Core/Internals/ImmutableList.cs diff --git a/Source/ReactiveProperty.NETStandard/AsyncReactiveCommand.cs b/Source/ReactiveProperty.Core/AsyncReactiveCommand.cs similarity index 79% rename from Source/ReactiveProperty.NETStandard/AsyncReactiveCommand.cs rename to Source/ReactiveProperty.Core/AsyncReactiveCommand.cs index 6e224873..2504bc79 100644 --- a/Source/ReactiveProperty.NETStandard/AsyncReactiveCommand.cs +++ b/Source/ReactiveProperty.Core/AsyncReactiveCommand.cs @@ -1,7 +1,8 @@ using System; -using System.Reactive.Linq; +using System.ComponentModel; using System.Threading.Tasks; using System.Windows.Input; +using Reactive.Bindings.Internals; namespace Reactive.Bindings; @@ -72,24 +73,19 @@ public class AsyncReactiveCommand : ICommand, IDisposable /// public event EventHandler? CanExecuteChanged; - private readonly object gate = new(); - private readonly IReactiveProperty canExecute; - private readonly IDisposable sourceSubscription; - private bool isCanExecute; - private bool isDisposed = false; - private Notifiers.ImmutableList> asyncActions = Notifiers.ImmutableList>.Empty; + private readonly object _gate = new(); + private readonly IReactiveProperty _canExecute; + private readonly IReadOnlyReactiveProperty? _canExecuteSource; + private bool _isCanExecute; + private readonly Action _disposeAction; + private bool _isDisposed = false; + private ImmutableList> _asyncActions = ImmutableList>.Empty; /// /// CanExecute is automatically changed when executing to false and finished to true. /// - public AsyncReactiveCommand() + public AsyncReactiveCommand() : this(new ReactivePropertySlim(true)) { - canExecute = new ReactivePropertySlim(true); - sourceSubscription = canExecute.Subscribe(x => - { - isCanExecute = x; - CanExecuteChanged?.Invoke(this, EventArgs.Empty); - }); } /// @@ -105,14 +101,30 @@ public AsyncReactiveCommand(IObservable canExecuteSource) /// public AsyncReactiveCommand(IObservable canExecuteSource, IReactiveProperty? sharedCanExecute) { - canExecute = sharedCanExecute ?? new ReactivePropertySlim(true); - sourceSubscription = canExecute.CombineLatest(canExecuteSource, (x, y) => x && y) - .DistinctUntilChanged() - .Subscribe(x => - { - isCanExecute = x; - CanExecuteChanged?.Invoke(this, EventArgs.Empty); - }); + void canExecute_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is not nameof(IReactiveProperty.Value)) return; + + var newValue = _canExecute.Value && this._canExecuteSource!.Value; + if (newValue == _isCanExecute) return; + + _isCanExecute = newValue; + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + + _canExecute = sharedCanExecute ?? new ReactivePropertySlim(true); + _canExecute.PropertyChanged += canExecute_PropertyChanged; + _canExecuteSource = canExecuteSource.ToReadOnlyReactivePropertySlim(); + _canExecuteSource.PropertyChanged += canExecute_PropertyChanged; + + _isCanExecute = _canExecute.Value && _canExecuteSource.Value; + + _disposeAction = () => + { + _canExecute.PropertyChanged -= canExecute_PropertyChanged; + _canExecuteSource.PropertyChanged -= canExecute_PropertyChanged; + _canExecuteSource?.Dispose(); + }; } /// @@ -121,12 +133,18 @@ public AsyncReactiveCommand(IObservable canExecuteSource, IReactivePropert /// public AsyncReactiveCommand(IReactiveProperty sharedCanExecute) { - canExecute = sharedCanExecute; - sourceSubscription = canExecute.Subscribe(x => + void canExecute_PropertyChanged(object sender, PropertyChangedEventArgs e) { - isCanExecute = x; + if (e.PropertyName is not nameof(IReactiveProperty.Value)) return; + + _isCanExecute = _canExecute.Value; CanExecuteChanged?.Invoke(this, EventArgs.Empty); - }); + } + + _canExecute = sharedCanExecute; + _canExecute.PropertyChanged += canExecute_PropertyChanged; + _isCanExecute = _canExecute.Value; + _disposeAction = () => _canExecute.PropertyChanged -= canExecute_PropertyChanged; } /// @@ -134,7 +152,7 @@ public AsyncReactiveCommand(IReactiveProperty sharedCanExecute) /// public bool CanExecute() { - return isDisposed ? false : isCanExecute; + return _isDisposed ? false : _isCanExecute; } /// @@ -142,7 +160,7 @@ public bool CanExecute() /// bool ICommand.CanExecute(object? parameter) { - return isDisposed ? false : isCanExecute; + return _isDisposed ? false : _isCanExecute; } /// @@ -155,10 +173,10 @@ bool ICommand.CanExecute(object? parameter) /// public async Task ExecuteAsync(T parameter) { - if (isCanExecute) + if (_isCanExecute) { - canExecute.Value = false; - var a = asyncActions.Data; + _canExecute.Value = false; + var a = _asyncActions.Data; if (a.Length == 1) { @@ -169,7 +187,7 @@ public async Task ExecuteAsync(T parameter) } finally { - canExecute.Value = true; + _canExecute.Value = true; } } else @@ -186,7 +204,7 @@ public async Task ExecuteAsync(T parameter) } finally { - canExecute.Value = true; + _canExecute.Value = true; } } } @@ -202,9 +220,9 @@ public async Task ExecuteAsync(T parameter) /// public IDisposable Subscribe(Func asyncAction) { - lock (gate) + lock (_gate) { - asyncActions = asyncActions.Add(asyncAction); + _asyncActions = _asyncActions.Add(asyncAction); } return new Subscription(this, asyncAction); @@ -215,16 +233,16 @@ public IDisposable Subscribe(Func asyncAction) /// public void Dispose() { - if (isDisposed) + if (_isDisposed) { return; } - isDisposed = true; - sourceSubscription.Dispose(); - if (isCanExecute) + _isDisposed = true; + _disposeAction(); + if (_isCanExecute) { - isCanExecute = false; + _isCanExecute = false; CanExecuteChanged?.Invoke(this, EventArgs.Empty); } } @@ -242,9 +260,9 @@ public Subscription(AsyncReactiveCommand parent, Func asyncAction) public void Dispose() { - lock (parent.gate) + lock (parent._gate) { - parent.asyncActions = parent.asyncActions.Remove(asyncAction); + parent._asyncActions = parent._asyncActions.Remove(asyncAction); } } } diff --git a/Source/ReactiveProperty.Core/Internals/ImmutableList.cs b/Source/ReactiveProperty.Core/Internals/ImmutableList.cs new file mode 100644 index 00000000..49f60d8d --- /dev/null +++ b/Source/ReactiveProperty.Core/Internals/ImmutableList.cs @@ -0,0 +1,59 @@ +using System; + +namespace Reactive.Bindings.Internals; + +// ImmutableList is from Rx internal +internal class ImmutableList +{ + public static readonly ImmutableList Empty = new(); + + T[] data; + + public T[] Data + { + get { return data; } + } + + ImmutableList() + { + data = new T[0]; + } + + public ImmutableList(T[] data) + { + this.data = data; + } + + public ImmutableList Add(T value) + { + var newData = new T[data.Length + 1]; + Array.Copy(data, newData, data.Length); + newData[data.Length] = value; + return new ImmutableList(newData); + } + + public ImmutableList Remove(T value) + { + var i = IndexOf(value); + if (i < 0) return this; + + var length = data.Length; + if (length == 1) return Empty; + + var newData = new T[length - 1]; + + Array.Copy(data, 0, newData, 0, i); + Array.Copy(data, i + 1, newData, i, length - i - 1); + + return new ImmutableList(newData); + } + + public int IndexOf(T value) + { + for (var i = 0; i < data.Length; ++i) + { + if (object.Equals(data[i], value)) return i; + } + return -1; + } +} diff --git a/Source/ReactiveProperty.NETStandard/Notifiers/MessageBroker.cs b/Source/ReactiveProperty.NETStandard/Notifiers/MessageBroker.cs index ddb0f64e..c39f193a 100644 --- a/Source/ReactiveProperty.NETStandard/Notifiers/MessageBroker.cs +++ b/Source/ReactiveProperty.NETStandard/Notifiers/MessageBroker.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reactive.Linq; using System.Threading.Tasks; +using Reactive.Bindings.Internals; namespace Reactive.Bindings.Notifiers; @@ -314,59 +315,3 @@ public static IObservable ToObservable(this IMessageSubscriber messageSubs }); } } - -// ImmutableList is from Rx internal -internal class ImmutableList -{ - public static readonly ImmutableList Empty = new(); - - T[] data; - - public T[] Data - { - get { return data; } - } - - ImmutableList() - { - data = new T[0]; - } - - public ImmutableList(T[] data) - { - this.data = data; - } - - public ImmutableList Add(T value) - { - var newData = new T[data.Length + 1]; - Array.Copy(data, newData, data.Length); - newData[data.Length] = value; - return new ImmutableList(newData); - } - - public ImmutableList Remove(T value) - { - var i = IndexOf(value); - if (i < 0) return this; - - var length = data.Length; - if (length == 1) return Empty; - - var newData = new T[length - 1]; - - Array.Copy(data, 0, newData, 0, i); - Array.Copy(data, i + 1, newData, i, length - i - 1); - - return new ImmutableList(newData); - } - - public int IndexOf(T value) - { - for (var i = 0; i < data.Length; ++i) - { - if (object.Equals(data[i], value)) return i; - } - return -1; - } -} From a3139c3733b8b8bf0200ba36c55c31269843f4b1 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Wed, 4 Jan 2023 19:14:11 +0900 Subject: [PATCH 19/32] Remove unnecessary this --- Source/ReactiveProperty.Core/AsyncReactiveCommand.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/ReactiveProperty.Core/AsyncReactiveCommand.cs b/Source/ReactiveProperty.Core/AsyncReactiveCommand.cs index 2504bc79..db05ddf3 100644 --- a/Source/ReactiveProperty.Core/AsyncReactiveCommand.cs +++ b/Source/ReactiveProperty.Core/AsyncReactiveCommand.cs @@ -101,11 +101,11 @@ public AsyncReactiveCommand(IObservable canExecuteSource) /// public AsyncReactiveCommand(IObservable canExecuteSource, IReactiveProperty? sharedCanExecute) { - void canExecute_PropertyChanged(object sender, PropertyChangedEventArgs e) + void canExecute_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName is not nameof(IReactiveProperty.Value)) return; - var newValue = _canExecute.Value && this._canExecuteSource!.Value; + var newValue = _canExecute.Value && _canExecuteSource!.Value; if (newValue == _isCanExecute) return; _isCanExecute = newValue; @@ -123,7 +123,7 @@ void canExecute_PropertyChanged(object sender, PropertyChangedEventArgs e) { _canExecute.PropertyChanged -= canExecute_PropertyChanged; _canExecuteSource.PropertyChanged -= canExecute_PropertyChanged; - _canExecuteSource?.Dispose(); + _canExecuteSource.Dispose(); }; } @@ -133,7 +133,7 @@ void canExecute_PropertyChanged(object sender, PropertyChangedEventArgs e) /// public AsyncReactiveCommand(IReactiveProperty sharedCanExecute) { - void canExecute_PropertyChanged(object sender, PropertyChangedEventArgs e) + void canExecute_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName is not nameof(IReactiveProperty.Value)) return; From a96207a2584653fe3f36db0d5c6b111228a28316 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Wed, 4 Jan 2023 19:14:21 +0900 Subject: [PATCH 20/32] Add ReactiveCommandSlim --- .../Internals/DelegateObserver.cs | 63 ++++ .../ReactiveCommandSlim.cs | 352 ++++++++++++++++++ .../ReactiveCommandSlimTest.cs | 332 +++++++++++++++++ 3 files changed, 747 insertions(+) create mode 100644 Source/ReactiveProperty.Core/Internals/DelegateObserver.cs create mode 100644 Source/ReactiveProperty.Core/ReactiveCommandSlim.cs create mode 100644 Test/ReactiveProperty.NETStandard.Tests/ReactiveCommandSlimTest.cs diff --git a/Source/ReactiveProperty.Core/Internals/DelegateObserver.cs b/Source/ReactiveProperty.Core/Internals/DelegateObserver.cs new file mode 100644 index 00000000..6adfdb70 --- /dev/null +++ b/Source/ReactiveProperty.Core/Internals/DelegateObserver.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reactive.Bindings.Internals; + +internal class DelegateObserver : IObserver +{ + private readonly Action _onNext; + private readonly Action? _onCompleted; + private readonly Action? _onError; + + public DelegateObserver(Action onNext, Action? onCompleted, Action? onError) + { + _onNext = onNext; + _onCompleted = onCompleted; + _onError = onError; + } + + public DelegateObserver(Action onNext) : this(onNext, null, null) + { + } + + public DelegateObserver(Action onNext, Action onCompleted) : this(onNext, onCompleted, null) + { + } + + public void OnCompleted() => _onCompleted?.Invoke(); + + public void OnError(Exception error) => _onError?.Invoke(error); + + public void OnNext() => _onNext(); + + void IObserver.OnNext(object? value) => OnNext(); +} + +internal class DelegateObserver : IObserver +{ + private readonly Action _onNext; + private readonly Action? _onCompleted; + private readonly Action? _onError; + + public DelegateObserver(Action onNext, Action? onCompleted, Action? onError) + { + _onNext = onNext; + _onCompleted = onCompleted; + _onError = onError; + } + + public DelegateObserver(Action onNext) : this(onNext, null, null) + { + } + + public DelegateObserver(Action onNext, Action onCompleted) : this(onNext, onCompleted, null) + { + } + + public void OnCompleted() => _onCompleted?.Invoke(); + + public void OnError(Exception error) => _onError?.Invoke(error); + + public void OnNext(T value) => _onNext(value); +} diff --git a/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs b/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs new file mode 100644 index 00000000..490d10e8 --- /dev/null +++ b/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs @@ -0,0 +1,352 @@ +using System; +using System.ComponentModel; +using System.Runtime.ExceptionServices; +using System.Windows.Input; +using Reactive.Bindings.Internals; + +namespace Reactive.Bindings; + +/// +/// +/// +public class ReactiveCommandSlim : ReactiveCommandSlim +{ + /// + /// Create a ReactiveCommandSlim instance. + /// + public ReactiveCommandSlim() : base() + { + } + + /// + /// Create a ReactiveCommandSlim instance. + /// + /// The shared CanExecute status + public ReactiveCommandSlim(IReactiveProperty sharedCanExecute) : base(sharedCanExecute) + { + } + + /// + /// Create a ReactiveCommandSlim instance. + /// + /// The CanExecute source + /// The canExecuteSource initial value if not provided + public ReactiveCommandSlim(IObservable canExecuteSource, bool initialValue = true) : base(canExecuteSource, initialValue) + { + } + + /// + /// Create a ReactiveCommandSlim instance. + /// + /// The CanExecute source + /// The shared CanExecute status + /// The canExecuteSource initial value if not provided + public ReactiveCommandSlim(IObservable canExecuteSource, IReactiveProperty sharedCanExecute, bool initialValue = true) : base(canExecuteSource, sharedCanExecute, initialValue) + { + } + + /// + /// Push null to subscribers. + /// + public void Execute() => Execute(null); + + /// + /// + /// + public bool CanExecute() => CanExecute(null); + + /// + /// Subscribe execute. + /// + public IDisposable Subscribe(Action onNext) + => this.Subscribe(new DelegateObserver(onNext)); +} + +/// +/// implementation for ReactiveProperty. +/// +public class ReactiveCommandSlim : ICommand, IObservable, IObserver, IObserverLinkedList, IDisposable +{ + private readonly IReactiveProperty _sharedCanExecute; + private readonly IReadOnlyReactiveProperty? _canExecuteSource; + private readonly Action _disposeAction; + private bool _canExecute = true; + private ObserverNode? _root; + private ObserverNode? _last; + + /// + public event EventHandler? CanExecuteChanged; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// true if this instance is disposed; otherwise, false. + public bool IsDisposed { get; private set; } + + /// + /// CanExecute is automatically changed when executing to false and finished to true. + /// + public ReactiveCommandSlim() : this(new ReactivePropertySlim(true)) + { + } + + /// + /// CanExecute is automatically changed when executing to false and finished to true. + /// + /// The shared CanExecute status + public ReactiveCommandSlim(IReactiveProperty sharedCanExecute) + { + void canExecute_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is not nameof(IReactiveProperty.Value)) return; + + _canExecute = _sharedCanExecute.Value; + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + + _sharedCanExecute = sharedCanExecute; + _sharedCanExecute.PropertyChanged += canExecute_PropertyChanged; + _canExecute = _sharedCanExecute.Value; + _disposeAction = () => _sharedCanExecute.PropertyChanged -= canExecute_PropertyChanged; + } + + /// + /// CanExecute is automatically changed when executing to false and finished to true. + /// + /// The CanExecute source + /// The canExecuteSource initial value if not provided + public ReactiveCommandSlim(IObservable canExecuteSource, bool initialValue = true) : this(canExecuteSource, new ReactivePropertySlim(true), initialValue) + { + } + + /// + /// CanExecute is automatically changed when executing to false and finished to true. + /// + /// The CanExecute source + /// The shared CanExecute status + /// The canExecuteSource initial value if not provided + public ReactiveCommandSlim(IObservable canExecuteSource, IReactiveProperty sharedCanExecute, bool initialValue = true) + { + void canExecute_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is not nameof(IReactiveProperty.Value)) return; + + var newValue = _sharedCanExecute.Value && _canExecuteSource!.Value; + if (newValue == _canExecute) return; + + _canExecute = newValue; + CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + + _sharedCanExecute = sharedCanExecute ?? new ReactivePropertySlim(true); + _canExecuteSource = canExecuteSource.ToReadOnlyReactivePropertySlim(initialValue); + _sharedCanExecute.PropertyChanged += canExecute_PropertyChanged; + _canExecuteSource.PropertyChanged += canExecute_PropertyChanged; + _canExecute = _sharedCanExecute.Value && _canExecuteSource.Value; + _disposeAction = () => + { + _sharedCanExecute.PropertyChanged += canExecute_PropertyChanged; + _canExecuteSource.PropertyChanged += canExecute_PropertyChanged; + _canExecuteSource.Dispose(); + }; + } + + /// + public bool CanExecute(T? parameter) => _canExecute; + + /// + public void Execute(T? parameter) + { + if (_canExecute is false) return; + + // call source.OnNext + var node = _root; + while (node != null) + { + node.OnNext(parameter); + node = node.Next; + } + } + + /// + /// Subscribe execute. + /// + public IDisposable Subscribe(Action onNext) + => this.Subscribe(new DelegateObserver(onNext)); + + + /// + bool ICommand.CanExecute(object? parameter) => CanExecute((T?)parameter); + + /// + void ICommand.Execute(object? parameter) => Execute((T?)parameter); + + /// + /// Notifies the provider that an observer is to receive notifications. + /// + /// The object that is to receive notifications. + /// + /// A reference to an interface that allows observers to stop receiving notifications before + /// the provider has finished sending them. + /// + public IDisposable Subscribe(IObserver observer) + { + if (IsDisposed) + { + observer.OnCompleted(); + return InternalDisposable.Empty; + } + + // subscribe node, node as subscription. + var next = new ObserverNode(this, observer); + if (_root == null) + { + _root = _last = next; + } + else + { + _last!.Next = next; + next.Previous = _last; + _last = next; + } + return next; + } + + void IObserverLinkedList.UnsubscribeNode(ObserverNode node) + { + if (node == _root) + { + _root = node.Next; + } + if (node == _last) + { + _last = node.Previous; + } + + if (node.Previous != null) + { + node.Previous.Next = node.Next; + } + if (node.Next != null) + { + node.Next.Previous = node.Previous; + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting + /// unmanaged resources. + /// + public void Dispose() + { + if (IsDisposed) + { + return; + } + + var node = _root; + _root = _last = null; + IsDisposed = true; + _disposeAction(); + _canExecute = false; + while (node != null) + { + node.OnCompleted(); + node = node.Next; + } + } + + void IObserver.OnCompleted() => _canExecute = false; + + void IObserver.OnError(Exception error) => ExceptionDispatchInfo.Capture(error).Throw(); + + void IObserver.OnNext(bool value) => _canExecute = value; +} + + +/// +/// ReactiveCommand factory extension methods. +/// +public static class ReactiveCommandSlimExtensions +{ + /// + /// CanExecuteChanged is called from canExecute sequence on UIDispatcherScheduler. + /// + public static ReactiveCommandSlim ToReactiveCommandSlim(this IObservable canExecuteSource, bool initialValue = true) => + new(canExecuteSource, initialValue); + + /// + /// CanExecuteChanged is called from canExecute sequence on UIDispatcherScheduler. + /// + public static ReactiveCommandSlim ToReactiveCommandSlim(this IObservable canExecuteSource, bool initialValue = true) => + new(canExecuteSource, initialValue); + + /// + /// CanExecuteChanged is called from canExecute sequence on UIDispatcherScheduler. + /// + public static ReactiveCommandSlim ToReactiveCommandSlim(this IObservable canExecuteSource, IReactiveProperty initialValue) => + new(canExecuteSource, initialValue); + + /// + /// CanExecuteChanged is called from canExecute sequence on UIDispatcherScheduler. + /// + public static ReactiveCommandSlim ToReactiveCommandSlim(this IObservable canExecuteSource, IReactiveProperty initialValue) => + new(canExecuteSource, initialValue); + + /// + /// Subscribe execute. + /// + /// ReactiveCommand + /// Action + /// Handling of the subscription. + /// Same of self argument + public static ReactiveCommandSlim WithSubscribe(this ReactiveCommandSlim self, Action onNext, Action? postProcess = null) + { + var d = self.Subscribe(new DelegateObserver(onNext)); + postProcess?.Invoke(d); + return self; + } + + /// + /// Subscribe execute. + /// + /// ReactiveCommand type argument. + /// ReactiveCommand + /// Action + /// Handling of the subscription. + /// Same of self argument + public static ReactiveCommandSlim WithSubscribe(this ReactiveCommandSlim self, Action onNext, Action? postProcess = null) + { + var d = self.Subscribe(new DelegateObserver(onNext)); + postProcess?.Invoke(d); + return self; + } + + /// + /// Subscribe execute. + /// + /// ReactiveCommand + /// Action + /// The return value of self.Subscribe(onNext) + /// Same of self argument + public static ReactiveCommandSlim WithSubscribe(this ReactiveCommandSlim self, Action onNext, out IDisposable disposable) + { + disposable = self.Subscribe(new DelegateObserver(onNext)); + return self; + } + + /// + /// Subscribe execute. + /// + /// ReactiveCommand type argument. + /// ReactiveCommand + /// Action + /// The return value of self.Subscribe(onNext) + /// Same of self argument + public static ReactiveCommandSlim WithSubscribe(this ReactiveCommandSlim self, Action onNext, out IDisposable disposable) + { + disposable = self.Subscribe(new DelegateObserver(onNext)); + return self; + } +} + diff --git a/Test/ReactiveProperty.NETStandard.Tests/ReactiveCommandSlimTest.cs b/Test/ReactiveProperty.NETStandard.Tests/ReactiveCommandSlimTest.cs new file mode 100644 index 00000000..cd09c934 --- /dev/null +++ b/Test/ReactiveProperty.NETStandard.Tests/ReactiveCommandSlimTest.cs @@ -0,0 +1,332 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using Microsoft.Reactive.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Reactive.Bindings; + +namespace ReactiveProperty.Tests; + +[TestClass] +public class ReactiveCommandSlimTest : ReactiveTest +{ + [TestMethod] + public void ReactiveCommandAllFlow() + { + var testScheduler = new TestScheduler(); + var @null = (object)null; + var recorder1 = testScheduler.CreateObserver(); + var recorder2 = testScheduler.CreateObserver(); + + var cmd = new ReactiveCommandSlim(); + cmd.Subscribe(recorder1); + cmd.Subscribe(recorder2); + + cmd.CanExecute().Is(true); + cmd.Execute(); testScheduler.AdvanceBy(10); + cmd.Execute(); testScheduler.AdvanceBy(10); + cmd.Execute(); testScheduler.AdvanceBy(10); + + cmd.Dispose(); + cmd.CanExecute().Is(false); + + cmd.Dispose(); // dispose again + + recorder1.Messages.Is( + OnNext(0, @null), + OnNext(10, @null), + OnNext(20, @null), + OnCompleted(30)); + + recorder2.Messages.Is( + OnNext(0, @null), + OnNext(10, @null), + OnNext(20, @null), + OnCompleted(30)); + } + + [TestMethod] + public void ReactiveCommandSubscribe() + { + var testScheduler = new TestScheduler(); + var recorder1 = testScheduler.CreateObserver(); + var recorder2 = testScheduler.CreateObserver(); + + var cmd = new ReactiveCommandSlim(); + var counter = 0; + Action countUp = () => counter++; + cmd.Subscribe(countUp); + Action recordAction1 = () => recorder1.OnNext(counter); + cmd.Subscribe(recordAction1); + Action recordAction2 = () => recorder2.OnNext(counter); + cmd.Subscribe(recordAction2); + + cmd.Execute(); testScheduler.AdvanceBy(10); + cmd.Execute(); testScheduler.AdvanceBy(10); + cmd.Execute(); testScheduler.AdvanceBy(10); + + recorder1.Messages.Is( + OnNext(0, 1), + OnNext(10, 2), + OnNext(20, 3)); + recorder2.Messages.Is( + OnNext(0, 1), + OnNext(10, 2), + OnNext(20, 3)); + } + + [TestMethod] + public void WithSubscribe() + { + var testScheduler = new TestScheduler(); + var recorder1 = testScheduler.CreateObserver(); + var recorder2 = testScheduler.CreateObserver(); + var recorder3 = testScheduler.CreateObserver(); + + var disposable1 = new CompositeDisposable(); + var disposable2 = new CompositeDisposable(); + var cmd = new ReactiveCommandSlim() + .WithSubscribe(() => recorder1.OnNext("x"), disposable1.Add) + .WithSubscribe(() => recorder2.OnNext("x"), disposable2.Add) + .WithSubscribe(() => recorder3.OnNext("x")); + + cmd.Execute(); + testScheduler.AdvanceBy(10); + + disposable1.Dispose(); + cmd.Execute(); + testScheduler.AdvanceBy(10); + + disposable2.Dispose(); + cmd.Execute(); + + recorder1.Messages.Is( + OnNext(0, "x")); + recorder2.Messages.Is( + OnNext(0, "x"), + OnNext(10, "x")); + recorder3.Messages.Is( + OnNext(0, "x"), + OnNext(10, "x"), + OnNext(20, "x")); + } + + [TestMethod] + public void WithSubscribeGenericVersion() + { + var testScheduler = new TestScheduler(); + var recorder1 = testScheduler.CreateObserver(); + var recorder2 = testScheduler.CreateObserver(); + var recorder3 = testScheduler.CreateObserver(); + + var disposable1 = new CompositeDisposable(); + var disposable2 = new CompositeDisposable(); + var cmd = new ReactiveCommandSlim() + .WithSubscribe(x => recorder1.OnNext(x), disposable1.Add) + .WithSubscribe(x => recorder2.OnNext(x), disposable2.Add) + .WithSubscribe(x => recorder3.OnNext(x)); + + cmd.Execute("a"); + testScheduler.AdvanceBy(10); + + disposable1.Dispose(); + cmd.Execute("b"); + testScheduler.AdvanceBy(10); + + disposable2.Dispose(); + cmd.Execute("c"); + + recorder1.Messages.Is( + OnNext(0, "a")); + recorder2.Messages.Is( + OnNext(0, "a"), + OnNext(10, "b")); + recorder3.Messages.Is( + OnNext(0, "a"), + OnNext(10, "b"), + OnNext(20, "c")); + } + + [TestMethod] + public void WithSubscribeDisposableOverride() + { + var testScheduler = new TestScheduler(); + var recorder1 = testScheduler.CreateObserver(); + var recorder2 = testScheduler.CreateObserver(); + var recorder3 = testScheduler.CreateObserver(); + var cmd = new ReactiveCommandSlim() + .WithSubscribe(() => recorder1.OnNext("x"), out var disposable1) + .WithSubscribe(() => recorder2.OnNext("x"), out var disposable2) + .WithSubscribe(() => recorder3.OnNext("x")); + + cmd.Execute(); + testScheduler.AdvanceBy(10); + + disposable1.Dispose(); + cmd.Execute(); + testScheduler.AdvanceBy(10); + + disposable2.Dispose(); + cmd.Execute(); + + recorder1.Messages.Is( + OnNext(0, "x")); + recorder2.Messages.Is( + OnNext(0, "x"), + OnNext(10, "x")); + recorder3.Messages.Is( + OnNext(0, "x"), + OnNext(10, "x"), + OnNext(20, "x")); + } + + [TestMethod] + public void WithSubscribeDisposableOverrideGenericVersion() + { + var testScheduler = new TestScheduler(); + var recorder1 = testScheduler.CreateObserver(); + var recorder2 = testScheduler.CreateObserver(); + var recorder3 = testScheduler.CreateObserver(); + var cmd = new ReactiveCommandSlim() + .WithSubscribe(x => recorder1.OnNext(x), out var disposable1) + .WithSubscribe(x => recorder2.OnNext(x), out var disposable2) + .WithSubscribe(x => recorder3.OnNext(x)); + + cmd.Execute("a"); + testScheduler.AdvanceBy(10); + + disposable1.Dispose(); + cmd.Execute("b"); + testScheduler.AdvanceBy(10); + + disposable2.Dispose(); + cmd.Execute("c"); + + recorder1.Messages.Is( + OnNext(0, "a")); + recorder2.Messages.Is( + OnNext(0, "a"), + OnNext(10, "b")); + recorder3.Messages.Is( + OnNext(0, "a"), + OnNext(10, "b"), + OnNext(20, "c")); + } + + [TestMethod] + public void ShredCanExecuteStateCase() + { + var rp = new ReactivePropertySlim(false); + + var executeCounter1 = 0; + var canExecutedCounter1 = 0; + var executeCounter2 = 0; + var canExecutedCounter2 = 0; + + var command1 = rp.ToReactiveCommandSlim(); + command1.Subscribe(() => executeCounter1++); + command1.CanExecuteChanged += (_, __) => canExecutedCounter1++; + var command2 = rp.ToReactiveCommandSlim(); + command2.Subscribe(() => executeCounter2++); + command2.CanExecuteChanged += (_, __) => canExecutedCounter2++; + + command1.CanExecute().IsFalse(); + command2.CanExecute().IsFalse(); + command1.Execute(); + command2.Execute(); + executeCounter1.Is(0); + executeCounter2.Is(0); + + rp.Value = true; + command1.CanExecute().IsTrue(); + command2.CanExecute().IsTrue(); + canExecutedCounter1.Is(1); + canExecutedCounter2.Is(1); + + command1.Execute(); + command2.Execute(); + command1.CanExecute().IsTrue(); + command2.CanExecute().IsTrue(); + executeCounter1.Is(1); + executeCounter2.Is(1); + canExecutedCounter1.Is(1); + canExecutedCounter1.Is(1); + + rp.Value = false; + command1.CanExecute().IsFalse(); + command2.CanExecute().IsFalse(); + canExecutedCounter1.Is(2); + canExecutedCounter1.Is(2); + } + + [TestMethod] + public void ShredReactivePropertyCase() + { + var rp = new ReactivePropertySlim(false); + + var executeCounter1 = 0; + var canExecutedCounter1 = 0; + var executeCounter2 = 0; + var canExecutedCounter2 = 0; + + var command1 = new ReactiveCommandSlim(rp); + command1.Subscribe(() => executeCounter1++); + command1.CanExecuteChanged += (_, __) => canExecutedCounter1++; + var command2 = new ReactiveCommandSlim(rp); + command2.Subscribe(() => executeCounter2++); + command2.CanExecuteChanged += (_, __) => canExecutedCounter2++; + + command1.CanExecute().IsFalse(); + command2.CanExecute().IsFalse(); + command1.Execute(); + command2.Execute(); + executeCounter1.Is(0); + executeCounter2.Is(0); + + rp.Value = true; + command1.CanExecute().IsTrue(); + command2.CanExecute().IsTrue(); + canExecutedCounter1.Is(1); + canExecutedCounter2.Is(1); + + command1.Execute(); + command2.Execute(); + command1.CanExecute().IsTrue(); + command2.CanExecute().IsTrue(); + executeCounter1.Is(1); + executeCounter2.Is(1); + canExecutedCounter1.Is(1); + canExecutedCounter1.Is(1); + + rp.Value = false; + command1.CanExecute().IsFalse(); + command2.CanExecute().IsFalse(); + canExecutedCounter1.Is(2); + canExecutedCounter1.Is(2); + } + + [TestMethod] + public void SharedSourceAndReactivePropertyCase() + { + var canExecuteSource = new Subject(); + var sharedCanExecuteSource = new ReactivePropertySlim(false); + var canExecuteChangedCounter = 0; + + var command = canExecuteSource.ToReactiveCommandSlim(sharedCanExecuteSource); + command.CanExecuteChanged += (_, _) => canExecuteChangedCounter++; + canExecuteChangedCounter.Is(0); + command.CanExecute().IsFalse(); + + sharedCanExecuteSource.Value = true; + canExecuteChangedCounter.Is(1); + command.CanExecute().IsTrue(); + + canExecuteSource.OnNext(false); + canExecuteChangedCounter.Is(2); + command.CanExecute().IsFalse(); + + canExecuteSource.OnNext(true); + canExecuteChangedCounter.Is(3); + command.CanExecute().IsTrue(); + } +} From 82e893aa7e2bab4910520f64a30e00da9270d757 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Wed, 4 Jan 2023 21:43:20 +0900 Subject: [PATCH 21/32] Fix typo --- Source/ReactiveProperty.Core/ReactiveCommandSlim.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs b/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs index 490d10e8..beb6dc9e 100644 --- a/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs +++ b/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs @@ -145,8 +145,8 @@ void canExecute_PropertyChanged(object? sender, PropertyChangedEventArgs e) _canExecute = _sharedCanExecute.Value && _canExecuteSource.Value; _disposeAction = () => { - _sharedCanExecute.PropertyChanged += canExecute_PropertyChanged; - _canExecuteSource.PropertyChanged += canExecute_PropertyChanged; + _sharedCanExecute.PropertyChanged -= canExecute_PropertyChanged; + _canExecuteSource.PropertyChanged -= canExecute_PropertyChanged; _canExecuteSource.Dispose(); }; } From d0f9ec13d1f950c28523a3f69c1c8f36651e8dc5 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Wed, 4 Jan 2023 21:43:44 +0900 Subject: [PATCH 22/32] More performance improvements --- .../ReactiveCommandSlim.cs | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs b/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs index beb6dc9e..85061543 100644 --- a/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs +++ b/Source/ReactiveProperty.Core/ReactiveCommandSlim.cs @@ -1,5 +1,7 @@ using System; +using System.Collections; using System.ComponentModel; +using System.Linq; using System.Runtime.ExceptionServices; using System.Windows.Input; using Reactive.Bindings.Internals; @@ -22,7 +24,7 @@ public ReactiveCommandSlim() : base() /// Create a ReactiveCommandSlim instance. /// /// The shared CanExecute status - public ReactiveCommandSlim(IReactiveProperty sharedCanExecute) : base(sharedCanExecute) + public ReactiveCommandSlim(IReadOnlyReactiveProperty sharedCanExecute) : base(sharedCanExecute) { } @@ -41,7 +43,7 @@ public ReactiveCommandSlim(IObservable canExecuteSource, bool initialValue /// The CanExecute source /// The shared CanExecute status /// The canExecuteSource initial value if not provided - public ReactiveCommandSlim(IObservable canExecuteSource, IReactiveProperty sharedCanExecute, bool initialValue = true) : base(canExecuteSource, sharedCanExecute, initialValue) + public ReactiveCommandSlim(IObservable canExecuteSource, IReadOnlyReactiveProperty sharedCanExecute, bool initialValue = true) : base(canExecuteSource, sharedCanExecute, initialValue) { } @@ -67,9 +69,9 @@ public IDisposable Subscribe(Action onNext) /// public class ReactiveCommandSlim : ICommand, IObservable, IObserver, IObserverLinkedList, IDisposable { - private readonly IReactiveProperty _sharedCanExecute; + private readonly IReadOnlyReactiveProperty? _sharedCanExecute; private readonly IReadOnlyReactiveProperty? _canExecuteSource; - private readonly Action _disposeAction; + private readonly Action? _disposeAction; private bool _canExecute = true; private ObserverNode? _root; private ObserverNode? _last; @@ -86,15 +88,16 @@ public class ReactiveCommandSlim : ICommand, IObservable, IObserver /// /// CanExecute is automatically changed when executing to false and finished to true. /// - public ReactiveCommandSlim() : this(new ReactivePropertySlim(true)) + public ReactiveCommandSlim() { + _canExecute = true; } /// /// CanExecute is automatically changed when executing to false and finished to true. /// /// The shared CanExecute status - public ReactiveCommandSlim(IReactiveProperty sharedCanExecute) + public ReactiveCommandSlim(IReadOnlyReactiveProperty sharedCanExecute) { void canExecute_PropertyChanged(object? sender, PropertyChangedEventArgs e) { @@ -115,7 +118,7 @@ void canExecute_PropertyChanged(object? sender, PropertyChangedEventArgs e) /// /// The CanExecute source /// The canExecuteSource initial value if not provided - public ReactiveCommandSlim(IObservable canExecuteSource, bool initialValue = true) : this(canExecuteSource, new ReactivePropertySlim(true), initialValue) + public ReactiveCommandSlim(IObservable canExecuteSource, bool initialValue = true) : this(canExecuteSource, TrueReadOnlyReactiveProperty.Instance, initialValue) { } @@ -125,7 +128,7 @@ void canExecute_PropertyChanged(object? sender, PropertyChangedEventArgs e) /// The CanExecute source /// The shared CanExecute status /// The canExecuteSource initial value if not provided - public ReactiveCommandSlim(IObservable canExecuteSource, IReactiveProperty sharedCanExecute, bool initialValue = true) + public ReactiveCommandSlim(IObservable canExecuteSource, IReadOnlyReactiveProperty sharedCanExecute, bool initialValue = true) { void canExecute_PropertyChanged(object? sender, PropertyChangedEventArgs e) { @@ -138,7 +141,7 @@ void canExecute_PropertyChanged(object? sender, PropertyChangedEventArgs e) CanExecuteChanged?.Invoke(this, EventArgs.Empty); } - _sharedCanExecute = sharedCanExecute ?? new ReactivePropertySlim(true); + _sharedCanExecute = sharedCanExecute; _canExecuteSource = canExecuteSource.ToReadOnlyReactivePropertySlim(initialValue); _sharedCanExecute.PropertyChanged += canExecute_PropertyChanged; _canExecuteSource.PropertyChanged += canExecute_PropertyChanged; @@ -247,7 +250,7 @@ public void Dispose() var node = _root; _root = _last = null; IsDisposed = true; - _disposeAction(); + _disposeAction?.Invoke(); _canExecute = false; while (node != null) { @@ -261,6 +264,28 @@ public void Dispose() void IObserver.OnError(Exception error) => ExceptionDispatchInfo.Capture(error).Throw(); void IObserver.OnNext(bool value) => _canExecute = value; + + private class TrueReadOnlyReactiveProperty : IReadOnlyReactiveProperty + { + public static IReadOnlyReactiveProperty Instance { get; } = new TrueReadOnlyReactiveProperty(); + + private TrueReadOnlyReactiveProperty() + { + } + + public bool Value => true; + + object? IReadOnlyReactiveProperty.Value => true; + + // noop + public event PropertyChangedEventHandler? PropertyChanged { add { } remove { } } + + public void Dispose() + { + } + + public IDisposable Subscribe(IObserver observer) => throw new NotSupportedException(); + } } @@ -284,14 +309,14 @@ public static ReactiveCommandSlim ToReactiveCommandSlim(this IObservable /// CanExecuteChanged is called from canExecute sequence on UIDispatcherScheduler. /// - public static ReactiveCommandSlim ToReactiveCommandSlim(this IObservable canExecuteSource, IReactiveProperty initialValue) => - new(canExecuteSource, initialValue); + public static ReactiveCommandSlim ToReactiveCommandSlim(this IObservable canExecuteSource, IReadOnlyReactiveProperty sharedCanExecute, bool initialValue = true) => + new(canExecuteSource, sharedCanExecute, initialValue); /// /// CanExecuteChanged is called from canExecute sequence on UIDispatcherScheduler. /// - public static ReactiveCommandSlim ToReactiveCommandSlim(this IObservable canExecuteSource, IReactiveProperty initialValue) => - new(canExecuteSource, initialValue); + public static ReactiveCommandSlim ToReactiveCommandSlim(this IObservable canExecuteSource, IReadOnlyReactiveProperty sharedCanExecute, bool initialValue = true) => + new(canExecuteSource, sharedCanExecute, initialValue); /// /// Subscribe execute. From 93352964e6bab6bf69f5653ce0a7087a08348f20 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Wed, 4 Jan 2023 21:43:56 +0900 Subject: [PATCH 23/32] Add benchmark for ReactiveCommandSlim --- .../ReactiveCommandBenchmark.cs | 41 +++++++++++++++++++ Benchmark/Benchmark.V6/BasicUsages.cs | 1 + Benchmark/Benchmark.V6/Program.cs | 6 +-- 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 Benchmark/Benchmark.Current/ReactiveCommandBenchmark.cs diff --git a/Benchmark/Benchmark.Current/ReactiveCommandBenchmark.cs b/Benchmark/Benchmark.Current/ReactiveCommandBenchmark.cs new file mode 100644 index 00000000..0b67e8e7 --- /dev/null +++ b/Benchmark/Benchmark.Current/ReactiveCommandBenchmark.cs @@ -0,0 +1,41 @@ +using System; +using System.Reactive.Subjects; +using BenchmarkDotNet.Attributes; +using Reactive.Bindings; + +namespace Benchmark.Current; + +public class ReactiveCommandBenchmarks +{ + [Benchmark] + public ReactiveCommand CreateReactiveCommand() => new ReactiveCommand(); + + [Benchmark] + public ReactiveCommandSlim CreateReactiveCommandSlim() => new ReactiveCommandSlim(); + + + + [Benchmark] + public ReactiveCommand BasicUsecaseForReactiveCommand() + { + var subject = new Subject(); + var cmd = subject.ToReactiveCommand(); + cmd.Subscribe(x => { }); + cmd.Execute(); + subject.OnNext(true); + cmd.Execute(); + return cmd; + } + + [Benchmark] + public ReactiveCommandSlim BasicUsecaseForReactiveCommandSlim() + { + var subject = new Subject(); + var cmd = subject.ToReactiveCommandSlim(); + cmd.Subscribe(x => { }); + cmd.Execute(); + subject.OnNext(true); + cmd.Execute(); + return cmd; + } +} diff --git a/Benchmark/Benchmark.V6/BasicUsages.cs b/Benchmark/Benchmark.V6/BasicUsages.cs index 2722515c..35ea931a 100644 --- a/Benchmark/Benchmark.V6/BasicUsages.cs +++ b/Benchmark/Benchmark.V6/BasicUsages.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Reactive.Subjects; using System.Runtime.CompilerServices; using System.Text; using BenchmarkDotNet.Attributes; diff --git a/Benchmark/Benchmark.V6/Program.cs b/Benchmark/Benchmark.V6/Program.cs index a8af61ab..676b0b25 100644 --- a/Benchmark/Benchmark.V6/Program.cs +++ b/Benchmark/Benchmark.V6/Program.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; namespace ReactivePropertyBenchmark { @@ -9,6 +6,5 @@ class Program { static void Main(string[] args) => BenchmarkRunner.Run(typeof(Program).Assembly); - } } From 189d2c59454fa2795fbf7c510edc6dc4a8a0cebf Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Wed, 4 Jan 2023 21:53:06 +0900 Subject: [PATCH 24/32] Add CompositeDisposable --- .../Disposables/CompositeDisposable.cs | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 Source/ReactiveProperty.Core/Disposables/CompositeDisposable.cs diff --git a/Source/ReactiveProperty.Core/Disposables/CompositeDisposable.cs b/Source/ReactiveProperty.Core/Disposables/CompositeDisposable.cs new file mode 100644 index 00000000..d56fc0d9 --- /dev/null +++ b/Source/ReactiveProperty.Core/Disposables/CompositeDisposable.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; + +namespace Reactive.Bindings.Disposables; + +// copy, modified from UniRx +// https://raw.githubusercontent.com/neuecc/UniRx/master/Assets/Plugins/UniRx/Scripts/Disposables/CompositeDisposable.cs + +/// +/// Collection of +/// +public sealed class CompositeDisposable : ICollection, IDisposable +{ + private readonly object _gate = new object(); + + private bool _disposed; + private List _disposables; + private int _count; + private const int SHRINK_THRESHOLD = 64; + + /// + /// Initializes a new instance of the class with no disposables contained by it initially. + /// + public CompositeDisposable() + { + _disposables = new List(); + } + + /// + /// Initializes a new instance of the class with the specified number of disposables. + /// + /// The number of disposables that the new CompositeDisposable can initially store. + /// is less than zero. + public CompositeDisposable(int capacity) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException("capacity"); + + _disposables = new List(capacity); + } + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is null. + public CompositeDisposable(params IDisposable[] disposables) + { + if (disposables == null) + throw new ArgumentNullException("disposables"); + + _disposables = new List(disposables); + _count = _disposables.Count; + } + + /// + /// Initializes a new instance of the class from a group of disposables. + /// + /// Disposables that will be disposed together. + /// is null. + public CompositeDisposable(IEnumerable disposables) + { + if (disposables == null) + throw new ArgumentNullException("disposables"); + + _disposables = new List(disposables); + _count = _disposables.Count; + } + + /// + /// Gets the number of disposables contained in the CompositeDisposable. + /// + public int Count + { + get + { + return _count; + } + } + + /// + /// Adds a disposable to the CompositeDisposable or disposes the disposable if the CompositeDisposable is disposed. + /// + /// Disposable to add. + /// is null. + public void Add(IDisposable item) + { + if (item == null) + throw new ArgumentNullException("item"); + + var shouldDispose = false; + lock (_gate) + { + shouldDispose = _disposed; + if (!_disposed) + { + _disposables.Add(item); + _count++; + } + } + if (shouldDispose) + item.Dispose(); + } + + /// + /// Removes and disposes the first occurrence of a disposable from the CompositeDisposable. + /// + /// Disposable to remove. + /// true if found; false otherwise. + /// is null. + public bool Remove(IDisposable item) + { + if (item == null) + throw new ArgumentNullException("item"); + + var shouldDispose = false; + + lock (_gate) + { + if (!_disposed) + { + // + // List doesn't shrink the size of the underlying array but does collapse the array + // by copying the tail one position to the left of the removal index. We don't need + // index-based lookup but only ordering for sequential disposal. So, instead of spending + // cycles on the Array.Copy imposed by Remove, we use a null sentinel value. We also + // do manual Swiss cheese detection to shrink the list if there's a lot of holes in it. + // + var i = _disposables.IndexOf(item); + if (i >= 0) + { + shouldDispose = true; + _disposables[i] = null; + _count--; + + if (_disposables.Capacity > SHRINK_THRESHOLD && _count < _disposables.Capacity / 2) + { + var old = _disposables; + _disposables = new List(_disposables.Capacity / 2); + + foreach (var d in old) + if (d != null) + _disposables.Add(d); + } + } + } + } + + if (shouldDispose) + item.Dispose(); + + return shouldDispose; + } + + /// + /// Disposes all disposables in the group and removes them from the group. + /// + public void Dispose() + { + var currentDisposables = default(IDisposable[]); + lock (_gate) + { + if (!_disposed) + { + _disposed = true; + currentDisposables = _disposables.ToArray(); + _disposables.Clear(); + _count = 0; + } + } + + if (currentDisposables != null) + { + foreach (var d in currentDisposables) + if (d != null) + d.Dispose(); + } + } + + /// + /// Removes and disposes all disposables from the CompositeDisposable, but does not dispose the CompositeDisposable. + /// + public void Clear() + { + var currentDisposables = default(IDisposable[]); + lock (_gate) + { + currentDisposables = _disposables.ToArray(); + _disposables.Clear(); + _count = 0; + } + + foreach (var d in currentDisposables) + if (d != null) + d.Dispose(); + } + + /// + /// Determines whether the CompositeDisposable contains a specific disposable. + /// + /// Disposable to search for. + /// true if the disposable was found; otherwise, false. + /// is null. + public bool Contains(IDisposable item) + { + if (item == null) + throw new ArgumentNullException("item"); + + lock (_gate) + { + return _disposables.Contains(item); + } + } + + /// + /// Copies the disposables contained in the CompositeDisposable to an array, starting at a particular array index. + /// + /// Array to copy the contained disposables to. + /// Target index at which to copy the first disposable of the group. + /// is null. + /// is less than zero. -or - is larger than or equal to the array length. + public void CopyTo(IDisposable[] array, int arrayIndex) + { + if (array == null) + throw new ArgumentNullException("array"); + if (arrayIndex < 0 || arrayIndex >= array.Length) + throw new ArgumentOutOfRangeException("arrayIndex"); + + lock (_gate) + { + var disArray = new List(); + foreach (var item in _disposables) + { + if (item != null) disArray.Add(item); + } + + Array.Copy(disArray.ToArray(), 0, array, arrayIndex, array.Length - arrayIndex); + } + } + + /// + /// Always returns false. + /// + public bool IsReadOnly => false; + + /// + /// Returns an enumerator that iterates through the CompositeDisposable. + /// + /// An enumerator to iterate over the disposables. + public IEnumerator GetEnumerator() + { + var res = new List(); + + lock (_gate) + { + foreach (var d in _disposables) + { + if (d != null) res.Add(d); + } + } + + return res.GetEnumerator(); + } + + /// + /// Returns an enumerator that iterates through the CompositeDisposable. + /// + /// An enumerator to iterate over the disposables. + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Gets a value that indicates whether the object is disposed. + /// + public bool IsDisposed => _disposed; +} From 8983c9efb7b7ac1a52bbddd4f5e15a8132753b03 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 5 Jan 2023 00:00:42 +0900 Subject: [PATCH 25/32] move ObserveProperty and PropertyChangedAsObservable to Core project. Add ObservePropertyLegacy and PropertyChangedAsObservableLegacy methods for users who want to use previous behavior. --- .../INotifyPropertyChangedExtensions.cs | 52 +++++ .../Internals/NestedPropertyObservable.cs | 46 +++++ .../Internals/PropertyChangedObservable.cs | 50 +++++ .../Internals/PropertyObservable.cs | 157 +++++++++++++++ .../Internals/PropertyPathNode.cs | 1 - .../Internals/SimplePropertyObservable.cs | 103 ++++++++++ .../INotifyPropertyChangedExtensions.cs | 26 +-- .../Helpers/CollectionUtilities.cs | 4 +- .../FilteredReadOnlyObservableCollection.cs | 4 +- ...ervable.cs => PropertyObservableLegacy.cs} | 14 +- .../Internals/PropertyPathNodeLegacy.cs | 179 ++++++++++++++++++ .../INotifyPropertyChangedExtensionsTest.cs | 21 +- .../Internals/PropertyObservableTest.cs | 2 +- 13 files changed, 626 insertions(+), 33 deletions(-) create mode 100644 Source/ReactiveProperty.Core/Extensions/INotifyPropertyChangedExtensions.cs create mode 100644 Source/ReactiveProperty.Core/Internals/NestedPropertyObservable.cs create mode 100644 Source/ReactiveProperty.Core/Internals/PropertyChangedObservable.cs create mode 100644 Source/ReactiveProperty.Core/Internals/PropertyObservable.cs rename Source/{ReactiveProperty.NETStandard => ReactiveProperty.Core}/Internals/PropertyPathNode.cs (99%) create mode 100644 Source/ReactiveProperty.Core/Internals/SimplePropertyObservable.cs rename Source/ReactiveProperty.NETStandard/Internals/{PropertyObservable.cs => PropertyObservableLegacy.cs} (69%) create mode 100644 Source/ReactiveProperty.NETStandard/Internals/PropertyPathNodeLegacy.cs diff --git a/Source/ReactiveProperty.Core/Extensions/INotifyPropertyChangedExtensions.cs b/Source/ReactiveProperty.Core/Extensions/INotifyPropertyChangedExtensions.cs new file mode 100644 index 00000000..d074c226 --- /dev/null +++ b/Source/ReactiveProperty.Core/Extensions/INotifyPropertyChangedExtensions.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.ComponentModel; +using Reactive.Bindings.Internals; +using System.Linq.Expressions; + +namespace Reactive.Bindings.Extensions; + +/// +/// Extension methods for +/// +public static class INotifyPropertyChangedExtensions +{ + /// + /// Converts PropertyChanged to an observable sequence. + /// + public static IObservable PropertyChangedAsObservable(this T subject) + where T : INotifyPropertyChanged => + new PropertyChangedObservable(subject); + + /// + /// Converts NotificationObject's property changed to an observable sequence. + /// + /// The type of the subject. + /// The type of the property. + /// The subject. + /// Argument is self, Return is target property. + /// Push current value on first subscribe. + /// + public static IObservable ObserveProperty( + this TSubject subject, Expression> propertySelector, + bool isPushCurrentValueAtFirst = true) + where TSubject : INotifyPropertyChanged + { + return ExpressionTreeUtils.IsNestedPropertyPath(propertySelector) ? + ObserveNestedProperty(subject, propertySelector, isPushCurrentValueAtFirst) : + ObserveSimpleProperty(subject, propertySelector, isPushCurrentValueAtFirst); + } + + internal static IObservable ObserveSimpleProperty( + this TSubject subject, Expression> propertySelector, + bool isPushCurrentValueAtFirst = true) + where TSubject : INotifyPropertyChanged => + new SimplePropertyObservable(subject, propertySelector, isPushCurrentValueAtFirst); + + internal static IObservable ObserveNestedProperty( + this TSubject subject, Expression> propertySelector, + bool isPushCurrentValueAtFirst = true) + where TSubject : INotifyPropertyChanged => + new NestedPropertyObservable(subject, propertySelector, isPushCurrentValueAtFirst); +} diff --git a/Source/ReactiveProperty.Core/Internals/NestedPropertyObservable.cs b/Source/ReactiveProperty.Core/Internals/NestedPropertyObservable.cs new file mode 100644 index 00000000..fd2cecac --- /dev/null +++ b/Source/ReactiveProperty.Core/Internals/NestedPropertyObservable.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Text; +using Reactive.Bindings.Disposables; + +namespace Reactive.Bindings.Internals; +internal class NestedPropertyObservable : IObservable + where TSubject : INotifyPropertyChanged +{ + private readonly TSubject _subject; + private readonly Expression> _propertySelector; + private readonly bool _isPushCurrentValueAtFirst; + + public NestedPropertyObservable( + TSubject subject, + Expression> propertySelector, + bool isPushCurrentValueAtFirst) + { + _subject = subject; + _propertySelector = propertySelector; + _isPushCurrentValueAtFirst = isPushCurrentValueAtFirst; + } + + public IDisposable Subscribe(IObserver observer) + { + var propertyObserver = PropertyObservable.CreateFromPropertySelector(_subject, _propertySelector); + if (_isPushCurrentValueAtFirst) + { + try + { + observer.OnNext(propertyObserver.GetPropertyPathValue()); + } + catch (Exception ex) + { + observer.OnError(ex); + propertyObserver.Dispose(); + return InternalDisposable.Empty; + } + } + + var disposable = propertyObserver.Subscribe(observer); + return new CompositeDisposable(propertyObserver, disposable); + } +} diff --git a/Source/ReactiveProperty.Core/Internals/PropertyChangedObservable.cs b/Source/ReactiveProperty.Core/Internals/PropertyChangedObservable.cs new file mode 100644 index 00000000..b2c601d9 --- /dev/null +++ b/Source/ReactiveProperty.Core/Internals/PropertyChangedObservable.cs @@ -0,0 +1,50 @@ +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Reactive.Bindings.Internals; + +internal class PropertyChangedObservable : IObservable +{ + private readonly INotifyPropertyChanged _notifyPropertyChanged; + + public PropertyChangedObservable(INotifyPropertyChanged notifyPropertyChanged) + { + _notifyPropertyChanged = notifyPropertyChanged; + } + + public IDisposable Subscribe(IObserver observer) => + new Subscription(_notifyPropertyChanged, observer); + + private class Subscription : IDisposable + { + private INotifyPropertyChanged _notifyPropertyChanged; + private IObserver _observer; + private bool _isDisposed; + + public Subscription(INotifyPropertyChanged notifyPropertyChanged, IObserver observer) + { + _notifyPropertyChanged = notifyPropertyChanged; + _observer = observer; + + _notifyPropertyChanged.PropertyChanged += PropertyChanged; + } + + private void PropertyChanged(object? _, PropertyChangedEventArgs e) + { + if (_isDisposed) throw new InvalidOperationException(); + _observer.OnNext(e); + } + + public void Dispose() + { + if (_isDisposed) return; + + _observer.OnCompleted(); + _isDisposed = true; + _notifyPropertyChanged!.PropertyChanged -= PropertyChanged; + _notifyPropertyChanged = null!; + _observer = null!; + } + } +} diff --git a/Source/ReactiveProperty.Core/Internals/PropertyObservable.cs b/Source/ReactiveProperty.Core/Internals/PropertyObservable.cs new file mode 100644 index 00000000..18d4099d --- /dev/null +++ b/Source/ReactiveProperty.Core/Internals/PropertyObservable.cs @@ -0,0 +1,157 @@ +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Xml.Linq; + +namespace Reactive.Bindings.Internals; + +internal sealed class PropertyObservable : IObservable, IDisposable, IObserverLinkedList +{ + private ObserverNode? _root; + private ObserverNode? _last; + private bool _isDisposed; + private bool _onError; + + private PropertyPathNode? RootNode { get; set; } + public string Path => RootNode?.Path; + public bool SetPropertyPathValue(TProperty value) => RootNode?.SetPropertyPathValue(value) ?? false; + public void SetSource(INotifyPropertyChanged source) => RootNode?.UpdateSource(source); + + private void RaisePropertyChanged() + { + try + { + var value = GetPropertyPathValue(); + // call source.OnNext + var node = _root; + while (node != null) + { + node.OnNext(value); + node = node.Next; + } + } + catch (Exception ex) + { + _onError = true; + // call source.OnError + var node = _root; + while (node != null) + { + node.OnError(ex); + node = node.Next; + } + + Dispose(); + } + } + + internal void SetRootNode(PropertyPathNode rootNode) + { + RootNode?.SetCallback(null); + rootNode?.SetCallback(RaisePropertyChanged); + RootNode = rootNode; + } + + public TProperty GetPropertyPathValue() + { + var value = RootNode?.GetPropertyPathValue(); + return value != null ? (TProperty)value : default!; + } + + + + public void Dispose() + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + var node = _root; + _root = _last = null; + + if (_onError is false) + { + while (node != null) + { + try + { + node.OnCompleted(); + } + catch + { + // noop + } + + node = node.Next; + } + } + + RootNode?.Dispose(); + RootNode = null; + } + + public IDisposable Subscribe(IObserver observer) + { + if (_isDisposed) + { + observer.OnCompleted(); + return InternalDisposable.Empty; + } + + // subscribe node, node as subscription. + var next = new ObserverNode(this, observer); + if (_root == null) + { + _root = _last = next; + } + else + { + _last!.Next = next; + next.Previous = _last; + _last = next; + } + + return next; + } + + void IObserverLinkedList.UnsubscribeNode(ObserverNode node) + { + if (node == _root) + { + _root = node.Next; + } + if (node == _last) + { + _last = node.Previous; + } + + if (node.Previous != null) + { + node.Previous.Next = node.Next; + } + if (node.Next != null) + { + node.Next.Previous = node.Previous; + } + } +} + +internal static class PropertyObservable +{ + public static PropertyObservable CreateFromPropertySelector(TSubject subject, Expression> propertySelector) + where TSubject : INotifyPropertyChanged + { + if (!(propertySelector.Body is MemberExpression current)) + { + throw new ArgumentException(); + } + + var result = new PropertyObservable(); + result.SetRootNode(PropertyPathNode.CreateFromPropertySelector(propertySelector)); + result.SetSource(subject); + return result; + } + +} diff --git a/Source/ReactiveProperty.NETStandard/Internals/PropertyPathNode.cs b/Source/ReactiveProperty.Core/Internals/PropertyPathNode.cs similarity index 99% rename from Source/ReactiveProperty.NETStandard/Internals/PropertyPathNode.cs rename to Source/ReactiveProperty.Core/Internals/PropertyPathNode.cs index e5c66344..708c72e8 100644 --- a/Source/ReactiveProperty.NETStandard/Internals/PropertyPathNode.cs +++ b/Source/ReactiveProperty.Core/Internals/PropertyPathNode.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel; using System.Linq.Expressions; -using System.Reactive; namespace Reactive.Bindings.Internals; diff --git a/Source/ReactiveProperty.Core/Internals/SimplePropertyObservable.cs b/Source/ReactiveProperty.Core/Internals/SimplePropertyObservable.cs new file mode 100644 index 00000000..2679e5ec --- /dev/null +++ b/Source/ReactiveProperty.Core/Internals/SimplePropertyObservable.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Text; + +namespace Reactive.Bindings.Internals; +internal class SimplePropertyObservable : IObservable + where TSubject : INotifyPropertyChanged +{ + private readonly TSubject _subject; + private readonly Expression> _propertySelector; + private readonly bool _isPushCurrentValueAtFirst; + private readonly Func _accessor; + private readonly string _propertyName; + + public SimplePropertyObservable(TSubject subject, + Expression> propertySelector, + bool isPushCurrentValueAtFirst = true) + { + _subject = subject; + _propertySelector = propertySelector; + _isPushCurrentValueAtFirst = isPushCurrentValueAtFirst; + _accessor = AccessorCache.LookupGet(propertySelector, out _propertyName); + } + + public IDisposable Subscribe(IObserver observer) + { + if (_isPushCurrentValueAtFirst) + { + try + { + observer.OnNext(_accessor(_subject)); + } + catch (Exception ex) + { + observer.OnError(ex); + return InternalDisposable.Empty; + } + } + + return new Subscription(_subject, _accessor, _propertyName, observer); + } + + private class Subscription : IDisposable + { + private TSubject _subject; + private Func _accessor; + private readonly string _propertyName; + private IObserver _observer; + private bool _isDisposed; + private bool _onError; + + public Subscription(TSubject subject, Func accessor, string propertyName, IObserver observer) + { + _subject = subject; + _accessor = accessor; + _propertyName = propertyName; + _observer = observer; + _subject.PropertyChanged += PropertyChanged; + } + + private void PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (_isDisposed) throw new InvalidOperationException(); + if (e.PropertyName != _propertyName && !string.IsNullOrEmpty(e.PropertyName)) return; + + try + { + _observer.OnNext(_accessor(_subject)); + } + catch (Exception ex) + { + _onError = true; + _observer.OnError(ex); + Dispose(); + } + } + + public void Dispose() + { + if (_isDisposed) return; + + _isDisposed = true; + if (_onError is false) + { + try + { + _observer.OnCompleted(); + } + catch + { + // noop + } + } + + _subject.PropertyChanged -= PropertyChanged; + _subject = default!; + _observer = null!; + _accessor = null!; + } + } +} diff --git a/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs b/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs index 400cfd6a..4d7b3ca3 100644 --- a/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs +++ b/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs @@ -15,7 +15,8 @@ public static class INotifyPropertyChangedExtensions /// /// Converts PropertyChanged to an observable sequence. /// - public static IObservable PropertyChangedAsObservable(this T subject) + [EditorBrowsable(EditorBrowsableState.Never)] + public static IObservable PropertyChangedAsObservableLegacy(this T subject) where T : INotifyPropertyChanged => InternalObservable.FromEvent( h => (sender, e) => h(e), @@ -31,17 +32,18 @@ public static IObservable PropertyChangedAsObservable< /// Argument is self, Return is target property. /// Push current value on first subscribe. /// - public static IObservable ObserveProperty( + [EditorBrowsable(EditorBrowsableState.Never)] + public static IObservable ObservePropertyLegacy( this TSubject subject, Expression> propertySelector, bool isPushCurrentValueAtFirst = true) where TSubject : INotifyPropertyChanged { return ExpressionTreeUtils.IsNestedPropertyPath(propertySelector) ? - ObserveNestedProperty(subject, propertySelector, isPushCurrentValueAtFirst) : - ObserveSimpleProperty(subject, propertySelector, isPushCurrentValueAtFirst); + ObserveNestedPropertyLegacy(subject, propertySelector, isPushCurrentValueAtFirst) : + ObserveSimplePropertyLegacy(subject, propertySelector, isPushCurrentValueAtFirst); } - private static IObservable ObserveSimpleProperty( + private static IObservable ObserveSimplePropertyLegacy( this TSubject subject, Expression> propertySelector, bool isPushCurrentValueAtFirst = true) where TSubject : INotifyPropertyChanged @@ -54,19 +56,19 @@ private static IObservable ObserveSimpleProperty var flag = isFirst; isFirst = false; - var q = subject.PropertyChangedAsObservable() + var q = subject.PropertyChangedAsObservableLegacy() .Where(e => e.PropertyName == propertyName || string.IsNullOrEmpty(e.PropertyName)) .Select(_ => accessor.Invoke(subject)); return (isPushCurrentValueAtFirst && flag) ? q.StartWith(accessor.Invoke(subject)) : q; }); } - private static IObservable ObserveNestedProperty( + private static IObservable ObserveNestedPropertyLegacy( this TSubject subject, Expression> propertySelector, bool isPushCurrentValueAtFirst = true) where TSubject : INotifyPropertyChanged { - var propertyObserver = PropertyObservable.CreateFromPropertySelector(subject, propertySelector); + var propertyObserver = PropertyObservableLegacy.CreateFromPropertySelector(subject, propertySelector); var ox = Observable.Using(() => propertyObserver, x => x); return isPushCurrentValueAtFirst ? ox.StartWith(propertyObserver.GetPropertyPathValue()) : @@ -114,7 +116,7 @@ public static ReactiveProperty ToReactivePropertyAsSynchronized observer, x => x) .ToReactiveProperty(raiseEventScheduler, initialValue: observer.GetPropertyPathValue(), mode: mode); result @@ -125,7 +127,7 @@ public static ReactiveProperty ToReactivePropertyAsSynchronized.LookupGet(propertySelector, out var _); - var result = subject.ObserveSimpleProperty(propertySelector, isPushCurrentValueAtFirst: false) + var result = subject.ObserveSimplePropertyLegacy(propertySelector, isPushCurrentValueAtFirst: false) .ToReactiveProperty(raiseEventScheduler, initialValue: getter(subject), mode: mode); var setter = AccessorCache.LookupSet(propertySelector, out _); result @@ -241,7 +243,7 @@ public static ReactiveProperty ToReactivePropertyAsSynchronized observer, x => x) .StartWith(observer.GetPropertyPathValue())) .ToReactiveProperty(raiseEventScheduler, mode: mode); @@ -252,7 +254,7 @@ public static ReactiveProperty ToReactivePropertyAsSynchronized.LookupSet(propertySelector, out _); - var result = convert(subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true)) + var result = convert(subject.ObservePropertyLegacy(propertySelector, isPushCurrentValueAtFirst: true)) .ToReactiveProperty(raiseEventScheduler, mode: mode); convertBack(result.Where(_ => !ignoreValidationErrorValue || !result.HasErrors)) .Subscribe(x => setter(subject, x)); diff --git a/Source/ReactiveProperty.NETStandard/Helpers/CollectionUtilities.cs b/Source/ReactiveProperty.NETStandard/Helpers/CollectionUtilities.cs index 01672d09..bdc6d934 100644 --- a/Source/ReactiveProperty.NETStandard/Helpers/CollectionUtilities.cs +++ b/Source/ReactiveProperty.NETStandard/Helpers/CollectionUtilities.cs @@ -52,7 +52,7 @@ public static IObservable> ObserveElementPrope return ObserveElementCore> ( source, - (x, observer) => x.ObserveProperty(propertySelector, isPushCurrentValueAtFirst).Subscribe(y => + (x, observer) => x.ObservePropertyLegacy(propertySelector, isPushCurrentValueAtFirst).Subscribe(y => { var pair = PropertyPack.Create(x, propertyInfo, y); observer.OnNext(pair); @@ -131,7 +131,7 @@ public static IObservable> ( source, - (x, observer) => x.PropertyChangedAsObservable().Subscribe(y => + (x, observer) => x.PropertyChangedAsObservableLegacy().Subscribe(y => { var pair = SenderEventArgsPair.Create(x, y); observer.OnNext(pair); diff --git a/Source/ReactiveProperty.NETStandard/Helpers/FilteredReadOnlyObservableCollection.cs b/Source/ReactiveProperty.NETStandard/Helpers/FilteredReadOnlyObservableCollection.cs index f2febb17..c068bf39 100644 --- a/Source/ReactiveProperty.NETStandard/Helpers/FilteredReadOnlyObservableCollection.cs +++ b/Source/ReactiveProperty.NETStandard/Helpers/FilteredReadOnlyObservableCollection.cs @@ -456,7 +456,7 @@ public static IFilteredReadOnlyObservableCollection ToFilteredReadOnlyObserva new FilteredReadOnlyObservableCollection, T, PropertyChangedEventArgs>( self, filter, - x => x.PropertyChangedAsObservable()); + x => x.PropertyChangedAsObservableLegacy()); /// /// create IFilteredReadOnlyObservableCollection from ReadOnlyObservableCollection @@ -470,7 +470,7 @@ public static IFilteredReadOnlyObservableCollection ToFilteredReadOnlyObserva new FilteredReadOnlyObservableCollection, T, PropertyChangedEventArgs>( self, filter, - x => x.PropertyChangedAsObservable()); + x => x.PropertyChangedAsObservableLegacy()); /// /// create IFilteredReadOnlyObservableCollection from ReadOnlyObservableCollection diff --git a/Source/ReactiveProperty.NETStandard/Internals/PropertyObservable.cs b/Source/ReactiveProperty.NETStandard/Internals/PropertyObservableLegacy.cs similarity index 69% rename from Source/ReactiveProperty.NETStandard/Internals/PropertyObservable.cs rename to Source/ReactiveProperty.NETStandard/Internals/PropertyObservableLegacy.cs index 1ed30b28..54a3054c 100644 --- a/Source/ReactiveProperty.NETStandard/Internals/PropertyObservable.cs +++ b/Source/ReactiveProperty.NETStandard/Internals/PropertyObservableLegacy.cs @@ -5,10 +5,10 @@ namespace Reactive.Bindings.Internals; -internal sealed class PropertyObservable : IObservable, IDisposable +internal sealed class PropertyObservableLegacy : IObservable, IDisposable { - private PropertyPathNode? RootNode { get; set; } - internal void SetRootNode(PropertyPathNode rootNode) + private PropertyPathNodeLegacy? RootNode { get; set; } + internal void SetRootNode(PropertyPathNodeLegacy rootNode) { RootNode?.SetCallback(null); rootNode?.SetCallback(RaisePropertyChanged); @@ -38,9 +38,9 @@ public void Dispose() public IDisposable Subscribe(IObserver observer) => _propertyChangedSource.Subscribe(observer); } -internal static class PropertyObservable +internal static class PropertyObservableLegacy { - public static PropertyObservable CreateFromPropertySelector(TSubject subject, Expression> propertySelector) + public static PropertyObservableLegacy CreateFromPropertySelector(TSubject subject, Expression> propertySelector) where TSubject : INotifyPropertyChanged { if (!(propertySelector.Body is MemberExpression current)) @@ -48,8 +48,8 @@ public static PropertyObservable CreateFromPropertySelector(); - result.SetRootNode(PropertyPathNode.CreateFromPropertySelector(propertySelector)); + var result = new PropertyObservableLegacy(); + result.SetRootNode(PropertyPathNodeLegacy.CreateFromPropertySelector(propertySelector)); result.SetSource(subject); return result; } diff --git a/Source/ReactiveProperty.NETStandard/Internals/PropertyPathNodeLegacy.cs b/Source/ReactiveProperty.NETStandard/Internals/PropertyPathNodeLegacy.cs new file mode 100644 index 00000000..3071da11 --- /dev/null +++ b/Source/ReactiveProperty.NETStandard/Internals/PropertyPathNodeLegacy.cs @@ -0,0 +1,179 @@ +using System; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reactive; + +namespace Reactive.Bindings.Internals; + +internal class PropertyPathNodeLegacy : IDisposable +{ + private bool _isDisposed = false; + private Action? _callback; + private Delegate? _getAccessor; + private Delegate? _setAccessor; + + public event EventHandler? PropertyChanged; + + public PropertyPathNodeLegacy(string propertyName) + { + PropertyName = propertyName; + } + + public string PropertyName { get; } + public object? Source { get; private set; } + private Type? PrevSourceType { get; set; } + public PropertyPathNodeLegacy? Next { get; private set; } + public PropertyPathNodeLegacy? Prev { get; private set; } + public void SetCallback(Action? callback) + { + _callback = callback; + Next?.SetCallback(callback); + } + + public PropertyPathNodeLegacy InsertBefore(string propertyName) + { + if (Prev != null) + { + Prev.Next = null; + } + + Prev = new PropertyPathNodeLegacy(propertyName); + Prev.Next = this; + return Prev; + } + + public void UpdateSource(object? source) + { + EnsureDispose(); + Cleanup(); + Source = source; + if (PrevSourceType != Source?.GetType()) + { + _getAccessor = null; + } + PrevSourceType = Source?.GetType(); + StartObservePropertyChanged(); + } + + private void StartObservePropertyChanged() + { + EnsureDispose(); + if (Source == null) { return; } + if (Source is INotifyPropertyChanged inpc) + { + inpc.PropertyChanged += SourcePropertyChangedEventHandler; + } + Next?.UpdateSource(GetPropertyValue()); + } + + private object? GetPropertyValue() + { + EnsureDispose(); + if (Source == null) return null; + return (_getAccessor ?? (_getAccessor = AccessorCache.LookupGet(Source.GetType(), PropertyName))) + .DynamicInvoke(Source); + } + + public object? GetPropertyPathValue() + { + if (Source == null) + { + return null; + } + + if (Next != null) + { + return Next.GetPropertyPathValue(); + } + + return GetPropertyValue(); + } + + public bool SetPropertyPathValue(object? value) + { + if (Source == null) + { + return false; + } + + if (Next != null) + { + return Next.SetPropertyPathValue(value); + } + else + { + var setter = _setAccessor ?? (_setAccessor = AccessorCache.LookupSet(Source.GetType(), PropertyName)); + setter.DynamicInvoke(Source, value); + return true; + } + } + + public string Path => $"{PropertyName}{(string.IsNullOrEmpty(Next?.Path) ? "" : $".{Next?.Path}")}"; + + public override string ToString() => Path; + + private void SourcePropertyChangedEventHandler(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == PropertyName || string.IsNullOrEmpty(e.PropertyName)) + { + Next?.UpdateSource(GetPropertyValue()); + _callback?.Invoke(); + } + } + + private void Cleanup() + { + if (Source != null) + { + if (Source is INotifyPropertyChanged inpc) + { + inpc.PropertyChanged -= SourcePropertyChangedEventHandler; + } + Source = null; + } + + Next?.Cleanup(); + } + + public void Dispose() + { + _isDisposed = true; + Cleanup(); + } + + private void EnsureDispose() + { + if (_isDisposed) { throw new ObjectDisposedException(nameof(PropertyPathNodeLegacy)); } + } + + private void RaisePropertyChanged() => PropertyChanged?.Invoke(this, EventArgs.Empty); + + + public static PropertyPathNodeLegacy CreateFromPropertySelector( + Expression> propertySelector) + { + if (!(propertySelector.Body is MemberExpression memberExpression)) + { + throw new ArgumentException(); + } + + var node = default(PropertyPathNodeLegacy); + MemberExpression? current = memberExpression; + while (current != null) + { + var propertyName = current.Member.Name; + if (node != null) + { + node = node.InsertBefore(propertyName); + } + else + { + node = new PropertyPathNodeLegacy(propertyName); + } + current = current.Expression as MemberExpression; + } + + if (node == null) throw new ArgumentException($"Can not parse property path expression {propertySelector}."); + return node; + } +} diff --git a/Test/ReactiveProperty.NETStandard.Tests/Extensions/INotifyPropertyChangedExtensionsTest.cs b/Test/ReactiveProperty.NETStandard.Tests/Extensions/INotifyPropertyChangedExtensionsTest.cs index 70b21a29..d3ce61fc 100644 --- a/Test/ReactiveProperty.NETStandard.Tests/Extensions/INotifyPropertyChangedExtensionsTest.cs +++ b/Test/ReactiveProperty.NETStandard.Tests/Extensions/INotifyPropertyChangedExtensionsTest.cs @@ -56,19 +56,23 @@ public void ObservePropertyException() var m = new Model() { Name = "aaa" }; m.ObserveProperty(x => x.Name) + .ObserveOn(testScheduler) .Do(x => recorder.OnNext(x)) .Do(_ => { throw commonEx; }) .OnErrorRetry((Exception e) => recorder.OnError(e)) .Subscribe(); - testScheduler.AdvanceTo(1000); + testScheduler.AdvanceTo(1); m.Name = "bbb"; + testScheduler.AdvanceTo(3); recorder.Messages.Is( - OnNext(0, "aaa"), - OnError(0, commonEx), - OnNext(1000, "bbb"), - OnError(1000, commonEx)); + OnNext(1, "aaa"), + OnError(1, commonEx), + OnNext(2, "aaa"), + OnError(2, commonEx), + OnNext(3, "bbb"), + OnError(3, commonEx)); } [TestMethod] @@ -80,17 +84,18 @@ public void ObservePropertyExceptionFalse() var m = new Model() { Name = "aaa" }; m.ObserveProperty(x => x.Name, false) + .ObserveOn(testScheduler) .Do(x => recorder.OnNext(x)) .Do(_ => { throw commonEx; }) .OnErrorRetry((Exception e) => recorder.OnError(e)) .Subscribe(); - testScheduler.AdvanceTo(1000); m.Name = "bbb"; + testScheduler.AdvanceTo(1); recorder.Messages.Is( - OnNext(1000, "bbb"), - OnError(1000, commonEx)); + OnNext(1, "bbb"), + OnError(1, commonEx)); } [TestMethod] diff --git a/Test/ReactiveProperty.NETStandard.Tests/Internals/PropertyObservableTest.cs b/Test/ReactiveProperty.NETStandard.Tests/Internals/PropertyObservableTest.cs index 76d70bb6..cf3591de 100644 --- a/Test/ReactiveProperty.NETStandard.Tests/Internals/PropertyObservableTest.cs +++ b/Test/ReactiveProperty.NETStandard.Tests/Internals/PropertyObservableTest.cs @@ -37,7 +37,7 @@ public void ObserveProperty() path.Dispose(); item.IsPropertyChangedEmpty.IsTrue(); item.Child.Value = 100; - testObserver.Messages.Is(OnNext(0, 1)); + testObserver.Messages.Is(OnNext(0, 1), OnCompleted(0)); } [TestMethod] From c8019dd8830aff33822c2ff474f0e21f82353ed0 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 5 Jan 2023 00:40:35 +0900 Subject: [PATCH 26/32] Move ToReactivePropertySlimAsSynchronized to Core project Add TinyLinq namespace for Select and Where --- .../INotifyPropertyChangedExtensions.cs | 113 +++++++++++++++++- .../Internals/DelegateObserver.cs | 12 +- .../TinyLinq/ObservableExtensions.cs | 81 +++++++++++++ .../INotifyPropertyChangedExtensions.cs | 105 ---------------- 4 files changed, 198 insertions(+), 113 deletions(-) create mode 100644 Source/ReactiveProperty.Core/TinyLinq/ObservableExtensions.cs diff --git a/Source/ReactiveProperty.Core/Extensions/INotifyPropertyChangedExtensions.cs b/Source/ReactiveProperty.Core/Extensions/INotifyPropertyChangedExtensions.cs index d074c226..25e84609 100644 --- a/Source/ReactiveProperty.Core/Extensions/INotifyPropertyChangedExtensions.cs +++ b/Source/ReactiveProperty.Core/Extensions/INotifyPropertyChangedExtensions.cs @@ -1,8 +1,7 @@ using System; -using System.Collections.Generic; -using System.Text; using System.ComponentModel; using Reactive.Bindings.Internals; +using Reactive.Bindings.TinyLinq; using System.Linq.Expressions; namespace Reactive.Bindings.Extensions; @@ -38,6 +37,116 @@ public static IObservable ObserveProperty( ObserveSimpleProperty(subject, propertySelector, isPushCurrentValueAtFirst); } + /// + /// Converts NotificationObject's property to ReactivePropertySlim. Value is two-way synchronized. + /// PropertyChanged raise on selected scheduler. + /// + /// The type of the subject. + /// The type of the property. + /// The subject. + /// Argument is self, Return is target property. + /// ReactiveProperty mode. + /// + public static ReactivePropertySlim ToReactivePropertySlimAsSynchronized( + this TSubject subject, + Expression> propertySelector, + ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe) + where TSubject : INotifyPropertyChanged + { + if (ExpressionTreeUtils.IsNestedPropertyPath(propertySelector)) + { + var observer = PropertyObservable.CreateFromPropertySelector(subject, propertySelector); + var result = new ReactivePropertySlim(observer.GetPropertyPathValue(), mode: mode); + var disposable = observer.Subscribe(new DelegateObserver(x => result.Value = x)); + result.Subscribe(new DelegateObserver(x => observer.SetPropertyPathValue(x), _ => disposable.Dispose(), () => disposable.Dispose())); + return result; + } + else + { + var setter = AccessorCache.LookupSet(propertySelector, out _); + var result = new ReactivePropertySlim(mode: mode); + var disposable = subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true) + .Subscribe(new DelegateObserver(x => result.Value = x)); + result.Subscribe(new DelegateObserver(x => setter(subject, x), _ => disposable.Dispose(), () => disposable.Dispose())); + return result; + } + } + + /// + /// Converts NotificationObject's property to ReactivePropertySlim. Value is two-way synchronized. + /// PropertyChanged raise on selected scheduler. + /// + /// The type of the subject. + /// The type of the property. + /// The type of the result. + /// The subject. + /// Argument is self, Return is target property. + /// Convert selector to ReactiveProperty. + /// Convert selector to source. + /// ReactiveProperty mode. + /// + public static ReactivePropertySlim ToReactivePropertySlimAsSynchronized( + this TSubject subject, + Expression> propertySelector, + Func convert, + Func convertBack, + ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe) + where TSubject : INotifyPropertyChanged => + ToReactivePropertySlimAsSynchronized(subject, propertySelector, + ox => ox.Select(convert), + ox => ox.Select(convertBack), + mode); + + /// + /// Converts NotificationObject's property to ReactiveProperty. Value is two-way synchronized. + /// PropertyChanged raise on selected scheduler. + /// + /// The type of the subject. + /// The type of the property. + /// The type of the result. + /// The subject. + /// Argument is self, Return is target property. + /// Convert selector to ReactiveProperty. + /// Convert selector to source. + /// ReactiveProperty mode. + /// + public static ReactivePropertySlim ToReactivePropertySlimAsSynchronized( + this TSubject subject, + Expression> propertySelector, + Func, IObservable> convert, + Func, IObservable> convertBack, + ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe) + where TSubject : INotifyPropertyChanged + { + if (ExpressionTreeUtils.IsNestedPropertyPath(propertySelector)) + { + var result = new ReactivePropertySlim(mode: mode); + var observer = PropertyObservable.CreateFromPropertySelector(subject, propertySelector); + var disposable = convert(subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true)) + .Subscribe(new DelegateObserver(x => result.Value = x)); + convertBack(result) + .Subscribe(new DelegateObserver( + x => observer.SetPropertyPathValue(x), + _ => disposable.Dispose(), + () => disposable.Dispose())); + return result; + } + else + { + var setter = AccessorCache.LookupSet(propertySelector, out _); + var result = new ReactivePropertySlim(mode: mode); + var disposable = convert(subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true)) + .Subscribe(new DelegateObserver(x => result.Value = x)); + convertBack(result) + .Subscribe(new DelegateObserver( + x => setter(subject, x), + _ => disposable.Dispose(), + () => disposable.Dispose())); + return result; + } + } + + internal static IObservable ObserveSimpleProperty( this TSubject subject, Expression> propertySelector, bool isPushCurrentValueAtFirst = true) diff --git a/Source/ReactiveProperty.Core/Internals/DelegateObserver.cs b/Source/ReactiveProperty.Core/Internals/DelegateObserver.cs index 6adfdb70..f82da17c 100644 --- a/Source/ReactiveProperty.Core/Internals/DelegateObserver.cs +++ b/Source/ReactiveProperty.Core/Internals/DelegateObserver.cs @@ -10,18 +10,18 @@ internal class DelegateObserver : IObserver private readonly Action? _onCompleted; private readonly Action? _onError; - public DelegateObserver(Action onNext, Action? onCompleted, Action? onError) + public DelegateObserver(Action onNext, Action? onError, Action? onCompleted) { _onNext = onNext; - _onCompleted = onCompleted; _onError = onError; + _onCompleted = onCompleted; } public DelegateObserver(Action onNext) : this(onNext, null, null) { } - public DelegateObserver(Action onNext, Action onCompleted) : this(onNext, onCompleted, null) + public DelegateObserver(Action onNext, Action onCompleted) : this(onNext, null, onCompleted) { } @@ -40,18 +40,18 @@ internal class DelegateObserver : IObserver private readonly Action? _onCompleted; private readonly Action? _onError; - public DelegateObserver(Action onNext, Action? onCompleted, Action? onError) + public DelegateObserver(Action onNext, Action? onError, Action? onCompleted) { _onNext = onNext; - _onCompleted = onCompleted; _onError = onError; + _onCompleted = onCompleted; } public DelegateObserver(Action onNext) : this(onNext, null, null) { } - public DelegateObserver(Action onNext, Action onCompleted) : this(onNext, onCompleted, null) + public DelegateObserver(Action onNext, Action onCompleted) : this(onNext, null, onCompleted) { } diff --git a/Source/ReactiveProperty.Core/TinyLinq/ObservableExtensions.cs b/Source/ReactiveProperty.Core/TinyLinq/ObservableExtensions.cs new file mode 100644 index 00000000..ba0fa9ae --- /dev/null +++ b/Source/ReactiveProperty.Core/TinyLinq/ObservableExtensions.cs @@ -0,0 +1,81 @@ +using System; +using Reactive.Bindings.Internals; + +namespace Reactive.Bindings.TinyLinq; + +public static class ObservableExtensions +{ + public static IObservable Select(this IObservable self, Func selector) => + new SelectObservable(self, selector); + + public static IObservable Where(this IObservable self, Func filter) => + new WhereObservable(self, filter); +} + +internal class SelectObservable : IObservable +{ + private readonly IObservable _source; + private readonly Func _selector; + + public SelectObservable(IObservable source, Func selector) + { + _source = source; + _selector = selector; + } + + public IDisposable Subscribe(IObserver observer) => + new Subscription(_source, _selector, observer); + + class Subscription : IDisposable + { + private readonly IDisposable _disposable; + + public Subscription(IObservable source, Func selector, IObserver observer) + { + _disposable = source.Subscribe(new DelegateObserver( + x => observer.OnNext(selector(x)), + observer.OnError, + observer.OnCompleted)); + } + + public void Dispose() => _disposable.Dispose(); + } +} + +internal class WhereObservable : IObservable +{ + private readonly IObservable _source; + private readonly Func _filter; + + public WhereObservable(IObservable source, Func filter) + { + _source = source; + _filter = filter; + } + + public IDisposable Subscribe(IObserver observer) + { + return new Subscription(_source, _filter, observer); + } + + class Subscription : IDisposable + { + private readonly IDisposable _disposable; + + public Subscription(IObservable source, Func filter, IObserver observer) + { + _disposable = source.Subscribe(new DelegateObserver( + x => + { + if (filter(x)) + { + observer.OnNext(x); + } + }, + observer.OnError, + observer.OnCompleted)); + } + + public void Dispose() => _disposable.Dispose(); + } +} diff --git a/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs b/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs index 4d7b3ca3..dfc72d1a 100644 --- a/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs +++ b/Source/ReactiveProperty.NETStandard/Extensions/INotifyPropertyChangedExtensions.cs @@ -261,109 +261,4 @@ public static ReactiveProperty ToReactivePropertyAsSynchronized - /// Converts NotificationObject's property to ReactivePropertySlim. Value is two-way synchronized. - /// PropertyChanged raise on selected scheduler. - /// - /// The type of the subject. - /// The type of the property. - /// The subject. - /// Argument is self, Return is target property. - /// ReactiveProperty mode. - /// - public static ReactivePropertySlim ToReactivePropertySlimAsSynchronized( - this TSubject subject, - Expression> propertySelector, - ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe) - where TSubject : INotifyPropertyChanged - { - if (ExpressionTreeUtils.IsNestedPropertyPath(propertySelector)) - { - var result = new ReactivePropertySlim(mode: mode); - var observer = PropertyObservable.CreateFromPropertySelector(subject, propertySelector); - var disposable = Observable.Using(() => observer, x => x) - .StartWith(observer.GetPropertyPathValue()) - .Subscribe(x => result.Value = x); - result.Subscribe(x => observer.SetPropertyPathValue(x), _ => disposable.Dispose(), () => disposable.Dispose()); - return result; - } - else - { - var setter = AccessorCache.LookupSet(propertySelector, out _); - var result = new ReactivePropertySlim(mode: mode); - var disposable = subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true) - .Subscribe(x => result.Value = x); - result.Subscribe(x => setter(subject, x), _ => disposable.Dispose(), () => disposable.Dispose()); - return result; - } - } - - /// - /// Converts NotificationObject's property to ReactivePropertySlim. Value is two-way synchronized. - /// PropertyChanged raise on selected scheduler. - /// - /// The type of the subject. - /// The type of the property. - /// The type of the result. - /// The subject. - /// Argument is self, Return is target property. - /// Convert selector to ReactiveProperty. - /// Convert selector to source. - /// ReactiveProperty mode. - /// - public static ReactivePropertySlim ToReactivePropertySlimAsSynchronized( - this TSubject subject, - Expression> propertySelector, - Func convert, - Func convertBack, - ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe) - where TSubject : INotifyPropertyChanged => - ToReactivePropertySlimAsSynchronized(subject, propertySelector, - ox => ox.Select(convert), - ox => ox.Select(convertBack), - mode); - - /// - /// Converts NotificationObject's property to ReactiveProperty. Value is two-way synchronized. - /// PropertyChanged raise on selected scheduler. - /// - /// The type of the subject. - /// The type of the property. - /// The type of the result. - /// The subject. - /// Argument is self, Return is target property. - /// Convert selector to ReactiveProperty. - /// Convert selector to source. - /// ReactiveProperty mode. - /// - public static ReactivePropertySlim ToReactivePropertySlimAsSynchronized( - this TSubject subject, - Expression> propertySelector, - Func, IObservable> convert, - Func, IObservable> convertBack, - ReactivePropertyMode mode = ReactivePropertyMode.DistinctUntilChanged | ReactivePropertyMode.RaiseLatestValueOnSubscribe) - where TSubject : INotifyPropertyChanged - { - if (ExpressionTreeUtils.IsNestedPropertyPath(propertySelector)) - { - var result = new ReactivePropertySlim(mode: mode); - var observer = PropertyObservable.CreateFromPropertySelector(subject, propertySelector); - var disposable = convert(subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true)) - .Subscribe(x => result.Value = x); - convertBack(result) - .Subscribe(x => observer.SetPropertyPathValue(x), _ => disposable.Dispose(), () => disposable.Dispose()); - return result; - } - else - { - var setter = AccessorCache.LookupSet(propertySelector, out _); - var result = new ReactivePropertySlim(mode: mode); - var disposable = convert(subject.ObserveProperty(propertySelector, isPushCurrentValueAtFirst: true)) - .Subscribe(x => result.Value = x); - convertBack(result) - .Subscribe(x => setter(subject, x), _ => disposable.Dispose(), () => disposable.Dispose()); - return result; - } - } } From 11caafcbc54a70cc1c64bc365742bf64e2135f0e Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 5 Jan 2023 11:58:37 +0900 Subject: [PATCH 27/32] Add ValidatableReactiveProperty --- ReactiveProperty.lutconfig | 6 + .../Internals/InternalSubject.cs | 99 +++++ .../ReactiveProperty.Core.csproj | 4 + .../ValidatableReactiveProperty.cs | 351 ++++++++++++++++++ .../ReactiveProperty.cs | 5 - .../ValidatableReactivePropertyTest.cs | 166 +++++++++ 6 files changed, 626 insertions(+), 5 deletions(-) create mode 100644 ReactiveProperty.lutconfig create mode 100644 Source/ReactiveProperty.Core/Internals/InternalSubject.cs create mode 100644 Source/ReactiveProperty.Core/ValidatableReactiveProperty.cs create mode 100644 Test/ReactiveProperty.NETStandard.Tests/ValidatableReactivePropertyTest.cs diff --git a/ReactiveProperty.lutconfig b/ReactiveProperty.lutconfig new file mode 100644 index 00000000..596a8603 --- /dev/null +++ b/ReactiveProperty.lutconfig @@ -0,0 +1,6 @@ + + + true + true + 180000 + \ No newline at end of file diff --git a/Source/ReactiveProperty.Core/Internals/InternalSubject.cs b/Source/ReactiveProperty.Core/Internals/InternalSubject.cs new file mode 100644 index 00000000..bc9e5edd --- /dev/null +++ b/Source/ReactiveProperty.Core/Internals/InternalSubject.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Linq; + +namespace Reactive.Bindings.Internals; +internal class InternalSubject : IObservable, IObserver, IObserverLinkedList, IDisposable +{ + private bool _isDisposed; + private ObserverNode? _root; + private ObserverNode? _last; + + public void Dispose() => DisposeInternal(null); + + private void DisposeInternal(Exception? error) + { + if (_isDisposed) + { + return; + } + + var node = _root; + _root = _last = null; + _isDisposed = true; + while (node != null) + { + if (error is null) + { + node.OnCompleted(); + } + else + { + node.OnError(error); + } + + node = node.Next; + } + } + + public void OnCompleted() => DisposeInternal(null); + + public void OnError(Exception error) => DisposeInternal(error); + + public void OnNext(T value) + { + // call source.OnNext + var node = _root; + while (node != null) + { + node.OnNext(value); + node = node.Next; + } + } + + public IDisposable Subscribe(IObserver observer) + { + if (_isDisposed) + { + observer.OnCompleted(); + return InternalDisposable.Empty; + } + + // subscribe node, node as subscription. + var next = new ObserverNode(this, observer); + if (_root == null) + { + _root = _last = next; + } + else + { + _last!.Next = next; + next.Previous = _last; + _last = next; + } + + return next; + } + + void IObserverLinkedList.UnsubscribeNode(ObserverNode node) + { + if (node == _root) + { + _root = node.Next; + } + if (node == _last) + { + _last = node.Previous; + } + + if (node.Previous != null) + { + node.Previous.Next = node.Next; + } + if (node.Next != null) + { + node.Next.Previous = node.Previous; + } + } +} diff --git a/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj b/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj index 0f06730c..6336450d 100644 --- a/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj +++ b/Source/ReactiveProperty.Core/ReactiveProperty.Core.csproj @@ -8,4 +8,8 @@ key.snk ReactiveProperty.Core includes minimum core classes such as ReactivePropertySlim and ReadOnlyReactivePropertySlim. + + + + \ No newline at end of file diff --git a/Source/ReactiveProperty.Core/ValidatableReactiveProperty.cs b/Source/ReactiveProperty.Core/ValidatableReactiveProperty.cs new file mode 100644 index 00000000..a4e3789a --- /dev/null +++ b/Source/ReactiveProperty.Core/ValidatableReactiveProperty.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Reactive.Bindings.Internals; + +namespace Reactive.Bindings; + +internal class SingletonDataErrorsChangedEventArgs +{ + public static readonly DataErrorsChangedEventArgs Value = new(nameof(IReactiveProperty.Value)); +} + +/// +/// IReactiveProperty implementations with validation feature. +/// +/// The value type. +public class ValidatableReactiveProperty : IReactiveProperty, IObserverLinkedList +{ + private const int IsDisposedFlagNumber = 1 << 9; // (reserve 0 ~ 8) + private ReactivePropertyMode _mode; // None = 0, DistinctUntilChanged = 1, RaiseLatestValueOnSubscribe = 2, Disposed = (1 << 9) + private readonly IEqualityComparer _equalityComparer; + private ObserverNode? _root; + private ObserverNode? _last; + private string[] _errorMessages = Array.Empty(); + + private IDisposable _sourceSubscribe = default!; + private readonly InternalSubject _observeErrorChanged = new(); + private readonly InternalSubject _observeHasErrors = new(); + + private T _latestValue; + private Func[] _validators; + + /// + /// Return the first error message from GetErrors(). If HasErrors is false, then return empty string. + /// + public string ErrorMessage => HasErrors ? _errorMessages[0] : ""; + + /// + public T Value + { + get => _latestValue; + set => Validate(value); + } + + /// + /// Get the source IReactiveProperty. + /// + public IReactiveProperty Source { get; } + + /// + IObservable IHasErrors.ObserveErrorChanged => _observeErrorChanged; + + /// + /// Get the observe error changed. + /// + public IObservable ObserveErrorChanged => _observeErrorChanged; + + /// + public IObservable ObserveHasErrors => _observeHasErrors; + + /// + public bool HasErrors => _errorMessages.Length != 0; + + /// + object? IReactiveProperty.Value { get => Value; set => Value = (T)value!; } + + /// + T IReadOnlyReactiveProperty.Value => _latestValue; + + /// + object? IReadOnlyReactiveProperty.Value => _latestValue; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// true if this instance is disposed; otherwise, false. + public bool IsDisposed => (int)_mode == IsDisposedFlagNumber; + + /// + /// Gets a value indicating whether this instance is distinct until changed. + /// + /// true if this instance is distinct until changed; otherwise, false. + public bool IsDistinctUntilChanged => (_mode & ReactivePropertyMode.DistinctUntilChanged) == ReactivePropertyMode.DistinctUntilChanged; + + /// + /// Gets a value indicating whether this instance is raise latest value on subscribe. + /// + /// + /// true if this instance is raise latest value on subscribe; otherwise, false. + /// + public bool IsRaiseLatestValueOnSubscribe => (_mode & ReactivePropertyMode.RaiseLatestValueOnSubscribe) == ReactivePropertyMode.RaiseLatestValueOnSubscribe; + + /// + /// Gets a value indicating whether this instance is ignore first validation errors. + /// + /// + /// true if this instance ignore first validation errors; otherwise, false. + /// + public bool IsIgnoreInitialValidationError => (_mode & ReactivePropertyMode.IgnoreInitialValidationError) == ReactivePropertyMode.IgnoreInitialValidationError; + + /// + public event PropertyChangedEventHandler? PropertyChanged; + /// + public event EventHandler? ErrorsChanged; + + /// + /// Create a ValidatableReactiveProperty instance. + /// + /// The source IReactiveProperty + /// The validation logics. + /// The ReactivePropertyMode + /// The EqualityComparer for T + public ValidatableReactiveProperty(IReactiveProperty source, + IEnumerable> validators, + ReactivePropertyMode mode = ReactivePropertyMode.Default, + IEqualityComparer? equalityComparer = null) + { + if ((mode & ReactivePropertyMode.IgnoreException) == ReactivePropertyMode.IgnoreException) + { + throw new NotSupportedException("IgnoreException isn't supported in ValidatableReactiveProperty."); + } + + Source = source; + + _validators = validators.ToArray(); + _latestValue = Source.Value; + _mode = mode; + _equalityComparer = equalityComparer ?? EqualityComparer.Default; + InitializeValidationProcess(); + } + + /// + /// Create a ValidatableReactiveProperty instance. + /// + /// The source IReactiveProperty + /// The validation logic. + /// The ReactivePropertyMode + /// The EqualityComparer for T + public ValidatableReactiveProperty(IReactiveProperty source, + Func validator, + ReactivePropertyMode mode = ReactivePropertyMode.Default, + IEqualityComparer? equalityComparer = null) : + this(source, new[] { validator }, mode, equalityComparer) + { + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting + /// unmanaged resources. + /// + public void Dispose() + { + if (IsDisposed) + { + return; + } + + var node = _root; + _root = _last = null; + _mode = (ReactivePropertyMode)IsDisposedFlagNumber; + + while (node != null) + { + node.OnCompleted(); + node = node.Next; + } + + _sourceSubscribe?.Dispose(); + _observeErrorChanged.Dispose(); + _observeHasErrors.Dispose(); + } + + /// + public void ForceNotify() => Validate(_latestValue); + + /// + IEnumerable INotifyDataErrorInfo.GetErrors(string? propertyName) => _errorMessages; + + + /// + /// Get error messages. If HasErros is false, then return empty string array. + /// + /// The error messages. + public string[] GetErrors() => _errorMessages; + + /// + /// Notifies the provider that an observer is to receive notifications. + /// + /// The object that is to receive notifications. + /// + /// A reference to an interface that allows observers to stop receiving notifications before + /// the provider has finished sending them. + /// + public IDisposable Subscribe(IObserver observer) + { + if (IsDisposed) + { + observer.OnCompleted(); + return InternalDisposable.Empty; + } + + if (IsRaiseLatestValueOnSubscribe) + { + observer.OnNext(_latestValue); + } + + // subscribe node, node as subscription. + var next = new ObserverNode(this, observer); + if (_root == null) + { + _root = _last = next; + } + else + { + _last!.Next = next; + next.Previous = _last; + _last = next; + } + return next; + } + + private void InitializeValidationProcess() + { + _sourceSubscribe = Source.Subscribe(new DelegateObserver(x => + { + Validate(x, ValidationKind.RequestFromSoucre); + })); + + Validate(_latestValue, ValidationKind.FirstTime); + } + + private void Validate(T value, ValidationKind validationKind = ValidationKind.Default) + { + var previousHasErrors = HasErrors; + var previousErrors = _errorMessages; + + var isNotEquals = !_equalityComparer.Equals(_latestValue, value); + var needValidation = + (validationKind is ValidationKind.FirstTime || !IsIgnoreInitialValidationError) || + isNotEquals; + + _latestValue = value; + if (needValidation) + { + _errorMessages = _validators.Select(x => x(value)) + .Where(x => x is not null) + .ToArray()!; + } + + if (isNotEquals || IsDistinctUntilChanged is false) + { + NotifyOnNext(); + } + + if (isNotEquals) + { + PropertyChanged?.Invoke(this, SingletonPropertyChangedEventArgs.Value); + } + + if (!previousErrors.SequenceEqual(_errorMessages)) + { + ErrorsChanged?.Invoke(this, SingletonDataErrorsChangedEventArgs.Value); + _observeErrorChanged.OnNext(_errorMessages); + } + + if (previousHasErrors != HasErrors) + { + _observeHasErrors.OnNext(HasErrors); + } + + if (validationKind is not ValidationKind.RequestFromSoucre && HasErrors is false) + { + Source.Value = _latestValue; + } + } + + private void NotifyOnNext() + { + // call source.OnNext + var node = _root; + while (node != null) + { + node.OnNext(_latestValue); + node = node.Next; + } + } + + void IObserverLinkedList.UnsubscribeNode(ObserverNode node) + { + if (node == _root) + { + _root = node.Next; + } + if (node == _last) + { + _last = node.Previous; + } + + if (node.Previous != null) + { + node.Previous.Next = node.Next; + } + if (node.Next != null) + { + node.Next.Previous = node.Previous; + } + } + + private enum ValidationKind + { + Default, + FirstTime, + RequestFromSoucre, + } +} + +/// +/// Factory extension methods for ValidatableReactiveProperty +/// +public static class ValidatableReactiveProperty +{ + /// + /// Create the ValidatableReactiveProperty instance. + /// + /// The source IReactiveProperty + /// The validation logic. + /// The ReactivePropertyMode + /// The EqualityComparer for T + public static ValidatableReactiveProperty ToValidatableReactiveProperty( + this IReactiveProperty source, + Func validator, + ReactivePropertyMode mode = ReactivePropertyMode.Default, + IEqualityComparer? equalityComparer = null) => + new ValidatableReactiveProperty(source, validator, mode, equalityComparer); + + /// + /// Create the ValidatableReactiveProperty instance. + /// + /// The source IReactiveProperty + /// The validation logics. + /// The ReactivePropertyMode + /// The EqualityComparer for T + public static ValidatableReactiveProperty ToValidatableReactiveProperty( + this IReactiveProperty source, + IEnumerable> validators, + ReactivePropertyMode mode = ReactivePropertyMode.Default, + IEqualityComparer? equalityComparer = null) => + new ValidatableReactiveProperty(source, validators, mode, equalityComparer); +} diff --git a/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs b/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs index 62983a5b..cbdb6411 100644 --- a/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs +++ b/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs @@ -13,11 +13,6 @@ namespace Reactive.Bindings; -internal class SingletonDataErrorsChangedEventArgs -{ - public static readonly DataErrorsChangedEventArgs Value = new(nameof(ReactiveProperty.Value)); -} - /// /// Two-way bindable IObservable<T> /// diff --git a/Test/ReactiveProperty.NETStandard.Tests/ValidatableReactivePropertyTest.cs b/Test/ReactiveProperty.NETStandard.Tests/ValidatableReactivePropertyTest.cs new file mode 100644 index 00000000..e6c91cc0 --- /dev/null +++ b/Test/ReactiveProperty.NETStandard.Tests/ValidatableReactivePropertyTest.cs @@ -0,0 +1,166 @@ +using System; +using Microsoft.Reactive.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Reactive.Bindings; + +namespace ReactiveProperty.Tests; + +[TestClass] +public class ValidatableReactivePropertyTest : ReactiveTest +{ + [TestMethod] + public void BasicUsage() + { + var source = new ReactivePropertySlim(""); + var target = source.ToValidatableReactiveProperty( + x => x == "valid" ? null : "invalid value"); + + var errorChangedCount = 0; + target.ErrorsChanged += (_, _) => errorChangedCount++; + + errorChangedCount.Is(0); + target.HasErrors.IsTrue(); + target.GetErrors().Is("invalid value"); + target.Value = "still invalid"; + errorChangedCount.Is(0); + target.HasErrors.IsTrue(); + target.GetErrors().Is("invalid value"); + source.Value.Is(""); + + target.Value = "valid"; + errorChangedCount.Is(1); + target.HasErrors.IsFalse(); + target.GetErrors().Length.Is(0); + source.Value.Is("valid"); + } + + [TestMethod] + public void ValidationLogicsCase() + { + var source = new ReactivePropertySlim(""); + var target = source.ToValidatableReactiveProperty( + new Func[] + { + x => string.IsNullOrWhiteSpace(x) ? "required" : null, + x => x == "valid" ? null : "invalid value", + }); + + var errorChangedCount = 0; + target.ErrorsChanged += (_, _) => errorChangedCount++; + + errorChangedCount.Is(0); + target.HasErrors.IsTrue(); + target.GetErrors().Is("required", "invalid value"); + target.ErrorMessage.Is("required"); + target.Value = "still invalid"; + errorChangedCount.Is(1); + target.HasErrors.IsTrue(); + target.GetErrors().Is("invalid value"); + target.ErrorMessage.Is("invalid value"); + source.Value.Is(""); + + target.Value = "valid"; + errorChangedCount.Is(2); + target.HasErrors.IsFalse(); + target.GetErrors().Length.Is(0); + target.ErrorMessage.Is(""); + source.Value.Is("valid"); + } + + [TestMethod] + public void DefaultModeTest() + { + var source = new ReactivePropertySlim("initialValue"); + var target = source.ToValidatableReactiveProperty( + x => string.IsNullOrWhiteSpace(x) ? "invalid" : null); + + string? raiseLatestValueOnSubscribed = null; + target.Subscribe(x => raiseLatestValueOnSubscribed = x).Dispose(); + + raiseLatestValueOnSubscribed.Is("initialValue"); + + int subscribeCount = 0; + target.Subscribe(x => subscribeCount++); + subscribeCount.Is(1); + target.Value = "initialValue"; + subscribeCount.Is(1); + target.Value = "changed"; + subscribeCount.Is(2); + } + + [TestMethod] + public void IgnoreInitialValidationErrorTest() + { + var source = new ReactivePropertySlim("initialValue"); + var target = source.ToValidatableReactiveProperty( + x => string.IsNullOrWhiteSpace(x) ? "invalid" : null, + mode: ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError); + + target.HasErrors.IsFalse(); + target.GetErrors().Length.Is(0); + target.Value.Is("initialValue"); + + target.Value = ""; + target.HasErrors.IsTrue(); + target.GetErrors().Is("invalid"); + } + + [TestMethod] + public void IgnoreExceptionCase() + { + var source = new ReactivePropertySlim("initialValue"); + + Assert.ThrowsException(() => + { + source.ToValidatableReactiveProperty( + x => string.IsNullOrWhiteSpace(x) ? "invalid" : null, + mode: ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreException); + }); + } + + [TestMethod] + public void ObserveErrorChangedTest() + { + var source = new ReactivePropertySlim(""); + var target = source.ToValidatableReactiveProperty( + x => string.IsNullOrWhiteSpace(x) ? "invalid" : null, + mode: ReactivePropertyMode.Default); + + var scheduler = new TestScheduler(); + var recorder = scheduler.CreateObserver(); + target.ObserveErrorChanged.Subscribe(recorder); + + recorder.Messages.Count.Is(0); + + target.Value = "valid"; + target.Value = ""; + target.Dispose(); + recorder.Messages.Is( + OnNext(0, (string[] x) => x is []), + OnNext(0, (string[] x) => x is ["invalid"]), + OnCompleted(0)); + } + + [TestMethod] + public void ObserveHasErrorsTest() + { + var source = new ReactivePropertySlim(""); + var target = source.ToValidatableReactiveProperty( + x => string.IsNullOrWhiteSpace(x) ? "invalid" : null, + mode: ReactivePropertyMode.Default); + + var scheduler = new TestScheduler(); + var recorder = scheduler.CreateObserver(); + target.ObserveHasErrors.Subscribe(recorder); + + recorder.Messages.Count.Is(0); + + target.Value = "valid"; + target.Value = ""; + target.Dispose(); + recorder.Messages.Is( + OnNext(0, false), + OnNext(0, true), + OnCompleted(0)); + } +} From 840b2c43ead6100a9907027ac0e7de541e8e4cbb Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 5 Jan 2023 16:42:44 +0900 Subject: [PATCH 28/32] Move notifiers and add CombineLatest and Fix Validation bug --- .../Internals/DelegateDisposable.cs | 41 +++ .../SingletonPropertyChangedEventArgs.cs | 2 + .../Notifiers/BooleanNotifier.cs | 4 +- .../Notifiers/BusyNotifier.cs | 7 +- .../Notifiers/CountNotifier.cs | 9 +- .../Notifiers/MessageBroker.cs | 25 +- .../TinyLinq/ObservableExtensions.cs | 130 +++++++-- .../ValidatableReactiveProperty.cs | 194 +++++++++++-- .../Notifiers/BooleanNotifierLegacy.cs | 85 ++++++ .../Notifiers/BusyNotifierLegacy.cs | 75 +++++ .../Notifiers/CountNotifierLegacy.cs | 138 +++++++++ .../Notifiers/MessageBrokerLegacy.cs | 263 ++++++++++++++++++ .../ReactiveProperty.cs | 2 +- 13 files changed, 921 insertions(+), 54 deletions(-) create mode 100644 Source/ReactiveProperty.Core/Internals/DelegateDisposable.cs rename Source/{ReactiveProperty.NETStandard => ReactiveProperty.Core}/Notifiers/BooleanNotifier.cs (95%) rename Source/{ReactiveProperty.NETStandard => ReactiveProperty.Core}/Notifiers/BusyNotifier.cs (90%) rename Source/{ReactiveProperty.NETStandard => ReactiveProperty.Core}/Notifiers/CountNotifier.cs (93%) rename Source/{ReactiveProperty.NETStandard => ReactiveProperty.Core}/Notifiers/MessageBroker.cs (93%) create mode 100644 Source/ReactiveProperty.NETStandard/Notifiers/BooleanNotifierLegacy.cs create mode 100644 Source/ReactiveProperty.NETStandard/Notifiers/BusyNotifierLegacy.cs create mode 100644 Source/ReactiveProperty.NETStandard/Notifiers/CountNotifierLegacy.cs create mode 100644 Source/ReactiveProperty.NETStandard/Notifiers/MessageBrokerLegacy.cs diff --git a/Source/ReactiveProperty.Core/Internals/DelegateDisposable.cs b/Source/ReactiveProperty.Core/Internals/DelegateDisposable.cs new file mode 100644 index 00000000..79a7709c --- /dev/null +++ b/Source/ReactiveProperty.Core/Internals/DelegateDisposable.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Reactive.Bindings.Internals; +internal class DelegateDisposable : IDisposable +{ + private bool _isDisposed; + private readonly Action _action; + + public DelegateDisposable(Action action) + { + _action = action; + } + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + _action(); + } +} + +internal class DelegateDisposable : IDisposable +{ + private bool _isDisposed; + private readonly Action _action; + private readonly TState _state; + + public DelegateDisposable(Action action, TState state) + { + _action = action; + _state = state; + } + + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + _action(_state); + } +} diff --git a/Source/ReactiveProperty.Core/Internals/SingletonPropertyChangedEventArgs.cs b/Source/ReactiveProperty.Core/Internals/SingletonPropertyChangedEventArgs.cs index 52838d98..e0fb4b94 100644 --- a/Source/ReactiveProperty.Core/Internals/SingletonPropertyChangedEventArgs.cs +++ b/Source/ReactiveProperty.Core/Internals/SingletonPropertyChangedEventArgs.cs @@ -6,4 +6,6 @@ internal static class SingletonPropertyChangedEventArgs { public static readonly PropertyChangedEventArgs Value = new(nameof(IReactiveProperty.Value)); public static readonly PropertyChangedEventArgs HasErrors = new(nameof(INotifyDataErrorInfo.HasErrors)); + public static readonly PropertyChangedEventArgs ErrorMessage = new(nameof(ValidatableReactiveProperty.ErrorMessage)); + } diff --git a/Source/ReactiveProperty.NETStandard/Notifiers/BooleanNotifier.cs b/Source/ReactiveProperty.Core/Notifiers/BooleanNotifier.cs similarity index 95% rename from Source/ReactiveProperty.NETStandard/Notifiers/BooleanNotifier.cs rename to Source/ReactiveProperty.Core/Notifiers/BooleanNotifier.cs index 76dd5396..d41bcbcc 100644 --- a/Source/ReactiveProperty.NETStandard/Notifiers/BooleanNotifier.cs +++ b/Source/ReactiveProperty.Core/Notifiers/BooleanNotifier.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel; -using System.Reactive.Subjects; using System.Runtime.CompilerServices; +using Reactive.Bindings.Internals; namespace Reactive.Bindings.Notifiers; @@ -16,7 +16,7 @@ public class BooleanNotifier : IObservable, INotifyPropertyChanged /// public event PropertyChangedEventHandler? PropertyChanged; - private readonly Subject boolTrigger = new(); + private readonly InternalSubject boolTrigger = new(); private bool boolValue; /// diff --git a/Source/ReactiveProperty.NETStandard/Notifiers/BusyNotifier.cs b/Source/ReactiveProperty.Core/Notifiers/BusyNotifier.cs similarity index 90% rename from Source/ReactiveProperty.NETStandard/Notifiers/BusyNotifier.cs rename to Source/ReactiveProperty.Core/Notifiers/BusyNotifier.cs index dd0156ad..a2735be5 100644 --- a/Source/ReactiveProperty.NETStandard/Notifiers/BusyNotifier.cs +++ b/Source/ReactiveProperty.Core/Notifiers/BusyNotifier.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel; -using System.Reactive.Disposables; -using System.Reactive.Subjects; +using Reactive.Bindings.Internals; namespace Reactive.Bindings.Notifiers; @@ -20,7 +19,7 @@ public class BusyNotifier : INotifyPropertyChanged, IObservable private int ProcessCounter { get; set; } - private Subject IsBusySubject { get; } = new Subject(); + private InternalSubject IsBusySubject { get; } = new (); private object LockObject { get; } = new object(); @@ -51,7 +50,7 @@ public IDisposable ProcessStart() { ProcessCounter++; IsBusy = ProcessCounter != 0; - return Disposable.Create(() => + return new DelegateDisposable(() => { lock (LockObject) { diff --git a/Source/ReactiveProperty.NETStandard/Notifiers/CountNotifier.cs b/Source/ReactiveProperty.Core/Notifiers/CountNotifier.cs similarity index 93% rename from Source/ReactiveProperty.NETStandard/Notifiers/CountNotifier.cs rename to Source/ReactiveProperty.Core/Notifiers/CountNotifier.cs index c6171210..fd49bc30 100644 --- a/Source/ReactiveProperty.NETStandard/Notifiers/CountNotifier.cs +++ b/Source/ReactiveProperty.Core/Notifiers/CountNotifier.cs @@ -1,8 +1,7 @@ using System; using System.ComponentModel; -using System.Reactive.Disposables; -using System.Reactive.Subjects; using System.Runtime.CompilerServices; +using Reactive.Bindings.Internals; namespace Reactive.Bindings.Notifiers; @@ -38,7 +37,7 @@ public enum CountChangedStatus public class CountNotifier : IObservable, INotifyPropertyChanged { private readonly object lockObject = new(); - private readonly Subject statusChanged = new(); + private readonly InternalSubject statusChanged = new(); /// /// Occurs when a property value changes. @@ -100,7 +99,7 @@ public IDisposable Increment(int incrementCount = 1) { if (Count == Max) { - return Disposable.Empty; + return InternalDisposable.Empty; } else if (incrementCount + Count > Max) { @@ -117,7 +116,7 @@ public IDisposable Increment(int incrementCount = 1) statusChanged.OnNext(CountChangedStatus.Max); } - return Disposable.Create(() => Decrement(incrementCount)); + return new DelegateDisposable(() => Decrement(incrementCount)); } } diff --git a/Source/ReactiveProperty.NETStandard/Notifiers/MessageBroker.cs b/Source/ReactiveProperty.Core/Notifiers/MessageBroker.cs similarity index 93% rename from Source/ReactiveProperty.NETStandard/Notifiers/MessageBroker.cs rename to Source/ReactiveProperty.Core/Notifiers/MessageBroker.cs index c39f193a..80225923 100644 --- a/Source/ReactiveProperty.NETStandard/Notifiers/MessageBroker.cs +++ b/Source/ReactiveProperty.Core/Notifiers/MessageBroker.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Reactive.Linq; using System.Threading.Tasks; using Reactive.Bindings.Internals; @@ -300,18 +299,28 @@ public static class MessageBrokerExtensions /// public static IObservable ToObservable(this IMessageSubscriber messageSubscriber) { - return Observable.Create(observer => + return new MessageSubscriberObservable(messageSubscriber); + } + + class MessageSubscriberObservable : IObservable + { + private readonly object _gate = new(); + private readonly IMessageSubscriber _messageSubscriber; + + public MessageSubscriberObservable(IMessageSubscriber messageSubscriber) { - var gate = new object(); - var d = messageSubscriber.Subscribe(x => + _messageSubscriber = messageSubscriber; + } + + public IDisposable Subscribe(IObserver observer) + { + return _messageSubscriber.Subscribe(x => { - // needs synchronize - lock (gate) + lock(_gate) { observer.OnNext(x); } }); - return d; - }); + } } } diff --git a/Source/ReactiveProperty.Core/TinyLinq/ObservableExtensions.cs b/Source/ReactiveProperty.Core/TinyLinq/ObservableExtensions.cs index ba0fa9ae..ac1e8195 100644 --- a/Source/ReactiveProperty.Core/TinyLinq/ObservableExtensions.cs +++ b/Source/ReactiveProperty.Core/TinyLinq/ObservableExtensions.cs @@ -1,38 +1,132 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using Reactive.Bindings.Disposables; +using Reactive.Bindings.Extensions; using Reactive.Bindings.Internals; namespace Reactive.Bindings.TinyLinq; +/// +/// Minimal LINQ subset extension methods (Select, Where and CombineLatest only). +/// If you want to use more LINQ methods, please add System.Reactive package from NuGet. +/// And Replace namespace from Reactive.Bindings.TinyLinq to System.Reactive.Linq. +/// public static class ObservableExtensions { - public static IObservable Select(this IObservable self, Func selector) => - new SelectObservable(self, selector); + /// + /// Projects each element of an observable sequence into a new form with the spcified source and selector + /// + /// The type of source. + /// The type of result. + /// A sequence of elements to invoke a transform function on. + /// A transform function to apply to each source element. + /// + public static IObservable Select( + this IObservable source, + Func selector) => + new SelectObservable(source, selector); - public static IObservable Where(this IObservable self, Func filter) => - new WhereObservable(self, filter); + /// + /// Filters the elements of an observable sequence based on a predicate. + /// + /// The type of source. + /// An observable sequence whose elements to filter. + /// A function to test each source element for a condition. + /// + public static IObservable Where( + this IObservable source, + Func filter) => + new WhereObservable(source, filter); + + public static IObservable CombineLatest( + this IEnumerable> sources, + Func, TResult> resultSelector) => + new CombineLatestObservable(sources, resultSelector); +} + +internal class CombineLatestObservable : IObservable +{ + private readonly IEnumerable> _sources; + private readonly Func, TResult> _resultSelector; + + public CombineLatestObservable(IEnumerable> sources, Func, TResult> resultSelector) + { + _sources = sources; + _resultSelector = resultSelector; + } + + public IDisposable Subscribe(IObserver observer) => + new Subscription(_sources.ToArray(), _resultSelector, observer); + + private class Subscription : IDisposable + { + private readonly CompositeDisposable _disposables = new(); + public Subscription(IObservable[] sources, Func, TResult> resultSelector, IObserver observer) + { + bool allValuesWerePublished = false; + var latestValues = new TSource[sources.Length]; + var published = new bool[sources.Length]; + for (int i = 0; i < sources.Length; i++) + { + var index = i; + sources[index].Subscribe(new DelegateObserver(x => + { + latestValues[index] = x; + if (allValuesWerePublished is false) + { + published[index] = true; + allValuesWerePublished = published.All(x => x); + } + + if (allValuesWerePublished) + { + observer.OnNext(resultSelector(latestValues)); + } + }, + error => + { + observer.OnError(error); + Dispose(); + }, + () => + { + observer.OnCompleted(); + Dispose(); + })).AddTo(_disposables); + } + } + + public void Dispose() + { + _disposables.Dispose(); + } + } } -internal class SelectObservable : IObservable +internal class SelectObservable : IObservable { - private readonly IObservable _source; - private readonly Func _selector; + private readonly IObservable _source; + private readonly Func _selector; - public SelectObservable(IObservable source, Func selector) + public SelectObservable(IObservable source, Func selector) { _source = source; _selector = selector; } - public IDisposable Subscribe(IObserver observer) => + public IDisposable Subscribe(IObserver observer) => new Subscription(_source, _selector, observer); class Subscription : IDisposable { private readonly IDisposable _disposable; - public Subscription(IObservable source, Func selector, IObserver observer) + public Subscription(IObservable source, Func selector, IObserver observer) { - _disposable = source.Subscribe(new DelegateObserver( + _disposable = source.Subscribe(new DelegateObserver( x => observer.OnNext(selector(x)), observer.OnError, observer.OnCompleted)); @@ -42,18 +136,18 @@ public Subscription(IObservable source, Func selector, IObserver obs } } -internal class WhereObservable : IObservable +internal class WhereObservable : IObservable { - private readonly IObservable _source; - private readonly Func _filter; + private readonly IObservable _source; + private readonly Func _filter; - public WhereObservable(IObservable source, Func filter) + public WhereObservable(IObservable source, Func filter) { _source = source; _filter = filter; } - public IDisposable Subscribe(IObserver observer) + public IDisposable Subscribe(IObserver observer) { return new Subscription(_source, _filter, observer); } @@ -62,9 +156,9 @@ class Subscription : IDisposable { private readonly IDisposable _disposable; - public Subscription(IObservable source, Func filter, IObserver observer) + public Subscription(IObservable source, Func filter, IObserver observer) { - _disposable = source.Subscribe(new DelegateObserver( + _disposable = source.Subscribe(new DelegateObserver( x => { if (filter(x)) diff --git a/Source/ReactiveProperty.Core/ValidatableReactiveProperty.cs b/Source/ReactiveProperty.Core/ValidatableReactiveProperty.cs index a4e3789a..20ef48b0 100644 --- a/Source/ReactiveProperty.Core/ValidatableReactiveProperty.cs +++ b/Source/ReactiveProperty.Core/ValidatableReactiveProperty.cs @@ -2,7 +2,10 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using Reactive.Bindings.Internals; namespace Reactive.Bindings; @@ -21,11 +24,11 @@ public class ValidatableReactiveProperty : IReactiveProperty, IObserverLin private const int IsDisposedFlagNumber = 1 << 9; // (reserve 0 ~ 8) private ReactivePropertyMode _mode; // None = 0, DistinctUntilChanged = 1, RaiseLatestValueOnSubscribe = 2, Disposed = (1 << 9) private readonly IEqualityComparer _equalityComparer; + private readonly bool _disposeSource; private ObserverNode? _root; private ObserverNode? _last; private string[] _errorMessages = Array.Empty(); - private IDisposable _sourceSubscribe = default!; private readonly InternalSubject _observeErrorChanged = new(); private readonly InternalSubject _observeHasErrors = new(); @@ -91,7 +94,7 @@ public T Value /// true if this instance is raise latest value on subscribe; otherwise, false. /// public bool IsRaiseLatestValueOnSubscribe => (_mode & ReactivePropertyMode.RaiseLatestValueOnSubscribe) == ReactivePropertyMode.RaiseLatestValueOnSubscribe; - + /// /// Gets a value indicating whether this instance is ignore first validation errors. /// @@ -112,10 +115,12 @@ public T Value /// The validation logics. /// The ReactivePropertyMode /// The EqualityComparer for T + /// Call Dispose of the source parameter when ValidatableReactiveProperty was called Dispose. public ValidatableReactiveProperty(IReactiveProperty source, IEnumerable> validators, ReactivePropertyMode mode = ReactivePropertyMode.Default, - IEqualityComparer? equalityComparer = null) + IEqualityComparer? equalityComparer = null, + bool disposeSource = false) { if ((mode & ReactivePropertyMode.IgnoreException) == ReactivePropertyMode.IgnoreException) { @@ -128,6 +133,7 @@ public ValidatableReactiveProperty(IReactiveProperty source, _latestValue = Source.Value; _mode = mode; _equalityComparer = equalityComparer ?? EqualityComparer.Default; + _disposeSource = disposeSource; InitializeValidationProcess(); } @@ -138,10 +144,12 @@ public ValidatableReactiveProperty(IReactiveProperty source, /// The validation logic. /// The ReactivePropertyMode /// The EqualityComparer for T + /// Call Dispose of the source parameter when ValidatableReactiveProperty was called Dispose. public ValidatableReactiveProperty(IReactiveProperty source, Func validator, ReactivePropertyMode mode = ReactivePropertyMode.Default, - IEqualityComparer? equalityComparer = null) : + IEqualityComparer? equalityComparer = null, + bool disposeSource = false) : this(source, new[] { validator }, mode, equalityComparer) { } @@ -167,9 +175,14 @@ public void Dispose() node = node.Next; } - _sourceSubscribe?.Dispose(); + Source.PropertyChanged -= Source_PropertyChanged; _observeErrorChanged.Dispose(); _observeHasErrors.Dispose(); + + if (_disposeSource) + { + Source.Dispose(); + } } /// @@ -223,22 +236,24 @@ public IDisposable Subscribe(IObserver observer) private void InitializeValidationProcess() { - _sourceSubscribe = Source.Subscribe(new DelegateObserver(x => - { - Validate(x, ValidationKind.RequestFromSoucre); - })); - + Source.PropertyChanged += Source_PropertyChanged; Validate(_latestValue, ValidationKind.FirstTime); } + private void Source_PropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(IReactiveProperty.Value)) return; + Validate(Source.Value, ValidationKind.RequestFromSoucre); + } + private void Validate(T value, ValidationKind validationKind = ValidationKind.Default) { var previousHasErrors = HasErrors; var previousErrors = _errorMessages; var isNotEquals = !_equalityComparer.Equals(_latestValue, value); - var needValidation = - (validationKind is ValidationKind.FirstTime || !IsIgnoreInitialValidationError) || + var needValidation = validationKind == ValidationKind.FirstTime ? + !IsIgnoreInitialValidationError : isNotEquals; _latestValue = value; @@ -263,14 +278,16 @@ private void Validate(T value, ValidationKind validationKind = ValidationKind.De { ErrorsChanged?.Invoke(this, SingletonDataErrorsChangedEventArgs.Value); _observeErrorChanged.OnNext(_errorMessages); + PropertyChanged?.Invoke(this, SingletonPropertyChangedEventArgs.ErrorMessage); } if (previousHasErrors != HasErrors) { _observeHasErrors.OnNext(HasErrors); + PropertyChanged?.Invoke(this, SingletonPropertyChangedEventArgs.HasErrors); } - if (validationKind is not ValidationKind.RequestFromSoucre && HasErrors is false) + if (validationKind != ValidationKind.RequestFromSoucre && HasErrors is false) { Source.Value = _latestValue; } @@ -328,12 +345,14 @@ public static class ValidatableReactiveProperty /// The validation logic. /// The ReactivePropertyMode /// The EqualityComparer for T + /// Call Dispose of the source parameter when ValidatableReactiveProperty was called Dispose. public static ValidatableReactiveProperty ToValidatableReactiveProperty( this IReactiveProperty source, Func validator, ReactivePropertyMode mode = ReactivePropertyMode.Default, - IEqualityComparer? equalityComparer = null) => - new ValidatableReactiveProperty(source, validator, mode, equalityComparer); + IEqualityComparer? equalityComparer = null, + bool disposeSource = false) => + new ValidatableReactiveProperty(source, validator, mode, equalityComparer, disposeSource); /// /// Create the ValidatableReactiveProperty instance. @@ -342,10 +361,153 @@ public static ValidatableReactiveProperty ToValidatableReactiveProperty( /// The validation logics. /// The ReactivePropertyMode /// The EqualityComparer for T + /// Call Dispose of the source parameter when ValidatableReactiveProperty was called Dispose. + /// The new instance of ValidatableReactiveProperty + public static ValidatableReactiveProperty ToValidatableReactiveProperty( + this IReactiveProperty source, + IEnumerable> validators, + ReactivePropertyMode mode = ReactivePropertyMode.Default, + IEqualityComparer? equalityComparer = null, + bool disposeSource = false) => + new ValidatableReactiveProperty(source, validators, mode, equalityComparer, disposeSource); + + /// + /// Create the ValidationReactiveProperty instance from DataAnnotations attributes. + /// + /// Property type + /// Target ReactiveProperty + /// Target property as expression + /// The ReactivePropertyMode + /// The EqualityComparer for T + /// Call Dispose of the source parameter when ValidatableReactiveProperty was called Dispose. + /// The new instance of ValidatableReactiveProperty public static ValidatableReactiveProperty ToValidatableReactiveProperty( this IReactiveProperty source, + Expression?>> selfSelector, + ReactivePropertyMode mode = ReactivePropertyMode.Default, + IEqualityComparer? equalityComparer = null, + bool disposeSource = false) + { + var memberExpression = (MemberExpression)selfSelector.Body; + var propertyInfo = (PropertyInfo)memberExpression.Member; + var display = propertyInfo.GetCustomAttribute(); + var attrs = propertyInfo.GetCustomAttributes().ToArray(); + if (attrs.Length == 0) + { + throw new InvalidOperationException($"Data annotations does not found on {propertyInfo.Name}."); + } + + var context = new ValidationContext(source) + { + DisplayName = display?.GetName() ?? propertyInfo.Name, + MemberName = nameof(IReactiveProperty.Value), + }; + + return source.ToValidatableReactiveProperty( + x => + { + var validationResults = new List(); + if (Validator.TryValidateValue(x, context, validationResults, attrs)) + { + return null; + } + + return validationResults[0].ErrorMessage; + }, + mode, + equalityComparer, + disposeSource); + } + + /// + /// Create the ValidationReactiveProperty instance from DataAnnotations attributes. + /// + /// Property type + /// Initial value of ValidatableReactiveProperty + /// Target ReactiveProperty + /// Target property as expression + /// The ReactivePropertyMode + /// The EqualityComparer for T + /// The new instance of ValidatableReactiveProperty + public static ValidatableReactiveProperty CreateFromDataAnnotations( + T initialValue, + Expression?>> selfSelector, + ReactivePropertyMode mode = ReactivePropertyMode.Default, + IEqualityComparer? equalityComparer = null) + { + var memberExpression = (MemberExpression)selfSelector.Body; + var propertyInfo = (PropertyInfo)memberExpression.Member; + var display = propertyInfo.GetCustomAttribute(); + var attrs = propertyInfo.GetCustomAttributes().ToArray(); + if (attrs.Length == 0) + { + throw new InvalidOperationException($"Data annotations does not found on {propertyInfo.Name}."); + } + + var source = new ReactivePropertySlim(initialValue); + var context = new ValidationContext(source) + { + DisplayName = display?.GetName() ?? propertyInfo.Name, + MemberName = nameof(IReactiveProperty.Value), + }; + + return new ValidatableReactiveProperty( + source, + x => + { + var validationResults = new List(); + if (Validator.TryValidateValue(x, context, validationResults, attrs)) + { + return null; + } + + return validationResults[0].ErrorMessage; + }, + mode, + equalityComparer, + true); + } + + /// + /// Create the ValidationReactiveProperty instance from a custom validation logic. + /// + /// Property type + /// Initial value of ValidatableReactiveProperty + /// The validation logic. + /// The ReactivePropertyMode + /// The EqualityComparer for T + /// The new instance of ValidatableReactiveProperty + public static ValidatableReactiveProperty CreateFromValidationLogic( + T initialValue, + Func validator, + ReactivePropertyMode mode = ReactivePropertyMode.Default, + IEqualityComparer? equalityComparer = null) => + new ValidatableReactiveProperty( + new ReactivePropertySlim(initialValue), + validator, + mode, + equalityComparer, + true); + + /// + /// Create the ValidationReactiveProperty instance from a custom validation logic. + /// + /// Property type + /// Initial value of ValidatableReactiveProperty + /// The validation logics. + /// The ReactivePropertyMode + /// The EqualityComparer for T + /// The new instance of ValidatableReactiveProperty + public static ValidatableReactiveProperty CreateFromValidationLogics( + T initialValue, IEnumerable> validators, ReactivePropertyMode mode = ReactivePropertyMode.Default, IEqualityComparer? equalityComparer = null) => - new ValidatableReactiveProperty(source, validators, mode, equalityComparer); + new ValidatableReactiveProperty( + new ReactivePropertySlim(initialValue), + validators, + mode, + equalityComparer, + true); + } diff --git a/Source/ReactiveProperty.NETStandard/Notifiers/BooleanNotifierLegacy.cs b/Source/ReactiveProperty.NETStandard/Notifiers/BooleanNotifierLegacy.cs new file mode 100644 index 00000000..ec6dd0f0 --- /dev/null +++ b/Source/ReactiveProperty.NETStandard/Notifiers/BooleanNotifierLegacy.cs @@ -0,0 +1,85 @@ +using System; +using System.ComponentModel; +using System.Reactive.Subjects; +using System.Runtime.CompilerServices; + +namespace Reactive.Bindings.Notifiers; + +/// +/// Notify boolean flag. +/// +public class BooleanNotifierLegacy : IObservable, INotifyPropertyChanged +{ + /// + /// Occurs when a property value changes. + /// + /// + public event PropertyChangedEventHandler? PropertyChanged; + + private readonly Subject boolTrigger = new(); + private bool boolValue; + + /// + /// Current flag value + /// + public bool Value + { + get + { + return boolValue; + } + + set + { + boolValue = value; + OnPropertyChanged(); + boolTrigger.OnNext(value); + } + } + + /// + /// Setup initial flag. + /// + public BooleanNotifierLegacy(bool initialValue = false) + { + Value = initialValue; + } + + /// + /// Set and raise true if current value isn't true. + /// + public void TurnOn() + { + if (Value != true) + { + Value = true; + } + } + + /// + /// Set and raise false if current value isn't false. + /// + public void TurnOff() + { + if (Value != false) + { + Value = false; + } + } + + /// + /// Set and raise reverse value. + /// + public void SwitchValue() => Value = !Value; + + /// + /// Subscribe observer. + /// + public IDisposable Subscribe(IObserver observer) => boolTrigger.Subscribe(observer); + + /// + /// Called when [property changed]. + /// + /// Name of the property. + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); +} diff --git a/Source/ReactiveProperty.NETStandard/Notifiers/BusyNotifierLegacy.cs b/Source/ReactiveProperty.NETStandard/Notifiers/BusyNotifierLegacy.cs new file mode 100644 index 00000000..d254eb61 --- /dev/null +++ b/Source/ReactiveProperty.NETStandard/Notifiers/BusyNotifierLegacy.cs @@ -0,0 +1,75 @@ +using System; +using System.ComponentModel; +using System.Reactive.Disposables; +using System.Reactive.Subjects; + +namespace Reactive.Bindings.Notifiers; + +/// +/// Notify of busy. +/// +public class BusyNotifierLegacy : INotifyPropertyChanged, IObservable +{ + + private static readonly PropertyChangedEventArgs IsBusyPropertyChangedEventArgs = new(nameof(IsBusy)); + + /// + /// property changed event handler + /// + public event PropertyChangedEventHandler? PropertyChanged; + + private int ProcessCounter { get; set; } + + private Subject IsBusySubject { get; } = new Subject(); + + private object LockObject { get; } = new object(); + + private bool isBusy; + + /// + /// Is process running. + /// + public bool IsBusy + { + get { return isBusy; } + set + { + if (isBusy == value) { return; } + isBusy = value; + PropertyChanged?.Invoke(this, IsBusyPropertyChangedEventArgs); + IsBusySubject.OnNext(isBusy); + } + } + + /// + /// Process start. + /// + /// Call dispose method when process end. + public IDisposable ProcessStart() + { + lock (LockObject) + { + ProcessCounter++; + IsBusy = ProcessCounter != 0; + return Disposable.Create(() => + { + lock (LockObject) + { + ProcessCounter--; + IsBusy = ProcessCounter != 0; + } + }); + } + } + + /// + /// Subscribe busy. + /// + /// observer + /// disposable + public IDisposable Subscribe(IObserver observer) + { + observer.OnNext(IsBusy); + return IsBusySubject.Subscribe(observer); + } +} diff --git a/Source/ReactiveProperty.NETStandard/Notifiers/CountNotifierLegacy.cs b/Source/ReactiveProperty.NETStandard/Notifiers/CountNotifierLegacy.cs new file mode 100644 index 00000000..ca190253 --- /dev/null +++ b/Source/ReactiveProperty.NETStandard/Notifiers/CountNotifierLegacy.cs @@ -0,0 +1,138 @@ +using System; +using System.ComponentModel; +using System.Reactive.Disposables; +using System.Reactive.Subjects; +using System.Runtime.CompilerServices; + +namespace Reactive.Bindings.Notifiers; + +/// +/// Notify event of count flag. +/// +public class CountNotifierLegacy : IObservable, INotifyPropertyChanged +{ + private readonly object lockObject = new(); + private readonly Subject statusChanged = new(); + + /// + /// Occurs when a property value changes. + /// + /// + public event PropertyChangedEventHandler? PropertyChanged; + + private readonly int max; + private int count; + + /// + /// Gets the maximum. + /// + /// The maximum. + public int Max => max; + + /// + /// Gets the count. + /// + /// The count. + public int Count + { + get + { + return count; + } + + private set + { + count = value; + OnPropertyChanged(); + } + } + + /// + /// Setup max count of signal. + /// + public CountNotifierLegacy(int max = int.MaxValue) + { + if (max <= 0) + { + throw new ArgumentException(nameof(max)); + } + + this.max = max; + } + + /// + /// Increment count and notify status. + /// + public IDisposable Increment(int incrementCount = 1) + { + if (incrementCount < 0) + { + throw new ArgumentException(nameof(incrementCount)); + } + + lock (lockObject) + { + if (Count == Max) + { + return Disposable.Empty; + } + else if (incrementCount + Count > Max) + { + Count = Max; + } + else + { + Count += incrementCount; + } + + statusChanged.OnNext(CountChangedStatus.Increment); + if (Count == Max) + { + statusChanged.OnNext(CountChangedStatus.Max); + } + + return Disposable.Create(() => Decrement(incrementCount)); + } + } + + /// + /// Decrement count and notify status. + /// + public void Decrement(int decrementCount = 1) + { + if (decrementCount < 0) + { + throw new ArgumentException(nameof(decrementCount)); + } + + lock (lockObject) + { + if (Count == 0) + { + return; + } + else if (Count - decrementCount < 0) + { + Count = 0; + } + else + { + Count -= decrementCount; + } + + statusChanged.OnNext(CountChangedStatus.Decrement); + if (Count == 0) + { + statusChanged.OnNext(CountChangedStatus.Empty); + } + } + } + + /// + /// Subscribe observer. + /// + public IDisposable Subscribe(IObserver observer) => statusChanged.Subscribe(observer); + + private void OnPropertyChanged([CallerMemberName] string? propertyName = null) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); +} diff --git a/Source/ReactiveProperty.NETStandard/Notifiers/MessageBrokerLegacy.cs b/Source/ReactiveProperty.NETStandard/Notifiers/MessageBrokerLegacy.cs new file mode 100644 index 00000000..d6bdbe19 --- /dev/null +++ b/Source/ReactiveProperty.NETStandard/Notifiers/MessageBrokerLegacy.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Reactive.Bindings.Internals; + +namespace Reactive.Bindings.Notifiers; + +/// +/// In-Memory PubSub filtered by Type. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public class MessageBrokerLegacy : IMessageBroker, IDisposable +{ + /// + /// MessageBroker in Global scope. + /// + public static readonly IMessageBroker Default = new MessageBrokerLegacy(); + + bool isDisposed = false; + readonly Dictionary notifiers = new(); + + /// + /// Send Message to all receiver. + /// + public void Publish(T message) + { + ImmutableList> notifier; + lock (notifiers) + { + if (isDisposed) throw new ObjectDisposedException("AsyncMessageBroker"); + + object _notifier; + if (notifiers.TryGetValue(typeof(T), out _notifier)) + { + notifier = (ImmutableList>)_notifier; + } + else + { + return; + } + } + + var data = notifier.Data; + for (int i = 0; i < data.Length; i++) + { + data[i].Invoke(message); + } + } + + /// + /// Subscribe typed message. + /// + public IDisposable Subscribe(Action action) + { + lock (notifiers) + { + if (isDisposed) throw new ObjectDisposedException("MessageBroker"); + + object _notifier; + if (!notifiers.TryGetValue(typeof(T), out _notifier)) + { + var notifier = ImmutableList>.Empty; + notifier = notifier.Add(action); + notifiers.Add(typeof(T), notifier); + } + else + { + var notifier = (ImmutableList>)_notifier; + notifier = notifier.Add(action); + notifiers[typeof(T)] = notifier; + } + } + + return new Subscription(this, action); + } + + /// + /// Stop Pub-Sub system. + /// + public void Dispose() + { + lock (notifiers) + { + if (!isDisposed) + { + isDisposed = true; + notifiers.Clear(); + } + } + } + + class Subscription : IDisposable + { + readonly MessageBrokerLegacy parent; + readonly Action action; + + public Subscription(MessageBrokerLegacy parent, Action action) + { + this.parent = parent; + this.action = action; + } + + public void Dispose() + { + lock (parent.notifiers) + { + object _notifier; + if (parent.notifiers.TryGetValue(typeof(T), out _notifier)) + { + var notifier = (ImmutableList>)_notifier; + notifier = notifier.Remove(action); + + parent.notifiers[typeof(T)] = notifier; + } + } + } + } +} + +/// +/// In-Memory PubSub filtered by Type. +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public class AsyncMessageBrokerLegacy : IAsyncMessageBroker, IDisposable +{ + static readonly Task EmptyTask = Task.FromResult(null); + + /// + /// AsyncMessageBroker in Global scope. + /// + public static readonly IAsyncMessageBroker Default = new AsyncMessageBrokerLegacy(); + + bool isDisposed = false; + readonly Dictionary notifiers = new(); + + /// + /// Send Message to all receiver and await complete. + /// + public Task PublishAsync(T message) + { + ImmutableList> notifier; + lock (notifiers) + { + if (isDisposed) throw new ObjectDisposedException("AsyncMessageBroker"); + + object _notifier; + if (notifiers.TryGetValue(typeof(T), out _notifier)) + { + notifier = (ImmutableList>)_notifier; + } + else + { + return EmptyTask; + } + } + + var data = notifier.Data; + var awaiter = new Task[data.Length]; + for (int i = 0; i < data.Length; i++) + { + awaiter[i] = data[i].Invoke(message); + } + return Task.WhenAll(awaiter); + } + + /// + /// Subscribe typed message. + /// + public IDisposable Subscribe(Func asyncAction) + { + lock (notifiers) + { + if (isDisposed) throw new ObjectDisposedException("AsyncMessageBroker"); + + object _notifier; + if (!notifiers.TryGetValue(typeof(T), out _notifier)) + { + var notifier = ImmutableList>.Empty; + notifier = notifier.Add(asyncAction); + notifiers.Add(typeof(T), notifier); + } + else + { + var notifier = (ImmutableList>)_notifier; + notifier = notifier.Add(asyncAction); + notifiers[typeof(T)] = notifier; + } + } + + return new Subscription(this, asyncAction); + } + + /// + /// Stop Pub-Sub system. + /// + public void Dispose() + { + lock (notifiers) + { + if (!isDisposed) + { + isDisposed = true; + notifiers.Clear(); + } + } + } + + class Subscription : IDisposable + { + readonly AsyncMessageBrokerLegacy parent; + readonly Func asyncAction; + + public Subscription(AsyncMessageBrokerLegacy parent, Func asyncAction) + { + this.parent = parent; + this.asyncAction = asyncAction; + } + + public void Dispose() + { + lock (parent.notifiers) + { + object _notifier; + if (parent.notifiers.TryGetValue(typeof(T), out _notifier)) + { + var notifier = (ImmutableList>)_notifier; + notifier = notifier.Remove(asyncAction); + + parent.notifiers[typeof(T)] = notifier; + } + } + } + } +} + +/// +/// Extensions of MessageBroker. +/// +public static class MessageBrokerExtensions +{ + /// + /// Convert IMessageSubscriber.Subscribe to Observable. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static IObservable ToObservableLegacy(this IMessageSubscriber messageSubscriber) + { + return Observable.Create(observer => + { + var gate = new object(); + var d = messageSubscriber.Subscribe(x => + { + // needs synchronize + lock (gate) + { + observer.OnNext(x); + } + }); + return d; + }); + } +} diff --git a/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs b/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs index cbdb6411..f16a190b 100644 --- a/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs +++ b/Source/ReactiveProperty.NETStandard/ReactiveProperty.cs @@ -472,7 +472,7 @@ public static ReactiveProperty FromObject( { if (ExpressionTreeUtils.IsNestedPropertyPath(propertySelector)) { - var propertyPath = PropertyPathNode.CreateFromPropertySelector(propertySelector); + var propertyPath = PropertyPathNodeLegacy.CreateFromPropertySelector(propertySelector); propertyPath.UpdateSource(target); var initialValue = propertyPath.GetPropertyPathValue(); From 3d82f38fe5a61b3e51f24b04da49789131ecf015 Mon Sep 17 00:00:00 2001 From: Kazuki Ota Date: Thu, 5 Jan 2023 16:43:24 +0900 Subject: [PATCH 29/32] add sample for core --- ReactiveProperty-Samples.sln | 19 +++-- .../ReactivePropertyCoreSample.WPF/App.xaml | 15 ++++ .../App.xaml.cs | 15 ++++ .../AssemblyInfo.cs | 10 +++ .../DisposeViewModelWhenClosedBehavior.cs | 23 ++++++ .../Messages/ShowWindowMessage.cs | 3 + .../Models/BindableBase.cs | 24 ++++++ .../Models/Poco.cs | 23 ++++++ .../ReactivePropertyCoreSample.WPF.csproj | 18 +++++ .../ViewModels/BasicUsagesWindowViewModel.cs | 25 ++++++ .../ViewModels/CreateFromPocoViewModel.cs | 39 ++++++++++ .../ViewModels/MainWindowViewModel.cs | 25 ++++++ .../ViewModels/ValidationViewModel.cs | 77 +++++++++++++++++++ .../ViewModels/ViewModelBase.cs | 13 ++++ .../Views/BasicUsagesWindow.xaml | 26 +++++++ .../Views/BasicUsagesWindow.xaml.cs | 25 ++++++ .../Views/CreateFromPocoWindow.xaml | 45 +++++++++++ .../Views/CreateFromPocoWindow.xaml.cs | 25 ++++++ .../Views/MainWindow.xaml | 30 ++++++++ .../Views/MainWindow.xaml.cs | 59 ++++++++++++++ .../Views/ValidationWindow.xaml | 40 ++++++++++ .../Views/ValidationWindow.xaml.cs | 25 ++++++ .../ViewModels/CreateFromPocoViewModel.cs | 6 +- 23 files changed, 599 insertions(+), 11 deletions(-) create mode 100644 Samples/ReactivePropertyCoreSample.WPF/App.xaml create mode 100644 Samples/ReactivePropertyCoreSample.WPF/App.xaml.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/AssemblyInfo.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Behaviors/DisposeViewModelWhenClosedBehavior.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Messages/ShowWindowMessage.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Models/BindableBase.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Models/Poco.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/ReactivePropertyCoreSample.WPF.csproj create mode 100644 Samples/ReactivePropertyCoreSample.WPF/ViewModels/BasicUsagesWindowViewModel.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/ViewModels/CreateFromPocoViewModel.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/ViewModels/MainWindowViewModel.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/ViewModels/ValidationViewModel.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/ViewModels/ViewModelBase.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Views/BasicUsagesWindow.xaml create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Views/BasicUsagesWindow.xaml.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Views/CreateFromPocoWindow.xaml create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Views/CreateFromPocoWindow.xaml.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Views/MainWindow.xaml create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Views/MainWindow.xaml.cs create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Views/ValidationWindow.xaml create mode 100644 Samples/ReactivePropertyCoreSample.WPF/Views/ValidationWindow.xaml.cs diff --git a/ReactiveProperty-Samples.sln b/ReactiveProperty-Samples.sln index 136eb1cf..aa7aac65 100644 --- a/ReactiveProperty-Samples.sln +++ b/ReactiveProperty-Samples.sln @@ -29,18 +29,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiUIThreadApp", "Samples EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Blazor", "Blazor", "{FED0F033-DD46-4A15-9041-999813FECF73}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorServerApp", "Samples\Blazor\BlazorServerApp\BlazorServerApp.csproj", "{8DE66105-1318-4606-A702-70A78754E036}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorServerApp", "Samples\Blazor\BlazorServerApp\BlazorServerApp.csproj", "{8DE66105-1318-4606-A702-70A78754E036}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorWasmApp", "Samples\Blazor\BlazorWasmApp\BlazorWasmApp.csproj", "{43DD55EB-1220-47D5-8F08-1F0989D8B0F6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorWasmApp", "Samples\Blazor\BlazorWasmApp\BlazorWasmApp.csproj", "{43DD55EB-1220-47D5-8F08-1F0989D8B0F6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorSample.Shared", "Samples\Blazor\BlazorSample.Shared\BlazorSample.Shared.csproj", "{EE15634D-C8A7-40DD-B966-6695A2ECA1E1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorSample.Shared", "Samples\Blazor\BlazorSample.Shared\BlazorSample.Shared.csproj", "{EE15634D-C8A7-40DD-B966-6695A2ECA1E1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveProperty.Platform.Blazor", "Source\ReactiveProperty.Platform.Blazor\ReactiveProperty.Platform.Blazor.csproj", "{3CF8E744-40E0-4874-B82D-1B3AD2523043}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactivePropertyCoreSample.WPF", "Samples\ReactivePropertyCoreSample.WPF\ReactivePropertyCoreSample.WPF.csproj", "{434764BE-10E5-46B2-AFC0-FE18F599D9D3}" +EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - Source\ReactiveProperty.Platform.Shared\ReactiveProperty.Platform.Shared.projitems*{ff52758c-f5fb-49c9-8dee-e4a7dfba4dc1}*SharedItemsImports = 5 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -94,6 +93,10 @@ Global {3CF8E744-40E0-4874-B82D-1B3AD2523043}.Debug|Any CPU.Build.0 = Debug|Any CPU {3CF8E744-40E0-4874-B82D-1B3AD2523043}.Release|Any CPU.ActiveCfg = Release|Any CPU {3CF8E744-40E0-4874-B82D-1B3AD2523043}.Release|Any CPU.Build.0 = Release|Any CPU + {434764BE-10E5-46B2-AFC0-FE18F599D9D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {434764BE-10E5-46B2-AFC0-FE18F599D9D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {434764BE-10E5-46B2-AFC0-FE18F599D9D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {434764BE-10E5-46B2-AFC0-FE18F599D9D3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -111,8 +114,12 @@ Global {43DD55EB-1220-47D5-8F08-1F0989D8B0F6} = {FED0F033-DD46-4A15-9041-999813FECF73} {EE15634D-C8A7-40DD-B966-6695A2ECA1E1} = {FED0F033-DD46-4A15-9041-999813FECF73} {3CF8E744-40E0-4874-B82D-1B3AD2523043} = {69BB0C02-5C1E-4720-AC3E-61E25B9F0143} + {434764BE-10E5-46B2-AFC0-FE18F599D9D3} = {1C3E6F75-DD1A-4183-8DFD-38D307782FC3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C0F7AB85-D9AA-401B-9DAE-0C5394272D48} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + Source\ReactiveProperty.Platform.Shared\ReactiveProperty.Platform.Shared.projitems*{ff52758c-f5fb-49c9-8dee-e4a7dfba4dc1}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/Samples/ReactivePropertyCoreSample.WPF/App.xaml b/Samples/ReactivePropertyCoreSample.WPF/App.xaml new file mode 100644 index 00000000..caad4287 --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/App.xaml @@ -0,0 +1,15 @@ + + + + + + diff --git a/Samples/ReactivePropertyCoreSample.WPF/App.xaml.cs b/Samples/ReactivePropertyCoreSample.WPF/App.xaml.cs new file mode 100644 index 00000000..01fd969e --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/App.xaml.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Data; +using System.Linq; +using System.Threading.Tasks; +using System.Windows; + +namespace ReactivePropertyCoreSample.WPF; +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application +{ +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/AssemblyInfo.cs b/Samples/ReactivePropertyCoreSample.WPF/AssemblyInfo.cs new file mode 100644 index 00000000..8b5504ec --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/Samples/ReactivePropertyCoreSample.WPF/Behaviors/DisposeViewModelWhenClosedBehavior.cs b/Samples/ReactivePropertyCoreSample.WPF/Behaviors/DisposeViewModelWhenClosedBehavior.cs new file mode 100644 index 00000000..acc0d880 --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/Behaviors/DisposeViewModelWhenClosedBehavior.cs @@ -0,0 +1,23 @@ +using System; +using System.Windows; +using Microsoft.Xaml.Behaviors; + +namespace ReactivePropertyCoreSample.WPF.Behaviors +{ + public class DisposeViewModelWhenClosedBehavior : Behavior + { + protected override void OnAttached() + { + base.OnAttached(); + AssociatedObject.Closed += AssociatedObject_Closed; + } + + private void AssociatedObject_Closed(object? sender, EventArgs e) => (AssociatedObject.DataContext as IDisposable)?.Dispose(); + + protected override void OnDetaching() + { + base.OnDetaching(); + AssociatedObject.Closed -= AssociatedObject_Closed; + } + } +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/Messages/ShowWindowMessage.cs b/Samples/ReactivePropertyCoreSample.WPF/Messages/ShowWindowMessage.cs new file mode 100644 index 00000000..10f094c5 --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/Messages/ShowWindowMessage.cs @@ -0,0 +1,3 @@ +namespace ReactivePropertyCoreSample.WPF.Messages; + +public record ShowWindowMessage(string Name); diff --git a/Samples/ReactivePropertyCoreSample.WPF/Models/BindableBase.cs b/Samples/ReactivePropertyCoreSample.WPF/Models/BindableBase.cs new file mode 100644 index 00000000..b7dc893e --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/Models/BindableBase.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Text; + +namespace ReactivePropertyCoreSample.WPF.Models; + +public class BindableBase : INotifyPropertyChanged +{ + public event PropertyChangedEventHandler? PropertyChanged; + + protected bool SetProperty(ref T field, T value, [CallerMemberName] string? propertyName = null) + { + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + field = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + return true; + } +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/Models/Poco.cs b/Samples/ReactivePropertyCoreSample.WPF/Models/Poco.cs new file mode 100644 index 00000000..e6ad2d92 --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/Models/Poco.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ReactivePropertyCoreSample.WPF.Models; + +public class Poco : BindableBase +{ + private string _firstName = ""; + public string FirstName + { + get { return _firstName; } + set { SetProperty(ref _firstName, value); } + } + + private string _lastName = ""; + public string LastName + { + get { return _lastName; } + set { SetProperty(ref _lastName, value); } + } + +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/ReactivePropertyCoreSample.WPF.csproj b/Samples/ReactivePropertyCoreSample.WPF/ReactivePropertyCoreSample.WPF.csproj new file mode 100644 index 00000000..466cf8af --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/ReactivePropertyCoreSample.WPF.csproj @@ -0,0 +1,18 @@ + + + + WinExe + net6.0-windows + enable + true + + + + + + + + + + + diff --git a/Samples/ReactivePropertyCoreSample.WPF/ViewModels/BasicUsagesWindowViewModel.cs b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/BasicUsagesWindowViewModel.cs new file mode 100644 index 00000000..854b844b --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/BasicUsagesWindowViewModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Reactive.Bindings; +using Reactive.Bindings.Extensions; +using Reactive.Bindings.TinyLinq; +using ReactivePropertyCoreSample.WPF.Models; + +namespace ReactivePropertyCoreSample.WPF.ViewModels; +public class BasicUsagesViewModel : ViewModelBase +{ + public ReactivePropertySlim Input { get; } + public ReadOnlyReactivePropertySlim Output { get; } + + public BasicUsagesViewModel() + { + Input = new ReactivePropertySlim("") + .AddTo(Disposables); + Output = Input.Select(x => x.ToUpper()) + .ToReadOnlyReactivePropertySlim("") + .AddTo(Disposables); + } +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/ViewModels/CreateFromPocoViewModel.cs b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/CreateFromPocoViewModel.cs new file mode 100644 index 00000000..b12ee90a --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/CreateFromPocoViewModel.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Reactive.Bindings; +using Reactive.Bindings.Extensions; +using ReactivePropertyCoreSample.WPF.Models; + +namespace ReactivePropertyCoreSample.WPF.ViewModels; +public class CreateFromPocoViewModel : ViewModelBase +{ + public Poco Poco { get; } = new() + { + FirstName = "Kazuki", + LastName = "Ota", + }; + + public ReactivePropertySlim FirstNameTwoWay { get; } + public ReactivePropertySlim LastNameTwoWay { get; } + public ReadOnlyReactivePropertySlim FirstNameOneWay { get; } + public ReadOnlyReactivePropertySlim LastNameOneWay { get; } + + public CreateFromPocoViewModel() + { + FirstNameTwoWay = Poco.ToReactivePropertySlimAsSynchronized(x => x.FirstName) + .AddTo(Disposables); + LastNameTwoWay = Poco.ToReactivePropertySlimAsSynchronized(x => x.LastName) + .AddTo(Disposables); + + FirstNameOneWay = Poco.ObserveProperty(x => x.FirstName) + .ToReadOnlyReactivePropertySlim("") + .AddTo(Disposables); + LastNameOneWay = Poco.ObserveProperty(x => x.LastName) + .ToReadOnlyReactivePropertySlim("") + .AddTo(Disposables); + } + +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/ViewModels/MainWindowViewModel.cs b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/MainWindowViewModel.cs new file mode 100644 index 00000000..7432903e --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Reactive.Bindings; +using Reactive.Bindings.Extensions; +using Reactive.Bindings.Notifiers; +using ReactivePropertyCoreSample.WPF.Messages; + +namespace ReactivePropertyCoreSample.WPF.ViewModels; +public class MainWindowViewModel : ViewModelBase +{ + public ReactiveCommandSlim ShowWindowCommand { get; } + public MainWindowViewModel() + { + ShowWindowCommand = new ReactiveCommandSlim() + .WithSubscribe(x => + { + MessageBroker.Default.Publish(new ShowWindowMessage(x)); + }, + Disposables.Add) + .AddTo(Disposables); + } +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/ViewModels/ValidationViewModel.cs b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/ValidationViewModel.cs new file mode 100644 index 00000000..6d95eb6f --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/ValidationViewModel.cs @@ -0,0 +1,77 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Reactive.Bindings; +using Reactive.Bindings.Extensions; +using Reactive.Bindings.TinyLinq; +using ReactivePropertyCoreSample.WPF.Models; + +namespace ReactivePropertyCoreSample.WPF.ViewModels +{ + public class ValidationViewModel : ViewModelBase + { + private readonly ReactivePropertySlim _withDataAnnotations; + [Required(ErrorMessage = "Required property")] + public ValidatableReactiveProperty WithDataAnnotations { get; } + + public ValidatableReactiveProperty WithCustomValidationLogic { get; } + + public ReadOnlyReactivePropertySlim HasValidationErrors { get; } + + public ReactiveCommandSlim SubmitCommand { get; } + + public ReadOnlyReactivePropertySlim Message { get; } + + [Required] + public ValidatableReactiveProperty IgnoreInitialValidationError { get; } + + public Poco Poco { get; } = new Poco { FirstName = "Kazuki" }; + [Required] + public ValidatableReactiveProperty FirstName { get; } + + public ValidationViewModel() + { + WithDataAnnotations = ValidatableReactiveProperty.CreateFromDataAnnotations( + "", + () => WithDataAnnotations) + .AddTo(Disposables); + + WithCustomValidationLogic = ValidatableReactiveProperty.CreateFromValidationLogic( + "", + x => !string.IsNullOrEmpty(x) && x.Contains("-") ? null : "Require '-'") + .AddTo(Disposables); + + HasValidationErrors = new[] + { + WithDataAnnotations.ObserveHasErrors, + WithCustomValidationLogic.ObserveHasErrors, + }.CombineLatest(x => x.Any(y => y)) + .ToReadOnlyReactivePropertySlim() + .AddTo(Disposables); + + SubmitCommand = new[] + { + WithDataAnnotations.ObserveHasErrors, + WithCustomValidationLogic.ObserveHasErrors, + }.CombineLatest(x => x.All(x => x is false)) + .ToReactiveCommandSlim(false) + .AddTo(Disposables); + Message = SubmitCommand + .Select(_ => $"You submittted \'{WithDataAnnotations.Value} & {WithCustomValidationLogic.Value}\'") + .ToReadOnlyReactivePropertySlim("") + .AddTo(Disposables); + + IgnoreInitialValidationError = ValidatableReactiveProperty.CreateFromDataAnnotations( + "", + () => IgnoreInitialValidationError, + mode: ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError) + .AddTo(Disposables); + + FirstName = Poco.ToReactivePropertySlimAsSynchronized(x => x.FirstName) + .ToValidatableReactiveProperty( + () => FirstName, + mode: ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError) + .AddTo(Disposables); + } + } +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/ViewModels/ViewModelBase.cs b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/ViewModelBase.cs new file mode 100644 index 00000000..4d1798ab --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/ViewModelBase.cs @@ -0,0 +1,13 @@ +using System; +using Reactive.Bindings.Disposables; +using ReactivePropertyCoreSample.WPF.Models; + +namespace ReactivePropertyCoreSample.WPF.ViewModels +{ + public class ViewModelBase : BindableBase, IDisposable + { + protected CompositeDisposable Disposables { get; } = new CompositeDisposable(); + + public void Dispose() => Disposables.Dispose(); + } +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/Views/BasicUsagesWindow.xaml b/Samples/ReactivePropertyCoreSample.WPF/Views/BasicUsagesWindow.xaml new file mode 100644 index 00000000..ba69b3ff --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/Views/BasicUsagesWindow.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + diff --git a/Samples/ReactivePropertyCoreSample.WPF/Views/BasicUsagesWindow.xaml.cs b/Samples/ReactivePropertyCoreSample.WPF/Views/BasicUsagesWindow.xaml.cs new file mode 100644 index 00000000..96677c86 --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/Views/BasicUsagesWindow.xaml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace ReactivePropertyCoreSample.WPF.Views; +/// +/// BasicUsagesWindow.xaml の相互作用ロジック +/// +public partial class BasicUsagesWindow : Window +{ + public BasicUsagesWindow() + { + InitializeComponent(); + } +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/Views/CreateFromPocoWindow.xaml b/Samples/ReactivePropertyCoreSample.WPF/Views/CreateFromPocoWindow.xaml new file mode 100644 index 00000000..7215267c --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/Views/CreateFromPocoWindow.xaml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/Samples/ReactivePropertyCoreSample.WPF/Views/CreateFromPocoWindow.xaml.cs b/Samples/ReactivePropertyCoreSample.WPF/Views/CreateFromPocoWindow.xaml.cs new file mode 100644 index 00000000..81e0101a --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/Views/CreateFromPocoWindow.xaml.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace ReactivePropertyCoreSample.WPF.Views; +/// +/// CreateFromPocoWindow.xaml の相互作用ロジック +/// +public partial class CreateFromPocoWindow : Window +{ + public CreateFromPocoWindow() + { + InitializeComponent(); + } +} diff --git a/Samples/ReactivePropertyCoreSample.WPF/Views/MainWindow.xaml b/Samples/ReactivePropertyCoreSample.WPF/Views/MainWindow.xaml new file mode 100644 index 00000000..72f711fc --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/Views/MainWindow.xaml @@ -0,0 +1,30 @@ + + + + + + + + + +