diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet6_0.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet6_0.verified.txt index 08891ca66e..afe71e7047 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet6_0.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet6_0.verified.txt @@ -349,6 +349,10 @@ namespace ReactiveUI string? PropertyName { get; } TSender Sender { get; } } + public interface IReactiveProperty : System.IDisposable, System.IObservable, System.Reactive.Disposables.ICancelable + { + T Value { get; set; } + } public interface IRoutableViewModel : ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging { ReactiveUI.IScreen HostScreen { get; } @@ -750,6 +754,20 @@ namespace ReactiveUI public TSender Sender { get; } } [System.Runtime.Serialization.DataContract] + public class ReactiveProperty : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty, System.IDisposable, System.IObservable, System.Reactive.Disposables.ICancelable + { + public ReactiveProperty() { } + public ReactiveProperty(T? initialValue) { } + public ReactiveProperty(T? initialValue, System.Reactive.Concurrency.IScheduler? scheduler) { } + public bool IsDisposed { get; } + [System.Runtime.Serialization.DataMember] + [System.Text.Json.Serialization.JsonInclude] + public T Value { get; set; } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public System.IDisposable Subscribe(System.IObserver observer) { } + } + [System.Runtime.Serialization.DataContract] public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable { public ReactiveRecord() { } diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet7_0.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet7_0.verified.txt index a5fc5ef9f0..2ca65762ff 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet7_0.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet7_0.verified.txt @@ -349,6 +349,10 @@ namespace ReactiveUI string? PropertyName { get; } TSender Sender { get; } } + public interface IReactiveProperty : System.IDisposable, System.IObservable, System.Reactive.Disposables.ICancelable + { + T Value { get; set; } + } public interface IRoutableViewModel : ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging { ReactiveUI.IScreen HostScreen { get; } @@ -750,6 +754,20 @@ namespace ReactiveUI public TSender Sender { get; } } [System.Runtime.Serialization.DataContract] + public class ReactiveProperty : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty, System.IDisposable, System.IObservable, System.Reactive.Disposables.ICancelable + { + public ReactiveProperty() { } + public ReactiveProperty(T? initialValue) { } + public ReactiveProperty(T? initialValue, System.Reactive.Concurrency.IScheduler? scheduler) { } + public bool IsDisposed { get; } + [System.Runtime.Serialization.DataMember] + [System.Text.Json.Serialization.JsonInclude] + public T Value { get; set; } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public System.IDisposable Subscribe(System.IObserver observer) { } + } + [System.Runtime.Serialization.DataContract] public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable { public ReactiveRecord() { } diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt index e38976eadc..6e4fff54e9 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt @@ -349,6 +349,10 @@ namespace ReactiveUI string? PropertyName { get; } TSender Sender { get; } } + public interface IReactiveProperty : System.IDisposable, System.IObservable, System.Reactive.Disposables.ICancelable + { + T Value { get; set; } + } public interface IRoutableViewModel : ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging { ReactiveUI.IScreen HostScreen { get; } @@ -750,6 +754,20 @@ namespace ReactiveUI public TSender Sender { get; } } [System.Runtime.Serialization.DataContract] + public class ReactiveProperty : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty, System.IDisposable, System.IObservable, System.Reactive.Disposables.ICancelable + { + public ReactiveProperty() { } + public ReactiveProperty(T? initialValue) { } + public ReactiveProperty(T? initialValue, System.Reactive.Concurrency.IScheduler? scheduler) { } + public bool IsDisposed { get; } + [System.Runtime.Serialization.DataMember] + [System.Text.Json.Serialization.JsonInclude] + public T Value { get; set; } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public System.IDisposable Subscribe(System.IObserver observer) { } + } + [System.Runtime.Serialization.DataContract] public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable { public ReactiveRecord() { } diff --git a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt index f8a1b294b7..b6b8144d15 100644 --- a/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt +++ b/src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt @@ -347,6 +347,10 @@ namespace ReactiveUI string? PropertyName { get; } TSender Sender { get; } } + public interface IReactiveProperty : System.IDisposable, System.IObservable, System.Reactive.Disposables.ICancelable + { + T Value { get; set; } + } public interface IRoutableViewModel : ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging { ReactiveUI.IScreen HostScreen { get; } @@ -748,6 +752,20 @@ namespace ReactiveUI public TSender Sender { get; } } [System.Runtime.Serialization.DataContract] + public class ReactiveProperty : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty, System.IDisposable, System.IObservable, System.Reactive.Disposables.ICancelable + { + public ReactiveProperty() { } + public ReactiveProperty(T? initialValue) { } + public ReactiveProperty(T? initialValue, System.Reactive.Concurrency.IScheduler? scheduler) { } + public bool IsDisposed { get; } + [System.Runtime.Serialization.DataMember] + [System.Text.Json.Serialization.JsonInclude] + public T Value { get; set; } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public System.IDisposable Subscribe(System.IObserver observer) { } + } + [System.Runtime.Serialization.DataContract] public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable { public ReactiveRecord() { } diff --git a/src/ReactiveUI.Tests/ReactiveProperty/ReactivePropertyTest.cs b/src/ReactiveUI.Tests/ReactiveProperty/ReactivePropertyTest.cs new file mode 100644 index 0000000000..3338337fb2 --- /dev/null +++ b/src/ReactiveUI.Tests/ReactiveProperty/ReactivePropertyTest.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using Microsoft.Reactive.Testing; + +namespace ReactiveUI.Tests.ReactiveProperty +{ + public class ReactivePropertyTest : ReactiveTest + { + [Fact] + public void NormalCase() + { + var rp = new ReactiveProperty(); + Assert.Null(rp.Value); + rp.Subscribe(x => Assert.Null(x)); + } + + [Fact] + public void InitialValue() + { + var rp = new ReactiveProperty("Hello world"); + Assert.Equal(rp.Value, "Hello world"); + rp.Subscribe(x => Assert.Equal(x, "Hello world")); + } + } +} diff --git a/src/ReactiveUI.Tests/ReactiveProperty/TestEnum.cs b/src/ReactiveUI.Tests/ReactiveProperty/TestEnum.cs new file mode 100644 index 0000000000..6ced78b84a --- /dev/null +++ b/src/ReactiveUI.Tests/ReactiveProperty/TestEnum.cs @@ -0,0 +1,14 @@ +// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.Tests.ReactiveProperty +{ + internal enum TestEnum + { + None, + Enum1, + Enum2 + } +} diff --git a/src/ReactiveUI/ReactiveProperty/IReactiveProperty.cs b/src/ReactiveUI/ReactiveProperty/IReactiveProperty.cs new file mode 100644 index 0000000000..22d3f38566 --- /dev/null +++ b/src/ReactiveUI/ReactiveProperty/IReactiveProperty.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI; + +/// +/// Reactive Property. +/// +/// The type of the property. +/// +/// +public interface IReactiveProperty : IObservable, ICancelable +{ + /// + /// Gets or sets the value. + /// + /// + /// The value. + /// + public T? Value { get; set; } +} diff --git a/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs b/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs new file mode 100644 index 0000000000..dedea1a4cd --- /dev/null +++ b/src/ReactiveUI/ReactiveProperty/ReactiveProperty.cs @@ -0,0 +1,100 @@ +// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI; + +/// +/// ReactiveProperty - a two way bindable declarative observable property with imperative get set. +/// +/// The type of the property. +/// +/// +[DataContract] +public class ReactiveProperty : ReactiveObject, IReactiveProperty +{ + private readonly IScheduler _scheduler; + private readonly CompositeDisposable _disposables = []; + private T? _value; + + /// + /// Initializes a new instance of the class. + /// + public ReactiveProperty() => _scheduler = RxApp.TaskpoolScheduler; + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + public ReactiveProperty(T? initialValue) + { + Value = initialValue; + _scheduler = RxApp.TaskpoolScheduler; + } + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + /// The scheduler. + public ReactiveProperty(T? initialValue, IScheduler? scheduler) + { + Value = initialValue; + _scheduler = scheduler ?? RxApp.TaskpoolScheduler; + } + + /// + /// Gets a value indicating whether gets a value that indicates whether the object is disposed. + /// + public bool IsDisposed => _disposables.IsDisposed; + + /// + /// Gets or sets the value. + /// + /// + /// The value. + /// + [DataMember] + [JsonInclude] + public T? Value + { + get => _value; + set => this.RaiseAndSetIfChanged(ref _value, value); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// 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) => + this.WhenAnyValue(vm => vm.Value) + .ObserveOn(_scheduler) + .Subscribe(observer) + .DisposeWith(_disposables); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposables?.IsDisposed == false && disposing) + { + _disposables?.Dispose(); + } + } +}