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: diff --git a/Benchmark/Benchmark.Current/BasicUsages.v9.cs b/Benchmark/Benchmark.Current/BasicUsages.v9.cs new file mode 100644 index 00000000..381648d3 --- /dev/null +++ b/Benchmark/Benchmark.Current/BasicUsages.v9.cs @@ -0,0 +1,71 @@ +using System; +using System.Reactive.Subjects; +using BenchmarkDotNet.Attributes; +using Reactive.Bindings; +using Reactive.Bindings.Extensions; + +namespace ReactivePropertyBenchmark; + +public partial class BasicUsages +{ + [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; + } + + [Benchmark] + public ReactiveProperty ReactivePropertyValidation() + { + var rp = new ReactiveProperty("") + .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "invalid" : null); + rp.Value = "xxx"; // valid + rp.Value = ""; // invalid + return rp; + } + + [Benchmark] + public ValidatableReactiveProperty ValidatableReactivePropertyValidation() + { + var rp = ValidatableReactiveProperty.CreateFromValidationLogic( + "", + x => string.IsNullOrEmpty(x) ? "invalid" : null); + rp.Value = "xxx"; // valid + rp.Value = ""; // invalid + return rp; + } + + [Benchmark] + public IObservable ObservePropertyLegacy() + { + var p = new Person(); + return p.ObservePropertyLegacy(x => x.Name); + } + +} 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..6b0972f5 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; @@ -9,7 +10,7 @@ namespace ReactivePropertyBenchmark { - public class BasicUsages + public partial class BasicUsages { [Benchmark] public ReactiveProperty CreateReactivePropertyInstance() => new ReactiveProperty(); @@ -18,23 +19,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] @@ -50,6 +53,13 @@ public ReactiveProperty ToReactivePropertyAsSynchronized() var p = new Person(); return p.ToReactivePropertyAsSynchronized(x => x.Name); } + + [Benchmark] + public ReactivePropertySlim ToReactivePropertyAsSynchronizedSlim() + { + var p = new Person(); + return p.ToReactivePropertySlimAsSynchronized(x => x.Name); + } } class Person : INotifyPropertyChanged 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.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); - } } 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 - + 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.| 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/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/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/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/ReactiveCommandViewModel.cs b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/ReactiveCommandViewModel.cs new file mode 100644 index 00000000..ed11d369 --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/ViewModels/ReactiveCommandViewModel.cs @@ -0,0 +1,114 @@ +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; + +namespace ReactivePropertyCoreSample.WPF.ViewModels; +public class ReactiveCommandViewModel : ViewModelBase +{ + public ReactiveCommandSlim CreateAndSubscribeCommand { get; } + public ReactivePropertySlim InvokedDateTime { get; } + public ReadOnlyReactivePropertySlim InvokedDateTimeFromCommand { get; } + + public ReactivePropertySlim IsChecked { get; } + public ReactiveCommandSlim CreateFromIObservableBoolCommand { get; } + public ReadOnlyReactivePropertySlim InvokedDateTimeForCreateFromIObservableBoolCommand { get; } + + public ReactiveCommandSlim ReactiveCommandWithParameter { get; } + public ReadOnlyReactivePropertySlim ReactiveCommandWithParameterOutput { get; } + + public AsyncReactiveCommand LongRunningCommand { get; } + public ReactivePropertySlim LongRunningCommandOutput { get; } + + public ReactivePropertySlim IsCheckedForAsyncReactiveCommand { get; } + public AsyncReactiveCommand CreateFromIObservableBoolAsyncReactiveCommand { get; } + public ReactivePropertySlim CreateFromIObservableBoolAsyncReactiveCommandOutput { get; } + + + public ReactivePropertySlim IsCheckedForSharedStatus { get; } + public AsyncReactiveCommand ACommand { get; } + public AsyncReactiveCommand BCommand { get; } + public ReactivePropertySlim SharedStatusOutput { get; } + + public ReactiveCommandViewModel() + { + InvokedDateTime = new ReactivePropertySlim(); + CreateAndSubscribeCommand = new ReactiveCommandSlim() + .WithSubscribe(() => InvokedDateTime.Value = $"The command was invoked at {DateTime.Now}.") + .AddTo(Disposables); + + InvokedDateTimeFromCommand = CreateAndSubscribeCommand + .Select(_ => $"The command was invoked at {DateTime.Now}.") + .ToReadOnlyReactivePropertySlim("") + .AddTo(Disposables); + + IsChecked = new ReactivePropertySlim() + .AddTo(Disposables); + CreateFromIObservableBoolCommand = IsChecked.ToReactiveCommandSlim() + .AddTo(Disposables); + InvokedDateTimeForCreateFromIObservableBoolCommand = CreateFromIObservableBoolCommand + .Select(_ => $"The command was invoked at {DateTime.Now}.") + .ToReadOnlyReactivePropertySlim("") + .AddTo(Disposables); + + ReactiveCommandWithParameter = new ReactiveCommandSlim() + .AddTo(Disposables); + ReactiveCommandWithParameterOutput = ReactiveCommandWithParameter + .Select(x => $"The command was invoked with {x}.") + .ToReadOnlyReactivePropertySlim("") + .AddTo(Disposables); + + LongRunningCommandOutput = new ReactivePropertySlim(); + LongRunningCommand = new AsyncReactiveCommand() + .WithSubscribe(async () => + { + LongRunningCommandOutput.Value = "Long running command was started."; + await Task.Delay(5000); + LongRunningCommandOutput.Value = "Long running command was finished."; + }) + .AddTo(Disposables); + + IsCheckedForAsyncReactiveCommand = new ReactivePropertySlim() + .AddTo(Disposables); + CreateFromIObservableBoolAsyncReactiveCommandOutput = new ReactivePropertySlim() + .AddTo(Disposables); + CreateFromIObservableBoolAsyncReactiveCommand = IsCheckedForAsyncReactiveCommand + .ToAsyncReactiveCommand() + .WithSubscribe(async () => + { + CreateFromIObservableBoolAsyncReactiveCommandOutput.Value = "Long running command was started."; + await Task.Delay(5000); + CreateFromIObservableBoolAsyncReactiveCommandOutput.Value = "Long running command was finished."; + }) + .AddTo(Disposables); + + var sharedStatus = new ReactivePropertySlim(true) + .AddTo(Disposables); + IsCheckedForSharedStatus = new ReactivePropertySlim() + .AddTo(Disposables); + SharedStatusOutput = new ReactivePropertySlim() + .AddTo(Disposables); + ACommand = IsCheckedForSharedStatus + .ToAsyncReactiveCommand(sharedStatus) + .WithSubscribe(async () => + { + SharedStatusOutput.Value = "A command was started."; + await Task.Delay(5000); + SharedStatusOutput.Value = "A command was finished."; + }) + .AddTo(Disposables); + BCommand = IsCheckedForSharedStatus + .ToAsyncReactiveCommand(sharedStatus) + .WithSubscribe(async () => + { + SharedStatusOutput.Value = "B command was started."; + await Task.Delay(5000); + SharedStatusOutput.Value = "B command was finished."; + }) + .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..f0885430 --- /dev/null +++ b/Samples/ReactivePropertyCoreSample.WPF/Views/MainWindow.xaml @@ -0,0 +1,31 @@ + + + + + + + + + +