Skip to content

Commit

Permalink
Merge pull request #40 from Cysharp/xaml-bindablereactiveproperty
Browse files Browse the repository at this point in the history
Add BindableReactiveProperty
  • Loading branch information
neuecc authored Jan 14, 2024
2 parents fd26e66 + f1ca4ea commit eb55456
Show file tree
Hide file tree
Showing 11 changed files with 660 additions and 46 deletions.
144 changes: 144 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ Additionally, there are other utilities for Disposables as follows.

```
Disposable.Create(Action);
Disposable.Dispose(...);
SingleAssignmentDisposable
SingleAssignmentDisposableCore // struct
SerialDisposable
Expand Down Expand Up @@ -438,6 +439,147 @@ Implement Custom Operator Guide
---
TODO:

XAML Platforms(`BindableReactiveProperty<T>`)
---
For XAML based application platforms, R3 provides `BindableReactiveProperty<T>` that can bind observable property to view like [Android LiveData](https://developer.android.com/topic/libraries/architecture/livedata) and [Kotlin StateFlow](https://developer.android.com/kotlin/flow/.stateflow-and-sharedflow).

Simple usage, expose `BindableReactiveProperty<T>` via `new` or `ToBindableReactiveProperty`.

Here is the simple In and Out BindableReactiveProperty ViewModel, Xaml and code-behind. In xaml, `.Value` to bind property.

```csharp
public class BasicUsagesViewModel : IDisposable
{
public BindableReactiveProperty<string> Input { get; }
public BindableReactiveProperty<string> Output { get; }

public BasicUsagesViewModel()
{
Input = new BindableReactiveProperty<string>("");
Output = Input.Select(x => x.ToUpper()).ToBindableReactiveProperty("");
}

public void Dispose()
{
Disposable.Dispose(Input, Output);
}
}
```

```xml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:BasicUsagesViewModel />
</Window.DataContext>
<StackPanel>
<TextBlock Text="Basic usages" FontSize="24" />

<Label Content="Input" />
<TextBox Text="{Binding Input.Value, UpdateSourceTrigger=PropertyChanged}" />

<Label Content="Output" />
<TextBlock Text="{Binding Output.Value}" />
</StackPanel>
</Window>
```

```csharp
namespace WpfApp1;

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}

protected override void OnClosed(EventArgs e)
{
(this.DataContext as IDisposable)?.Dispose();
}
}
```

![image](https://github.com/Cysharp/R3/assets/46207/01c3738f-e941-412e-b517-8e7867d6f709)

BindableReactiveProperty also supports validation via DataAnnotation or custom logic. If you want to use DataAnnotation attribute, require to call `EnableValidation<T>()` in field initializer or `EnableValidation(Expression selfSelector)` in constructor.

```csharp
public class ValidationViewModel : IDisposable
{
// Pattern 1. use EnableValidation<T> to enable DataAnnotation validation in field initializer
[Range(0.0, 300.0)]
public BindableReactiveProperty<double> Height { get; } = new BindableReactiveProperty<double>().EnableValidation<ValidationViewModel>();

[Range(0.0, 300.0)]
public BindableReactiveProperty<double> Weight { get; }

IDisposable customValidation1Subscription;
public BindableReactiveProperty<double> CustomValidation1 { get; set; }

public BindableReactiveProperty<double> CustomValidation2 { get; set; }

public ValidationViewModel()
{
// Pattern 2. use EnableValidation(Expression) to enable DataAnnotation validation
Weight = new BindableReactiveProperty<double>().EnableValidation(() => Weight);

// Pattern 3. EnableValidation() and call OnErrorResume to set custom error meessage
CustomValidation1 = new BindableReactiveProperty<double>().EnableValidation();
customValidation1Subscription = CustomValidation1.Subscribe(x =>
{
if (0.0 <= x && x <= 300.0) return;

CustomValidation1.OnErrorResume(new Exception("value is not in range."));
});

// Pattern 4. simplified version of Pattern3, EnableValidation(Func<T, Exception?>)
CustomValidation2 = new BindableReactiveProperty<double>().EnableValidation(x =>
{
if (0.0 <= x && x <= 300.0) return null; // null is no validate result
return new Exception("value is not in range.");
});
}

public void Dispose()
{
Disposable.Dispose(Height, Weight, CustomValidation1, customValidation1Subscription, CustomValidation2);
}
}
```

```xml
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:ValidationViewModel />
</Window.DataContext>

