Skip to content

Commit cec4cca

Browse files
authored
Feature: Add ReactiveProperty (#3729)
<!-- Please be sure to read the [Contribute](https://github.com/reactiveui/reactiveui#contribute) section of the README --> **What kind of change does this PR introduce?** <!-- Bug fix, feature, docs update, ... --> Feature **What is the current behavior?** <!-- You can also link to an open issue here. --> Boiler plate code is required to get an Observable Property **What is the new behavior?** <!-- If this is a feature change --> A quick and simple ReactiveProperty has been added to suit a small number of properties. ReactiveProperty is a two way bindable declarative observable property with imperative get set Value. Useful when mixing different UI Frameworks without the requirement for a ReactiveObject based ViewModel Declare as ``` IReactiveProperty<string> MyProperty { get; } = new ReactiveProperty<string>(); ``` Use declarativly ``` MyProperty.Subscribe(x => // use x as desired); ``` Use imperativly ``` MyProperty.Value = "Set the value"; var value = MyProperty.Value; ``` Use XAML Bindings ``` {Binding MyProperty.Value} ``` **What might this PR break?** None **Please check if the PR fulfills these requirements** - [ ] Tests for the changes have been added (for bug fixes / features) - [ ] Docs have been added / updated (for bug fixes / features) **Other information**:
1 parent 00305af commit cec4cca

8 files changed

+237
-0
lines changed

src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet6_0.verified.txt

+18
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ namespace ReactiveUI
349349
string? PropertyName { get; }
350350
TSender Sender { get; }
351351
}
352+
public interface IReactiveProperty<T> : System.IDisposable, System.IObservable<T?>, System.Reactive.Disposables.ICancelable
353+
{
354+
T Value { get; set; }
355+
}
352356
public interface IRoutableViewModel : ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging
353357
{
354358
ReactiveUI.IScreen HostScreen { get; }
@@ -750,6 +754,20 @@ namespace ReactiveUI
750754
public TSender Sender { get; }
751755
}
752756
[System.Runtime.Serialization.DataContract]
757+
public class ReactiveProperty<T> : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty<T>, System.IDisposable, System.IObservable<T?>, System.Reactive.Disposables.ICancelable
758+
{
759+
public ReactiveProperty() { }
760+
public ReactiveProperty(T? initialValue) { }
761+
public ReactiveProperty(T? initialValue, System.Reactive.Concurrency.IScheduler? scheduler) { }
762+
public bool IsDisposed { get; }
763+
[System.Runtime.Serialization.DataMember]
764+
[System.Text.Json.Serialization.JsonInclude]
765+
public T Value { get; set; }
766+
public void Dispose() { }
767+
protected virtual void Dispose(bool disposing) { }
768+
public System.IDisposable Subscribe(System.IObserver<T?> observer) { }
769+
}
770+
[System.Runtime.Serialization.DataContract]
753771
public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged<ReactiveUI.IReactiveObject>, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable<ReactiveUI.ReactiveRecord>
754772
{
755773
public ReactiveRecord() { }

src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet7_0.verified.txt

+18
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ namespace ReactiveUI
349349
string? PropertyName { get; }
350350
TSender Sender { get; }
351351
}
352+
public interface IReactiveProperty<T> : System.IDisposable, System.IObservable<T?>, System.Reactive.Disposables.ICancelable
353+
{
354+
T Value { get; set; }
355+
}
352356
public interface IRoutableViewModel : ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging
353357
{
354358
ReactiveUI.IScreen HostScreen { get; }
@@ -750,6 +754,20 @@ namespace ReactiveUI
750754
public TSender Sender { get; }
751755
}
752756
[System.Runtime.Serialization.DataContract]
757+
public class ReactiveProperty<T> : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty<T>, System.IDisposable, System.IObservable<T?>, System.Reactive.Disposables.ICancelable
758+
{
759+
public ReactiveProperty() { }
760+
public ReactiveProperty(T? initialValue) { }
761+
public ReactiveProperty(T? initialValue, System.Reactive.Concurrency.IScheduler? scheduler) { }
762+
public bool IsDisposed { get; }
763+
[System.Runtime.Serialization.DataMember]
764+
[System.Text.Json.Serialization.JsonInclude]
765+
public T Value { get; set; }
766+
public void Dispose() { }
767+
protected virtual void Dispose(bool disposing) { }
768+
public System.IDisposable Subscribe(System.IObserver<T?> observer) { }
769+
}
770+
[System.Runtime.Serialization.DataContract]
753771
public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged<ReactiveUI.IReactiveObject>, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable<ReactiveUI.ReactiveRecord>
754772
{
755773
public ReactiveRecord() { }

src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.DotNet8_0.verified.txt

+18
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ namespace ReactiveUI
349349
string? PropertyName { get; }
350350
TSender Sender { get; }
351351
}
352+
public interface IReactiveProperty<T> : System.IDisposable, System.IObservable<T?>, System.Reactive.Disposables.ICancelable
353+
{
354+
T Value { get; set; }
355+
}
352356
public interface IRoutableViewModel : ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging
353357
{
354358
ReactiveUI.IScreen HostScreen { get; }
@@ -750,6 +754,20 @@ namespace ReactiveUI
750754
public TSender Sender { get; }
751755
}
752756
[System.Runtime.Serialization.DataContract]
757+
public class ReactiveProperty<T> : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty<T>, System.IDisposable, System.IObservable<T?>, System.Reactive.Disposables.ICancelable
758+
{
759+
public ReactiveProperty() { }
760+
public ReactiveProperty(T? initialValue) { }
761+
public ReactiveProperty(T? initialValue, System.Reactive.Concurrency.IScheduler? scheduler) { }
762+
public bool IsDisposed { get; }
763+
[System.Runtime.Serialization.DataMember]
764+
[System.Text.Json.Serialization.JsonInclude]
765+
public T Value { get; set; }
766+
public void Dispose() { }
767+
protected virtual void Dispose(bool disposing) { }
768+
public System.IDisposable Subscribe(System.IObserver<T?> observer) { }
769+
}
770+
[System.Runtime.Serialization.DataContract]
753771
public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged<ReactiveUI.IReactiveObject>, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable<ReactiveUI.ReactiveRecord>
754772
{
755773
public ReactiveRecord() { }

src/ReactiveUI.Tests/API/ApiApprovalTests.ReactiveUI.Net4_7.verified.txt

+18
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ namespace ReactiveUI
347347
string? PropertyName { get; }
348348
TSender Sender { get; }
349349
}
350+
public interface IReactiveProperty<T> : System.IDisposable, System.IObservable<T?>, System.Reactive.Disposables.ICancelable
351+
{
352+
T Value { get; set; }
353+
}
350354
public interface IRoutableViewModel : ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging
351355
{
352356
ReactiveUI.IScreen HostScreen { get; }
@@ -748,6 +752,20 @@ namespace ReactiveUI
748752
public TSender Sender { get; }
749753
}
750754
[System.Runtime.Serialization.DataContract]
755+
public class ReactiveProperty<T> : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty<T>, System.IDisposable, System.IObservable<T?>, System.Reactive.Disposables.ICancelable
756+
{
757+
public ReactiveProperty() { }
758+
public ReactiveProperty(T? initialValue) { }
759+
public ReactiveProperty(T? initialValue, System.Reactive.Concurrency.IScheduler? scheduler) { }
760+
public bool IsDisposed { get; }
761+
[System.Runtime.Serialization.DataMember]
762+
[System.Text.Json.Serialization.JsonInclude]
763+
public T Value { get; set; }
764+
public void Dispose() { }
765+
protected virtual void Dispose(bool disposing) { }
766+
public System.IDisposable Subscribe(System.IObserver<T?> observer) { }
767+
}
768+
[System.Runtime.Serialization.DataContract]
751769
public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged<ReactiveUI.IReactiveObject>, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable<ReactiveUI.ReactiveRecord>
752770
{
753771
public ReactiveRecord() { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
using Microsoft.Reactive.Testing;
7+
8+
namespace ReactiveUI.Tests.ReactiveProperty
9+
{
10+
public class ReactivePropertyTest : ReactiveTest
11+
{
12+
[Fact]
13+
public void NormalCase()
14+
{
15+
var rp = new ReactiveProperty<string>();
16+
Assert.Null(rp.Value);
17+
rp.Subscribe(x => Assert.Null(x));
18+
}
19+
20+
[Fact]
21+
public void InitialValue()
22+
{
23+
var rp = new ReactiveProperty<string>("Hello world");
24+
Assert.Equal(rp.Value, "Hello world");
25+
rp.Subscribe(x => Assert.Equal(x, "Hello world"));
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
namespace ReactiveUI.Tests.ReactiveProperty
7+
{
8+
internal enum TestEnum
9+
{
10+
None,
11+
Enum1,
12+
Enum2
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
namespace ReactiveUI;
7+
8+
/// <summary>
9+
/// Reactive Property.
10+
/// </summary>
11+
/// <typeparam name="T">The type of the property.</typeparam>
12+
/// <seealso cref="IObservable&lt;T&gt;" />
13+
/// <seealso cref="ICancelable" />
14+
public interface IReactiveProperty<T> : IObservable<T?>, ICancelable
15+
{
16+
/// <summary>
17+
/// Gets or sets the value.
18+
/// </summary>
19+
/// <value>
20+
/// The value.
21+
/// </value>
22+
public T? Value { get; set; }
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2+
// Licensed to the .NET Foundation under one or more agreements.
3+
// The .NET Foundation licenses this file to you under the MIT license.
4+
// See the LICENSE file in the project root for full license information.
5+
6+
namespace ReactiveUI;
7+
8+
/// <summary>
9+
/// ReactiveProperty - a two way bindable declarative observable property with imperative get set.
10+
/// </summary>
11+
/// <typeparam name="T">The type of the property.</typeparam>
12+
/// <seealso cref="ReactiveObject" />
13+
/// <seealso cref="IReactiveProperty&lt;T&gt;" />
14+
[DataContract]
15+
public class ReactiveProperty<T> : ReactiveObject, IReactiveProperty<T>
16+
{
17+
private readonly IScheduler _scheduler;
18+
private readonly CompositeDisposable _disposables = [];
19+
private T? _value;
20+
21+
/// <summary>
22+
/// Initializes a new instance of the <see cref="ReactiveProperty{T}"/> class.
23+
/// </summary>
24+
public ReactiveProperty() => _scheduler = RxApp.TaskpoolScheduler;
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="ReactiveProperty{T}"/> class.
28+
/// </summary>
29+
/// <param name="initialValue">The initial value.</param>
30+
public ReactiveProperty(T? initialValue)
31+
{
32+
Value = initialValue;
33+
_scheduler = RxApp.TaskpoolScheduler;
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="ReactiveProperty{T}"/> class.
38+
/// </summary>
39+
/// <param name="initialValue">The initial value.</param>
40+
/// <param name="scheduler">The scheduler.</param>
41+
public ReactiveProperty(T? initialValue, IScheduler? scheduler)
42+
{
43+
Value = initialValue;
44+
_scheduler = scheduler ?? RxApp.TaskpoolScheduler;
45+
}
46+
47+
/// <summary>
48+
/// Gets a value indicating whether gets a value that indicates whether the object is disposed.
49+
/// </summary>
50+
public bool IsDisposed => _disposables.IsDisposed;
51+
52+
/// <summary>
53+
/// Gets or sets the value.
54+
/// </summary>
55+
/// <value>
56+
/// The value.
57+
/// </value>
58+
[DataMember]
59+
[JsonInclude]
60+
public T? Value
61+
{
62+
get => _value;
63+
set => this.RaiseAndSetIfChanged(ref _value, value);
64+
}
65+
66+
/// <summary>
67+
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
68+
/// </summary>
69+
public void Dispose()
70+
{
71+
Dispose(true);
72+
GC.SuppressFinalize(this);
73+
}
74+
75+
/// <summary>
76+
/// Notifies the provider that an observer is to receive notifications.
77+
/// </summary>
78+
/// <param name="observer">The object that is to receive notifications.</param>
79+
/// <returns>
80+
/// A reference to an interface that allows observers to stop receiving notifications before
81+
/// the provider has finished sending them.
82+
/// </returns>
83+
public IDisposable Subscribe(IObserver<T?> observer) =>
84+
this.WhenAnyValue(vm => vm.Value)
85+
.ObserveOn(_scheduler)
86+
.Subscribe(observer)
87+
.DisposeWith(_disposables);
88+
89+
/// <summary>
90+
/// Releases unmanaged and - optionally - managed resources.
91+
/// </summary>
92+
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
93+
protected virtual void Dispose(bool disposing)
94+
{
95+
if (_disposables?.IsDisposed == false && disposing)
96+
{
97+
_disposables?.Dispose();
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)