Skip to content

Commit

Permalink
Feature: Add ReactiveProperty (#3729)
Browse files Browse the repository at this point in the history
<!-- 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**:
  • Loading branch information
ChrisPulman committed Feb 15, 2024
1 parent 00305af commit cec4cca
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 0 deletions.
Expand Up @@ -349,6 +349,10 @@ namespace ReactiveUI
string? PropertyName { get; }
TSender Sender { get; }
}
public interface IReactiveProperty<T> : System.IDisposable, System.IObservable<T?>, 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; }
Expand Down Expand Up @@ -750,6 +754,20 @@ namespace ReactiveUI
public TSender Sender { get; }
}
[System.Runtime.Serialization.DataContract]
public class ReactiveProperty<T> : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty<T>, System.IDisposable, System.IObservable<T?>, 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<T?> observer) { }
}
[System.Runtime.Serialization.DataContract]
public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged<ReactiveUI.IReactiveObject>, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable<ReactiveUI.ReactiveRecord>
{
public ReactiveRecord() { }
Expand Down
Expand Up @@ -349,6 +349,10 @@ namespace ReactiveUI
string? PropertyName { get; }
TSender Sender { get; }
}
public interface IReactiveProperty<T> : System.IDisposable, System.IObservable<T?>, 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; }
Expand Down Expand Up @@ -750,6 +754,20 @@ namespace ReactiveUI
public TSender Sender { get; }
}
[System.Runtime.Serialization.DataContract]
public class ReactiveProperty<T> : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty<T>, System.IDisposable, System.IObservable<T?>, 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<T?> observer) { }
}
[System.Runtime.Serialization.DataContract]
public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged<ReactiveUI.IReactiveObject>, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable<ReactiveUI.ReactiveRecord>
{
public ReactiveRecord() { }
Expand Down
Expand Up @@ -349,6 +349,10 @@ namespace ReactiveUI
string? PropertyName { get; }
TSender Sender { get; }
}
public interface IReactiveProperty<T> : System.IDisposable, System.IObservable<T?>, 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; }
Expand Down Expand Up @@ -750,6 +754,20 @@ namespace ReactiveUI
public TSender Sender { get; }
}
[System.Runtime.Serialization.DataContract]
public class ReactiveProperty<T> : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty<T>, System.IDisposable, System.IObservable<T?>, 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<T?> observer) { }
}
[System.Runtime.Serialization.DataContract]
public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged<ReactiveUI.IReactiveObject>, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable<ReactiveUI.ReactiveRecord>
{
public ReactiveRecord() { }
Expand Down
Expand Up @@ -347,6 +347,10 @@ namespace ReactiveUI
string? PropertyName { get; }
TSender Sender { get; }
}
public interface IReactiveProperty<T> : System.IDisposable, System.IObservable<T?>, 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; }
Expand Down Expand Up @@ -748,6 +752,20 @@ namespace ReactiveUI
public TSender Sender { get; }
}
[System.Runtime.Serialization.DataContract]
public class ReactiveProperty<T> : ReactiveUI.ReactiveObject, ReactiveUI.IReactiveProperty<T>, System.IDisposable, System.IObservable<T?>, 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<T?> observer) { }
}
[System.Runtime.Serialization.DataContract]
public class ReactiveRecord : ReactiveUI.IHandleObservableErrors, ReactiveUI.IReactiveNotifyPropertyChanged<ReactiveUI.IReactiveObject>, ReactiveUI.IReactiveObject, Splat.IEnableLogger, System.ComponentModel.INotifyPropertyChanged, System.ComponentModel.INotifyPropertyChanging, System.IEquatable<ReactiveUI.ReactiveRecord>
{
public ReactiveRecord() { }
Expand Down
28 changes: 28 additions & 0 deletions 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<string>();
Assert.Null(rp.Value);
rp.Subscribe(x => Assert.Null(x));
}

[Fact]
public void InitialValue()
{
var rp = new ReactiveProperty<string>("Hello world");
Assert.Equal(rp.Value, "Hello world");
rp.Subscribe(x => Assert.Equal(x, "Hello world"));
}
}
}
14 changes: 14 additions & 0 deletions 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
}
}
23 changes: 23 additions & 0 deletions 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;

/// <summary>
/// Reactive Property.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <seealso cref="IObservable&lt;T&gt;" />
/// <seealso cref="ICancelable" />
public interface IReactiveProperty<T> : IObservable<T?>, ICancelable
{
/// <summary>
/// Gets or sets the value.
/// </summary>
/// <value>
/// The value.
/// </value>
public T? Value { get; set; }
}
100 changes: 100 additions & 0 deletions 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;

/// <summary>
/// ReactiveProperty - a two way bindable declarative observable property with imperative get set.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <seealso cref="ReactiveObject" />
/// <seealso cref="IReactiveProperty&lt;T&gt;" />
[DataContract]
public class ReactiveProperty<T> : ReactiveObject, IReactiveProperty<T>
{
private readonly IScheduler _scheduler;
private readonly CompositeDisposable _disposables = [];
private T? _value;

/// <summary>
/// Initializes a new instance of the <see cref="ReactiveProperty{T}"/> class.
/// </summary>
public ReactiveProperty() => _scheduler = RxApp.TaskpoolScheduler;

/// <summary>
/// Initializes a new instance of the <see cref="ReactiveProperty{T}"/> class.
/// </summary>
/// <param name="initialValue">The initial value.</param>
public ReactiveProperty(T? initialValue)
{
Value = initialValue;
_scheduler = RxApp.TaskpoolScheduler;
}

/// <summary>
/// Initializes a new instance of the <see cref="ReactiveProperty{T}"/> class.
/// </summary>
/// <param name="initialValue">The initial value.</param>
/// <param name="scheduler">The scheduler.</param>
public ReactiveProperty(T? initialValue, IScheduler? scheduler)
{
Value = initialValue;
_scheduler = scheduler ?? RxApp.TaskpoolScheduler;
}

/// <summary>
/// Gets a value indicating whether gets a value that indicates whether the object is disposed.
/// </summary>
public bool IsDisposed => _disposables.IsDisposed;

/// <summary>
/// Gets or sets the value.
/// </summary>
/// <value>
/// The value.
/// </value>
[DataMember]
[JsonInclude]
public T? Value
{
get => _value;
set => this.RaiseAndSetIfChanged(ref _value, value);
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Notifies the provider that an observer is to receive notifications.
/// </summary>
/// <param name="observer">The object that is to receive notifications.</param>
/// <returns>
/// A reference to an interface that allows observers to stop receiving notifications before
/// the provider has finished sending them.
/// </returns>
public IDisposable Subscribe(IObserver<T?> observer) =>
this.WhenAnyValue(vm => vm.Value)
.ObserveOn(_scheduler)
.Subscribe(observer)
.DisposeWith(_disposables);

/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposables?.IsDisposed == false && disposing)
{
_disposables?.Dispose();
}
}
}

0 comments on commit cec4cca

Please sign in to comment.