<StackPanel Margin="10">
<Label Content="Validation" />
<TextBox Text="{Binding Height.Value, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding Weight.Value, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding CustomValidation1.Value, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding CustomValidation2.Value, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Window>
```

![image](https://github.com/Cysharp/R3/assets/46207/f80149e6-1573-46b5-9a77-b78776dd3527)

Platform Supports
---
Even without adding specific platform support, it is possible to use only the core library. However, Rx becomes more user-friendly by replacing the standard `TimeProvider` and `FrameProvider` with those optimized for each platform. For example, while the standard `TimeProvider` is thread-based, using a UI thread-based `TimeProvider` for each platform can eliminate the need for dispatch through `ObserveOn`, enhancing usability. Additionally, since message loops differ across platforms, the use of individual `FrameProvider` is essential.
Expand Down Expand Up @@ -508,6 +650,8 @@ In addition to the above, the following `ObserveOn`/`SubscribeOn` methods have b
* SubscribeOnDispatcher
* SubscribeOnCurrentDispatcher

ViewModel binding support, see [`BindableReactiveProperty<T>`](#xaml-platformsbindablereactivepropertyt) section.

### Avalonia

> PM> Install-Package [R3.Avalonia](https://www.nuget.org/packages/R3.Avalonia)
Expand Down
39 changes: 32 additions & 7 deletions sandbox/ConsoleApp1/Program.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
using ConsoleApp1;
using R3;
using System.ComponentModel.DataAnnotations;



//Dump.Factory();

var hoge = new Hoge();

hoge.MyProperty1.ErrorsChanged += MyProperty1_ErrorsChanged;
hoge.MyProperty2.ErrorsChanged += MyProperty2_ErrorsChanged;

var subject = new Subject<int>();
for (int i = 0; i < 10000; i++)
void MyProperty1_ErrorsChanged(object? sender, System.ComponentModel.DataErrorsChangedEventArgs e)
{
subject.Subscribe();
foreach (var item in hoge.MyProperty1.GetErrors(null!))
{
Console.WriteLine(item);
}
}

subject.Subscribe();
subject.OnCompleted();
Console.WriteLine(subject);
void MyProperty2_ErrorsChanged(object? sender, System.ComponentModel.DataErrorsChangedEventArgs e)
{
foreach (var item in hoge.MyProperty2.GetErrors(null!))
{
Console.WriteLine(item);
}
}
hoge.MyProperty1.Value = 30;
hoge.MyProperty2.Value = 40;


//SubscriptionTracker.EnableTracking = true;
Expand Down Expand Up @@ -71,3 +82,17 @@
// }
//}


public class Hoge
{
[Range(1, 10)]
public BindableReactiveProperty<int> MyProperty1 { get; set; } = new BindableReactiveProperty<int>().EnableValidation<Hoge>();

[Range(1, 10)]
public BindableReactiveProperty<int> MyProperty2 { get; set; }

public Hoge()
{
MyProperty2 = new BindableReactiveProperty<int>().EnableValidation(() => MyProperty2);
}
}
22 changes: 19 additions & 3 deletions sandbox/WpfApp1/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,23 @@
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<TextBlock Name="textBlock" HorizontalAlignment="Left" Margin="108,131,0,0" TextWrapping="Wrap" Text="TextBlock" VerticalAlignment="Top" Height="50" Width="292"/>
</Grid>
<Window.DataContext>
<!--<local:BasicUsagesViewModel />-->
<local:ValidationViewModel />
</Window.DataContext>
<!--<StackPanel>
<TextBlock Text="Basic usages" FontSize="24" />
<Label Content="Input" />
<TextBox Text="{Binding Input.Value, UpdateSourceTrigger=PropertyChanged}" />
<Label Content="Output" />
<TextBlock Text="{Binding Output.Value}" />
</StackPanel>-->

<StackPanel Margin="10">
<Label Content="Validation" />
<TextBox Text="{Binding Height.Value, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding Weight.Value, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding CustomValidation1.Value, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding CustomValidation2.Value, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</Window>
83 changes: 76 additions & 7 deletions sandbox/WpfApp1/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using R3;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media.Media3D;

namespace WpfApp1;
/// <summary>
Expand All @@ -12,6 +14,9 @@ public MainWindow()
{
InitializeComponent();

//var vm = new BasicUsagesViewModel();
//vm.Input.Value = "hogemogehugahuga";
//this.DataContext = new BasicUsagesViewModel();


//Dispatcher.Yield(DispatcherPriority.Input);
Expand All @@ -25,22 +30,86 @@ public MainWindow()
//Observable.EveryValueChanged(this, x => x.Width).Subscribe(x => textBlock.Text = x.ToString());
// this.ObserveEveryValueChanged(x => x.Height).Subscribe(x => HeightText.Text = x.ToString());

var sw = Stopwatch.StartNew();
// var sw = Stopwatch.StartNew();

//System.Reactive.Linq.Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5)).Subscribe(_ =>
//{
// textBlock.Text = "Hello World:" + sw.Elapsed;
//});
Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5))
// // .ObserveOnCurrentDispatcher()
.Subscribe(_ =>
{
textBlock.Text = "Hello World:" + sw.Elapsed;
});
//Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5))
//// // .ObserveOnCurrentDispatcher()
// .Subscribe(_ =>
// {
// textBlock.Text = "Hello World:" + sw.Elapsed;
// });

//Observable.TimerFrame(50, 100).Subscribe(_ =>
//{
// textBlock.Text = "Hello World:" + ObservableSystem.DefaultFrameProvider.GetFrameCount();
//});
}

protected override void OnClosed(EventArgs e)
{
(this.DataContext as IDisposable)?.Dispose();
}
}

public class BasicUsagesViewModel : IDisposable
{
public BindableReactiveProperty<string> Input { get; }
public BindableReactiveProperty<string> Output { get; }

public BasicUsagesViewModel()
{
Input = new BindableReactiveProperty<string>("");
Output = Input.Select(x => x.ToUpper()).ToBindableReactiveProperty("");
}

public void Dispose()
{
Disposable.Dispose(Input, Output);
}
}

public class ValidationViewModel : IDisposable
{
// Pattern 1. use EnableValidation<T> to enable DataAnnotation validation in field initializer
[Range(0.0, 300.0)]
public BindableReactiveProperty<double> Height { get; } = new BindableReactiveProperty<double>().EnableValidation<ValidationViewModel>();

[Range(0.0, 300.0)]
public BindableReactiveProperty<double> Weight { get; }

IDisposable customValidation1Subscription;
public BindableReactiveProperty<double> CustomValidation1 { get; set; }

public BindableReactiveProperty<double> CustomValidation2 { get; set; }

public ValidationViewModel()
{
// Pattern 2. use EnableValidation(Expression) to enable DataAnnotation validation
Weight = new BindableReactiveProperty<double>().EnableValidation(() => Weight);

// Pattern 3. EnableValidation() and call OnErrorResume to set custom error meessage
CustomValidation1 = new BindableReactiveProperty<double>().EnableValidation();
customValidation1Subscription = CustomValidation1.Subscribe(x =>
{
if (0.0 <= x && x <= 300.0) return;

CustomValidation1.OnErrorResume(new Exception("value is not in range."));
});

// Pattern 4. simplified version of Pattern3, EnableValidation(Func<T, Exception?>)
CustomValidation2 = new BindableReactiveProperty<double>().EnableValidation(x =>
{
if (0.0 <= x && x <= 300.0) return null; // null is no validate result
return new Exception("value is not in range.");
});
}

public void Dispose()
{
Disposable.Dispose(Height, Weight, CustomValidation1, customValidation1Subscription, CustomValidation2);
}
}
Loading

0 comments on commit eb55456

Please sign in to comment.