Merge pull request #2520 from unoplatform/feat/istate.fluent.api

feat: Implement FluentAPI
This commit is contained in:
Andres Pineda 2024-08-27 12:21:59 -04:00 коммит произвёл GitHub
Родитель 09fb50b49c f7184babfd
Коммит 1e08674c6a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
12 изменённых файлов: 619 добавлений и 84 удалений

Просмотреть файл

@ -4,15 +4,15 @@ uid: Uno.Extensions.Mvux.Advanced.Selection
# Selection
MVUX has embedded support for both [single item](#single-item-selection) and [multi-item selection](#multi-item-selection).
Any control that inherits [`Selector`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.primitives.selector) (e.g. [`ListView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.listview), [`GridView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.gridview), [`ComboBox`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.combobox), [`FlipView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.flipview)), has automatic support for updating a List-State with its current selection.
Binding to the `SelectedItem` property is not even required, as this works automatically.
MVUX has built-in support for both [single item](#single-item-selection) and [multi-item selection](#multi-item-selection).
Any control that inherits [`Selector`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.primitives.selector) (e.g. [`ListView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.listview), [`GridView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.gridview), [`ComboBox`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.combobox), [`FlipView`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.flipview)), has automatic support for updating a List-State with its current selection.
Binding to the `SelectedItem` property is not even required, as this works automatically.
To synchronize to the selected value in the `Model` side, use the `Selection` operator of the `IListFeed`.
## Recap of the *PeopleApp* example
We'll be using the *PeopleApp* example which we've built step-by-step in [this tutorial](xref:Uno.Extensions.Mvux.HowToListFeed).
We'll be using the *PeopleApp* example which we've built step-by-step in [this tutorial](xref:Uno.Extensions.Mvux.HowToListFeed).
The *PeopleApp* uses an `IListFeed<T>` where `T` is a `Person` [record](https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/record) with the properties `FirstName` and `LastName`.
It has a service that has the following contract:
@ -50,19 +50,19 @@ The data is then displayed on the View using a `ListView`:
</Page>
```
> [!NOTE]
> [!NOTE]
> The use of the `FeedView` is not necessary in our example, hence the `ListView` has been extracted from it, and its `ItemsSource` property has been directly data-bound to the Feed.
### Implement selection in the *PeopleApp*
MVUX has two extension methods of `IListFeed<T>`, that enable single or multi-selection.
> [!NOTE]
> [!NOTE]
> The source code for the sample app demonstrated in this section can be found on [GitHub](https://github.com/unoplatform/Uno.Samples/tree/master/UI/MvuxHowTos/SelectionPeopleApp).
## Single-item selection
A Feed doesn't store any state, so the `People` property won't be able to hold any information, nor the currently selected item.
A Feed doesn't store any state, so the `People` property won't be able to hold any information, nor the currently selected item.
To enable storing the selected value in the model, we'll create an `IState<Person>` which will be updated by the `Selection` operator of the `IListFeed<T>` (it's an extension method).
Let's change the `PeopleModel` as follows:
@ -81,12 +81,12 @@ public partial record PeopleModel(IPeopleService PeopleService)
The `SelectedPerson` State is initialized with an empty value using `State<Person>.Empty(this)` (we still need a reference to the current instance to enable caching).
> [!NOTE]
> [!NOTE]
> Read [this](xref:Uno.Extensions.Mvux.States#other-ways-to-create-feeds) to learn more about States and the `Empty` factory method.
The `Selection` operator was added to the existing `ListFeed.Async(...)` line, it will listen to the `People` List-Feed and will affect its selection changes onto the `SelectedPerson` State property.
In the View side, wrap the `ListView` element in a `Grid`, and insert additional elements to reflect the currently selected value via the `SelectedPerson` State.
In the View side, wrap the `ListView` element in a `Grid`, and insert additional elements to display the currently selected value via the `SelectedPerson` State.
We'll also add a separator (using `Border`) to be able to distinguish them.
The View code shall look like the following:
@ -118,7 +118,7 @@ When running the app, the top section will reflect the item the user selects in
![A video demonstrating selection with MVUX](../Assets/Selection.gif)
> [!NOTE]
> [!NOTE]
> The source code for the sample app can be found [GitHub](https://github.com/unoplatform/Uno.Samples/tree/master/UI/MvuxHowTos/SelectionPeopleApp).
### Listening to the selected value
@ -139,9 +139,9 @@ A `TextBlock` can then be added in the UI to display the selected value:
<TextBlock Text="{Binding GreetingSelect}"/>
```
#### Using the ForEachAsync operator
#### Using the ForEach operator
Selection can also be propagated manually to a State using the [`ForEachAsync`](xref:Uno.Extensions.Mvux.States#foreachasync) operator.
Selection can also be propagated manually to a State using the [`ForEach`](xref:Uno.Extensions.Mvux.States#foreach) operator.
First, we need to create a State with a default value, which will be used to store the processed value once a selection has occurred.
```csharp
@ -155,11 +155,11 @@ public partial record PeopleModel
{
private IPeopleService _peopleService;
public PeopleModel(IPeopleService peopleService)
public PeopleModel(IPeopleService peopleService)
{
_peopleService = peopleService;
SelectedPerson.ForEachAsync(action: SelectionChanged);
SelectedPerson.ForEach(action: SelectionChanged);
}
...
@ -176,7 +176,7 @@ public partial record PeopleModel
The `ForEach` operator listens to a selection occurrence and invokes the `SelectionChanged` callback with the newly available data, in this case, the recently selected `Person` entity.
> [!TIP]
> [!TIP]
> MVUX takes care of the lifetime of the subscription, so it will be disposed of along with its declaring `Model` being garbage-collected.
#### On-demand using a Command parameter
@ -192,7 +192,7 @@ public ValueTask CheckSelection(Person selectedPerson)
In the above example, since `selectedPerson` has the same name as the `SelectedPerson` feed, it will be automatically evaluated and provided as a parameter on the command execution.
> [!TIP]
> [!TIP]
> This behavior can also be controlled using attributes.
> To learn more about commands and how they can be configured using attributes, refer to the [Commands](xref:Uno.Extensions.Mvux.Advanced.Commands) page.
@ -224,12 +224,12 @@ public partial record PeopleModel(IPeopleService PeopleService)
Head to the View and enable multi-selection in the `ListView` by changing its `SelectionMode` property to `Multiple`.
> [!NOTE]
> [!NOTE]
> The source code for the sample app can be found [here](https://github.com/unoplatform/Uno.Samples/tree/master/UI/MvuxHowTos/SelectionPeopleApp).
## Manual selection
The options above explained how to subscribe to selection that has been requested in the View by a Selector control (i.e. `ListView`).
The options above explained how to subscribe to selection that has been requested in the View by a Selector control (i.e. `ListView`).
If you want to manually select an item or multiple items, rather use a [List-State](xref:Uno.Extensions.Mvux.ListStates) instead of a List-Feed to load the items, so that you can update their selection state. You can then use the List-State's selection operators to manually select items.
Refer to the [selection operators](xref:Uno.Extensions.Mvux.ListStates#selection-operators) section in the List-State page for documentation on how to use manual selection.

Просмотреть файл

@ -94,7 +94,7 @@ await MyStrings.InsertAsync("Margaret Atwood", cancellationToken);
There are various ways to update values in the list-state:
The `Update` method has an `updater` parameter like the `State` does.
The `Update` method has an `updater` parameter like the `State` does.
This parameter is a `Func<IImmutableList<T>, IImmutableList<T>>`, which when called passes in the existing collection, allows you to apply your modifications to it, and then returns it.
For example:
@ -134,12 +134,12 @@ await MyStrings.RemoveAllAsync(
ct: cancellationToken);
```
#### ForEachAsync
#### ForEach
This operator can be called from an `IListState<T>` to execute an asynchronous action when the data changes. The action is invoked once for the entire set of data, rather than for individual items:
```csharp
await MyStrings.ForEachAsync(async(list, ct) => await PerformAction(items, ct));
await MyStrings.ForEach(async(list, ct) => await PerformAction(items, ct));
...
@ -152,7 +152,7 @@ private async ValueTask PerformAction(IImmutableList<string> items, Cancellation
### Selection operators
Like list-feed, list-state provides out-the-box support for Selection.
Like list-feed, list-state provides out-the-box support for Selection.
This feature enables flagging single or multiple items in the State as 'selected'.
Selection works seamlessly and automatically with the `ListView` and other selection controls.
@ -199,5 +199,5 @@ await MyStrings.ClearSelection(cancellationToken);
### Subscribing to the selection
You can create a Feed that reflects the currently selected item or items (when using multi-selection) of a Feed.
You can create a Feed that reflects the currently selected item or items (when using multi-selection) of a Feed.
This is explained in detail in the [Selection page](xref:Uno.Extensions.Mvux.Advanced.Selection).

Просмотреть файл

@ -10,10 +10,10 @@ Like [feeds](xref:Uno.Extensions.Mvux.Feeds), states are used to manage asynchro
Contrary to Feeds, states are stateful (as the name suggests) in that they keep a record of the current data value. States also allow the current value to be modified, which is useful for two-way binding scenarios.
MVUX utilizes its powerful code-generation engine to generate a bindable proxy for each Model, which holds the state information of the data, as well as a bindable proxy for entities where needed, for instance, if the entities are immutable (e.g. records - the recommended type).
MVUX utilizes its powerful code-generation engine to generate a bindable proxy for each Model, which holds the state information of the data, as well as a bindable proxy for entities where needed, for instance, if the entities are immutable (e.g. records - the recommended type).
The bindable proxies use as a bridge that enables immutable entities to work with the WinUI data-binding engine. The states in the Model are monitored for data-binding changes, and in response to any change, the objects are recreated fresh, instead of their properties being changed.
States keep the current value of the data, so every new subscription to them, (such as awaiting them or binding them to an additional control, etc.), will use the data currently loaded in the state (if any).
States keep the current value of the data, so every new subscription to them, (such as awaiting them or binding them to an additional control, etc.), will use the data currently loaded in the state (if any).
Like a feed, states can be reloaded, which will invoke the asynchronous operation that is used to create the state.
@ -52,7 +52,7 @@ A State can also be created from an Async Enumerable as follows:
public IState<StockValue> MyStockCurrentValue => State.AsyncEnumerable(this, ContactsService.GetMyStockCurrentValue);
```
Make sure the Async Enumerable methods have a `CancellationToken` parameter and are decorated with the `EnumerationCancellation` attribute.
Make sure the Async Enumerable methods have a `CancellationToken` parameter and are decorated with the `EnumerationCancellation` attribute.
You can learn more about Async Enumerables in [this article](https://learn.microsoft.com/archive/msdn-magazine/2019/november/csharp-iterating-with-async-enumerables-in-csharp-8#a-tour-through-async-enumerables).
#### Start with an empty state
@ -83,7 +83,7 @@ public IState<int> MyState => State.FromFeed(this, MyFeed);
#### Other ways to create states
> [!TIP]
> A state can also be constructed manually by building its underlying Messages or Options.
> A state can also be constructed manually by building its underlying Messages or Options.
> This is intended for advanced users and is explained [here](xref:Uno.Extensions.Reactive.State#create).
### Usage of States
@ -111,7 +111,7 @@ States are built to be cooperating with the data-binding engine. A State will au
1. Replace all child elements in the _MainPage.xaml_ with the following:
```xml
<Page
<Page
x:Class="SliderApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@ -119,15 +119,15 @@ States are built to be cooperating with the data-binding engine. A State will au
<Page.DataContext>
<local:BindableSliderModel />
</Page.DataContext>
<StackPanel>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="Current state value:" />
<TextBlock Text="{Binding SliderValue}" />
</StackPanel>
<Border Height="1" Background="DarkGray" />
<TextBlock Text="Set state value:"/>
<Slider Value="{Binding SliderValue, Mode=TwoWay}" />
</StackPanel>
@ -144,7 +144,7 @@ In this scenario, the `DataContext` is set to an instance of the `BindableSlider
#### Update
To manually update the current value of a state, use its `Update` method.
To manually update the current value of a state, use its `Update` method.
In this example we'll add the method `IncrementSlider` that gets the current value and increases it by one (if it doesn't exceed 100):
@ -168,14 +168,14 @@ There are additional methods that update the data of a State such as `Set` and `
```csharp
public async ValueTask SetSliderMiddle(CancellationToken ct = default)
{
{
await SliderValue.SetAsync(50, ct);
}
```
### Subscribing to changes
The `ForEachAsync` enables executing a callback each time the value of the `IState<T>` is updated.
The `ForEach` enables executing a callback each time the value of the `IState<T>` is updated.
This extension-method takes a single parameter which is a async callback that takes two parameters. The first parameter is of type `T?`, where `T` is type of the `IState`, and represents the new value of the state. The second parameter is a `CancellationToken` which can be used to cancel a long running action.
@ -188,7 +188,7 @@ public partial record Model
public async ValueTask EnableChangeTracking()
{
MyState.ForEachAsync(PerformAction);
MyState.ForEach(PerformAction);
}
public async ValueTask PerformAction(string item, CancellationToken ct)
@ -198,6 +198,22 @@ public partial record Model
}
```
Additionally, the `ForEach` method can be set using the Fluent API:
```csharp
public partial record Model
{
public IState<string> MyState => State.Value(this, "Initial value")
.ForEach(PerformAction);
public async ValueTask PerformAction(string item, CancellationToken ct)
{
...
}
}
```
### Commands
Part of the MVUX toolbox is the automatic generation of Commands.

Просмотреть файл

@ -0,0 +1,40 @@
using CommunityToolkit.Mvvm.Messaging;
namespace Uno.Extensions.Reactive.Messaging;
/// <summary>
/// Set of extensions to update an <see cref="IListState{T}"/> from messaging structures.
/// </summary>
public static class ListStateExtensions
{
/// <summary>
/// Listen for <see cref="EntityMessage{TEntity}"/> on the given <paramref name="messenger"/> and updates the <paramref name="listState"/> accordingly.
/// </summary>
/// <typeparam name="TEntity">Type of the value of the state.</typeparam>
/// <typeparam name="TKey">Type of the identifier that uniquely identifies a <typeparamref name="TEntity"/>.</typeparam>
/// <param name="listState">The list state to update.</param>
/// <param name="messenger">The messenger to listen for <see cref="EntityMessage{TEntity}"/></param>
/// <param name="keySelector">A selector to get a unique identifier of a <typeparamref name="TEntity"/>.</param>
/// <param name="disposable"> A disposable that can be used to unbind the state from the messenger.</param>
/// <returns>An <see cref="IListState{TEntity}"/> that can be used to chain other operations.</returns>
public static IListState<TEntity> Observe<TEntity, TKey>(this IListState<TEntity> listState, IMessenger messenger, Func<TEntity, TKey> keySelector, out IDisposable disposable)
{
disposable = messenger.Observe(listState, keySelector);
return listState;
}
/// <summary>
/// Listen for <see cref="EntityMessage{TEntity}"/> on the given <paramref name="messenger"/> and updates the <paramref name="listState"/> accordingly.
/// </summary>
/// <typeparam name="TEntity">Type of the value of the state.</typeparam>
/// <typeparam name="TKey">Type of the identifier that uniquely identifies a <typeparamref name="TEntity"/>.</typeparam>
/// <param name="listState">The list state to update.</param>
/// <param name="messenger">The messenger to listen for <see cref="EntityMessage{TEntity}"/></param>
/// <param name="keySelector">A selector to get a unique identifier of a <typeparamref name="TEntity"/>.</param>
/// <returns>An <see cref="IListState{TEntity}"/> that can be used to chain other operations.</returns>
public static IListState<TEntity> Observe<TEntity, TKey>(this IListState<TEntity> listState, IMessenger messenger, Func<TEntity, TKey> keySelector)
{
_ = messenger.Observe(listState, keySelector);
return listState;
}
}

Просмотреть файл

@ -23,7 +23,10 @@ public static class MessengerExtensions
/// <param name="keySelector">A selector to get a unique identifier of a <typeparamref name="TEntity"/>.</param>
/// <returns>A disposable that can be used to unbind the state from the messenger.</returns>
public static IDisposable Observe<TEntity, TKey>(this IMessenger messenger, IState<TEntity> state, Func<TEntity, TKey> keySelector)
=> AttachedProperty.GetOrCreate(state, keySelector, messenger, (s, ks, msg) => new Recipient<IState<TEntity>, TEntity, TKey>(s, msg, ks, StateExtensions.Update));
=> AttachedProperty.GetOrCreate(owner: state,
key: keySelector,
state: messenger,
factory: static (s, ks, msg) => new Recipient<IState<TEntity>, TEntity, TKey>(s, msg, ks, StateExtensions.Update));
/// <summary>
/// Listen for <see cref="EntityMessage{TEntity}"/> on the given <paramref name="messenger"/> and updates the <paramref name="listState"/> accordingly.

Просмотреть файл

@ -2,8 +2,10 @@ using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Uno.Extensions.Reactive.Core;
using Uno.Extensions.Reactive.Sources;
using Uno.Extensions.Reactive.Utils;
namespace Uno.Extensions.Reactive.Messaging;
@ -153,4 +155,37 @@ public static class StateExtensions
return await Task.WhenAny(refreshed, messageListener) == refreshed;
}
/// <summary>
/// Listen for <see cref="EntityMessage{TEntity}"/> on the given <paramref name="messenger"/> and updates the <paramref name="state"/> accordingly.
/// </summary>
/// <typeparam name="TEntity">Type of the value of the state.</typeparam>
/// <typeparam name="TKey">Type of the identifier that uniquely identifies a <typeparamref name="TEntity"/>.</typeparam>
/// <param name="messenger">The messenger to listen for <see cref="EntityMessage{TEntity}"/></param>
/// <param name="state">The state to update.</param>
/// <param name="keySelector">A selector to get a unique identifier of a <typeparamref name="TEntity"/>.</param>
/// <returns>An <see cref="IState"/> that can be used to chain other operations.</returns>
public static IState<TEntity> Observe<TEntity, TKey>(this IState<TEntity> state, IMessenger messenger, Func<TEntity, TKey> keySelector)
{
_ = messenger.Observe(state, keySelector);
return state;
}
/// <summary>
/// Listen for <see cref="EntityMessage{TEntity}"/> on the given <paramref name="messenger"/> and updates the <paramref name="state"/> accordingly.
/// </summary>
/// <typeparam name="TEntity">Type of the value of the state.</typeparam>
/// <typeparam name="TKey">Type of the identifier that uniquely identifies a <typeparamref name="TEntity"/>.</typeparam>
/// <param name="messenger">The messenger to listen for <see cref="EntityMessage{TEntity}"/></param>
/// <param name="state">The state to update.</param>
/// <param name="keySelector">A selector to get a unique identifier of a <typeparamref name="TEntity"/>.</param>
/// <param name="disposable"> A <see cref="IDisposable"/> that can be used to remove the callback registration.</param>
/// <returns>An <see cref="IState"/> that can be used to chain other operations.</returns>
public static IState<TEntity> Observe<TEntity, TKey>(this IState<TEntity> state, IMessenger messenger, Func<TEntity, TKey> keySelector, out IDisposable disposable)
{
disposable = messenger.Observe(state, keySelector);
return state;
}
}

Просмотреть файл

@ -44,6 +44,94 @@ public class Given_Messaging : FeedTests
result.Should().BeEquivalentTo(new MyEntity(42, 1));
}
[TestMethod]
public async Task When_Fluent_Value_Updated_Then_StateUpdated()
{
var messenger = new WeakReferenceMessenger();
var state = State
.Value(this, () => new MyEntity(42))
.Observe(messenger, i => i.Key);
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Updated, new(42, 1)));
var result = await state;
result.Should().BeEquivalentTo(new MyEntity(42, 1));
}
[TestMethod]
public async Task When_Fluent_Multiple_Observe_Value_Updated_Then_StateUpdated_Once()
{
int callsCount = 0;
var messenger = new WeakReferenceMessenger();
var state = State.Value(this, () => new MyEntity(42))
.Observe(messenger, i => i.Key)
.Observe(messenger, i => i.Key)
.ForEach(async (i, ct) => callsCount++);
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Updated, new(42, 1)));
var result = await state;
result.Should().BeEquivalentTo(new MyEntity(42, 1));
callsCount.Should().Be(1);
}
[TestMethod]
public async Task When_Mixed_Multiple_Observe_Value_Updated_Then_StateUpdated_Once()
{
int callsCount = 0;
var messenger = new WeakReferenceMessenger();
var state = State.Value(this, () => new MyEntity(42))
.Observe(messenger, i => i.Key)
.ForEach(async (i, ct) => callsCount++);
messenger.Observe(state, i => i.Key);
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Updated, new(42, 1)));
var result = await state;
result.Should().BeEquivalentTo(new MyEntity(42, 1));
callsCount.Should().Be(1);
}
[TestMethod]
public async Task When_Fluent_Value_Updated_Then_StateUpdated_And_ForEach()
{
var versions = new List<int>();
var messenger = new WeakReferenceMessenger();
var state = State.Value(this, () => new MyEntity(42))
.Observe(messenger, i => i.Key)
.ForEach(async (i, ct) => versions.Add(i!.Version));
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Updated, new(42, 3)));
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Updated, new(42, 4)));
var result = await state;
result.Should().BeEquivalentTo(new MyEntity(42, 4));
versions.Should().BeEquivalentTo(new[] { 3, 4 });
}
[TestMethod]
public async Task When_Fluent_Async_Updated_Then_StateUpdated_And_ForEach()
{
var versions = new List<int>();
var messenger = new WeakReferenceMessenger();
var state = State.Async(this, async ct => new MyEntity(42))
.Observe(messenger, i => i.Key)
.ForEach(async (i, ct) => versions.Add(i!.Version));
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Updated, new(42, 5)));
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Updated, new(42, 1)));
var result = await state;
result.Should().BeEquivalentTo(new MyEntity(42, 1));
versions.Should().BeEquivalentTo(new[] { 5, 1 });
}
[TestMethod]
public async Task When_Deleted_Then_StateUpdated()
{
@ -72,6 +160,19 @@ public class Given_Messaging : FeedTests
result.Should().BeEquivalentTo(Items(42));
}
[TestMethod]
public async Task When_Fluent_Created_Then_ListStateUpdated()
{
var messenger = new WeakReferenceMessenger();
var state = ListState<MyEntity>.Empty(this)
.Observe(messenger, i => i.Key);
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Created, new(42)));
var result = await state;
result.Should().BeEquivalentTo(Items(42));
}
[TestMethod]
public async Task When_Updated_Then_ListStateUpdated()
{
@ -86,6 +187,19 @@ public class Given_Messaging : FeedTests
result.Should().BeEquivalentTo(Items((42, 1)));
}
[TestMethod]
public async Task When_Fluent_Updated_Then_ListStateUpdated()
{
var messenger = new WeakReferenceMessenger();
var state = ListState<MyEntity>.Value(this, () => Items(42))
.Observe(messenger, i => i.Key);
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Updated, new(42, 1)));
var result = await state;
result.Should().BeEquivalentTo(Items((42, 1)));
}
[TestMethod]
public async Task When_Deleted_Then_ListStateUpdated()
{
@ -211,6 +325,21 @@ public class Given_Messaging : FeedTests
result.Should().BeEquivalentTo(new MyEntity(42));
}
[TestMethod]
public async Task When_Fluent_DisposedAndUpdated_Then_StateNotUpdated()
{
var messenger = new WeakReferenceMessenger();
var state = State.Value(this, () => new MyEntity(42))
.Observe(messenger, i => i.Key, out var disposable);
disposable.Dispose();
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Updated, new(42, 1)));
var result = await state;
result.Should().BeEquivalentTo(new MyEntity(42));
}
[TestMethod]
public async Task When_DisposedAndUpdated_Then_ListStateNotUpdated()
{
@ -225,6 +354,21 @@ public class Given_Messaging : FeedTests
result.Should().BeEquivalentTo(Items(42));
}
[TestMethod]
public async Task When_Fluent_DisposedAndUpdated_Then_ListStateNotUpdated()
{
var messenger = new WeakReferenceMessenger();
var state = ListState<MyEntity>.Value(this, () => Items(42))
.Observe(messenger, i => i.Key, out var sut);
sut.Dispose();
messenger.Send(new EntityMessage<MyEntity>(EntityChange.Updated, new(42, 1)));
var result = await state;
result.Should().BeEquivalentTo(Items(42));
}
[TestMethod]
public async Task When_DisposedAndUpdatedUsingOther_Then_StateNotUpdated()
{

Просмотреть файл

@ -7,6 +7,8 @@ using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Uno.Extensions.Equality;
using Uno.Extensions.Reactive.Messaging;
using Uno.Extensions.Reactive.Testing;
namespace Uno.Extensions.Reactive.Tests.Extensions;
@ -15,39 +17,39 @@ namespace Uno.Extensions.Reactive.Tests.Extensions;
public class Given_StateForEach : FeedTests
{
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))] // Note: This is a compilation tests!
[ExpectedException(typeof(ArgumentNullException))] // Note: This is a compilation tests!
public async Task When_ForEachAsync_Then_AcceptsNotNullAndStruct()
{
default(IState<int>)!.ForEachAsync(async (i, ct) => this.ToString());
default(IState<int?>)!.ForEachAsync(async (i, ct) => this.ToString());
default(IState<string>)!.ForEachAsync(async (i, ct) => this.ToString());
_ = default(IState<int>)!.ForEach(async (i, ct) => this.ToString());
_ = default(IState<int?>)!.ForEach(async (i, ct) => this.ToString());
_ = default(IState<string>)!.ForEach(async (i, ct) => this.ToString());
#nullable disable
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
default(IState<string?>)!.ForEachAsync(async (i, ct) => this.ToString());
_ = default(IState<string?>)!.ForEach(async (i, ct) => this.ToString());
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#nullable restore
default(IState<MyStruct>)!.ForEachAsync(async (i, ct) => this.ToString());
default(IState<MyStruct?>)!.ForEachAsync(async (i, ct) => this.ToString());
default(IState<MyClass>)!.ForEachAsync(async (i, ct) => this.ToString());
_ = default(IState<MyStruct>)!.ForEach(async (i, ct) => this.ToString());
_ = default(IState<MyStruct?>)!.ForEach(async (i, ct) => this.ToString());
_ = default(IState<MyClass>)!.ForEach(async (i, ct) => this.ToString());
#nullable disable
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
default(IState<MyClass?>)!.ForEachAsync(async (i, ct) => this.ToString());
_ = default(IState<MyClass?>)!.ForEach(async (i, ct) => this.ToString());
#pragma warning restore CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
#nullable restore
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))] // Note: This is a compilation tests!
[ExpectedException(typeof(ArgumentNullException))] // Note: This is a compilation tests!
public async Task When_ForEachDataAsync_Then_AcceptsNotNullAndStruct()
{
default(IState<int>)!.ForEachAsync(async (i, ct) => this.ToString());
default(IState<int?>)!.ForEachDataAsync(async (i, ct) => this.ToString());
default(IState<string>)!.ForEachDataAsync(async (i, ct) => this.ToString());
default(IState<string?>)!.ForEachDataAsync(async (i, ct) => this.ToString());
default(IState<MyStruct>)!.ForEachDataAsync(async (i, ct) => this.ToString());
default(IState<MyStruct?>)!.ForEachDataAsync(async (i, ct) => this.ToString());
default(IState<MyClass>)!.ForEachDataAsync(async (i, ct) => this.ToString());
default(IState<MyClass?>)!.ForEachDataAsync(async (i, ct) => this.ToString());
_ = default(IState<int>)!.ForEach(async (i, ct) => this.ToString());
_ = default(IState<int?>)!.ForEachData(async (i, ct) => this.ToString());
_ = default(IState<string>)!.ForEachData(async (i, ct) => this.ToString());
_ = default(IState<string?>)!.ForEachData(async (i, ct) => this.ToString());
_ = default(IState<MyStruct>)!.ForEachData(async (i, ct) => this.ToString());
_ = default(IState<MyStruct?>)!.ForEachData(async (i, ct) => this.ToString());
_ = default(IState<MyClass>)!.ForEachData(async (i, ct) => this.ToString());
_ = default(IState<MyClass?>)!.ForEachData(async (i, ct) => this.ToString());
}
[TestMethod]
@ -56,7 +58,69 @@ public class Given_StateForEach : FeedTests
var state = State.Value(this, () => 1);
var result = new List<int>();
state.ForEachAsync(async (i, ct) => result.Add(i));
_ = state.ForEach(async (i, ct) => result.Add(i));
await state.SetAsync(2, CT);
await state.SetAsync(3, CT);
await state.SetAsync(4, CT);
result.Should().BeEquivalentTo(new[] { 2, 3, 4 });
}
[TestMethod]
public async Task When_Fluent_UpdateState_Then_CallbackInvokedIgnoringInitialValue()
{
var result = new List<int>();
var state = State.Async(this, async ct => 1)
.ForEach(async (i, ct) => result.Add(i));
await state.SetAsync(2, CT);
await state.SetAsync(3, CT);
await state.SetAsync(4, CT);
result.Should().BeEquivalentTo(new[] { 2, 3, 4 });
}
[TestMethod]
public async Task When_Fluent_Multiple_UpdateState_Then_CallbackInvokedIgnoringInitialValue()
{
var result = new List<int>();
ValueTask UpdateResultAsync(int i, CancellationToken ct)
{
result.Add(i);
return ValueTask.CompletedTask;
}
var state = State.Async(this, async ct => 1)
.ForEach(UpdateResultAsync)
.ForEach(UpdateResultAsync);
await state.SetAsync(2, CT);
await state.SetAsync(3, CT);
await state.SetAsync(4, CT);
result.Should().BeEquivalentTo(new[] { 2, 3, 4 });
}
[TestMethod]
public async Task When_Mixed_Multiple_UpdateState_Then_CallbackInvokedIgnoringInitialValue()
{
var result = new List<int>();
ValueTask UpdateResultAsync(int i, CancellationToken ct)
{
result.Add(i);
return ValueTask.CompletedTask;
}
var state = State.Async(this, async ct => 1)
.ForEach(UpdateResultAsync);
await state.ForEach(UpdateResultAsync);
await state.SetAsync(2, CT);
await state.SetAsync(3, CT);
@ -73,7 +137,7 @@ public class Given_StateForEach : FeedTests
var tcs1 = new TaskCompletionSource();
var tcs2 = new TaskCompletionSource();
state.ForEachAsync(async (i, ct) =>
_ = state.ForEach(async (i, ct) =>
{
await (i switch
{
@ -100,7 +164,7 @@ public class Given_StateForEach : FeedTests
var state = State.Value(this, () => 1);
var result = new List<int>();
state.ForEachAsync(async (i, ct) =>
await state.ForEach(async (i, ct) =>
{
if (i is 42)
{
@ -120,7 +184,24 @@ public class Given_StateForEach : FeedTests
public async Task When_DisposeState_Then_EnumerationStop()
{
var state = State.Value(this, () => 1);
var sut = state.ForEachAsync(async (i, ct) => this.ToString());
await state.ForEach(async (i, ct) => this.ToString(), out var sut);
await state.DisposeAsync();
var enumerationTask = sut.GetType().GetField("_task", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(sut) as Task;
if (enumerationTask is null)
{
Assert.Fail("Unable to get the private _task field of the StateListener<T>.");
}
enumerationTask.Status.Should().Be(TaskStatus.RanToCompletion);
}
[TestMethod]
public async Task When_Fluent_DisposeState_Then_EnumerationStop()
{
var state = State.Value(this, () => 1)
.ForEach(async (i, ct) => this.ToString(), out var sut);
await state.DisposeAsync();
@ -137,9 +218,29 @@ public class Given_StateForEach : FeedTests
public async Task When_DisposeExecute_Then_EnumerationStop()
{
var state = State.Value(this, () => 1);
var sut = state.ForEachAsync(async (i, ct) => this.ToString());
await state.ForEach(async (i, ct) => this.ToString(), out var sut);
sut.Dispose();
await state.SetAsync(42, CT);
var enumerationTask = sut.GetType().GetField("_task", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(sut) as Task;
if (enumerationTask is null)
{
Assert.Fail("Unable to get the private _task field of the StateListener<T>.");
}
enumerationTask.Status.Should().Be(TaskStatus.RanToCompletion);
}
[TestMethod]
public async Task When_Fluent_And_DisposeExecute_Then_EnumerationStop()
{
var state = State.Value(this, () => 1)
.ForEach(async (i, ct) => this.ToString(), out var sut);
sut.Dispose();
await state.SetAsync(42, CT);
var enumerationTask = sut.GetType().GetField("_task", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(sut) as Task;
@ -157,7 +258,20 @@ public class Given_StateForEach : FeedTests
var state = ListState.Value(this, () => ImmutableList.Create(1, 2, 3));
var result = new List<IImmutableList<int>>();
state.ForEachAsync(async (list, ct) => result.Add(list));
await state.ForEach(async (list, ct) => result.Add(list));
await state.UpdateDataAsync(_ => Option.None<IImmutableList<int>>(), CT);
result.Single().Should().NotBeNull().And.BeEquivalentTo(ImmutableList<int>.Empty);
}
[TestMethod]
public async Task When_Fluent_UpdateListStateWithNone_Then_CallbackGetsEmptyList()
{
var result = new List<IImmutableList<int>>();
var state = ListState.Value(this, () => ImmutableList.Create(1, 2, 3))
.ForEach(async (list, ct) => result.Add(list));
await state.UpdateDataAsync(_ => Option.None<IImmutableList<int>>(), CT);

Просмотреть файл

@ -6,6 +6,7 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Uno.Extensions.Equality;
using Uno.Extensions.Reactive.Utils;
namespace Uno.Extensions.Reactive;
@ -181,13 +182,26 @@ static partial class ListState
[EditorBrowsable(EditorBrowsableState.Never)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away
[Obsolete("Use ForEachAsync")]
[Obsolete("Use ForEach")]
#endif
public static IDisposable Execute<T>(this IListState<T> state, AsyncAction<IImmutableList<T>> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : notnull
=> ForEachAsync(state, action, caller, line);
/// <summary>
/// [DEPRECATED] Use .ForEach instead
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away
[Obsolete("Use ForEach")]
#endif
public static IDisposable ForEachAsync<T>(this IListState<T> state, AsyncAction<IImmutableList<T>> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : notnull
=> new StateForEach<IImmutableList<T>>(state, (list, ct) => action(list.SomeOrDefault() ?? ImmutableList<T>.Empty, ct), $"ForEachAsync defined in {caller} at line {line}.");
/// <summary>
/// Execute an async callback each time the state is being updated.
/// </summary>
@ -196,10 +210,41 @@ static partial class ListState
/// <param name="action">The callback to invoke on each update of the state.</param>
/// <param name="caller"> For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="line">For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <returns>A <see cref="IDisposable"/> that can be used to remove the callback registration.</returns>
public static IDisposable ForEachAsync<T>(this IListState<T> state, AsyncAction<IImmutableList<T>> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
/// <returns>A <see cref="IListState{T}"/> that can be used to chain other operations.</returns>
public static IListState<T> ForEach<T>(this IListState<T> state, AsyncAction<IImmutableList<T>> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : notnull
=> new StateForEach<IImmutableList<T>>(state, (list, ct) => action(list.SomeOrDefault() ?? ImmutableList<T>.Empty, ct), $"ForEachAsync defined in {caller} at line {line}.");
{
_ = AttachedProperty.GetOrCreate(
owner: state,
key: action,
state: (caller, line),
factory: static (s, a, d) => new StateForEach<IImmutableList<T>>(s, (list, ct) => a(list.SomeOrDefault() ?? ImmutableList<T>.Empty, ct), $"ForEach defined in {d.caller} at line {d.line}."));
return state;
}
/// <summary>
/// Execute an async callback each time the state is being updated.
/// </summary>
/// <typeparam name="T">The type of the state</typeparam>
/// <param name="state">The state to listen.</param>
/// <param name="action">The callback to invoke on each update of the state.</param>
/// <param name="caller"> For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="line">For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="disposable"> A <see cref="IDisposable"/> that can be used to remove the callback registration.</param>
/// <returns>A <see cref="IListState{T}"/> that can be used to chain other operations.</returns>
public static IListState<T> ForEach<T>(this IListState<T> state, AsyncAction<IImmutableList<T>> action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : notnull
{
disposable = AttachedProperty.GetOrCreate(
owner: state,
key: action,
state: (caller, line),
factory: static (s, a, d) => new StateForEach<IImmutableList<T>>(s, (list, ct) => a(list.SomeOrDefault() ?? ImmutableList<T>.Empty, ct), $"ForEachAsync defined in {d.caller} at line {d.line}."));
return state;
}
#endregion
/// <summary>

Просмотреть файл

@ -178,32 +178,71 @@ partial class State
=> SetAsync(state, value, ct);
/// <summary>
/// Execute an async callback each time the state is being updated.
/// [DEPRECATED] Use ForEach instead
/// </summary>
/// <typeparam name="T">The type of the state</typeparam>
/// <param name="state">The state to listen.</param>
/// <param name="action">The callback to invoke on each update of the state.</param>
/// <param name="caller"> For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="line">For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <returns>A <see cref="IDisposable"/> that can be used to remove the callback registration.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
#if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away
[Obsolete("Use ForEach")]
#endif
public static IDisposable ForEachAsync<T>(this IState<T> state, AsyncAction<T?> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : notnull
=> new StateForEach<T>(state, action.SomeOrNone(), $"ForEachAsync defined in {caller} at line {line}.");
/// <summary>
/// Execute an async callback each time the state is being updated.
/// [DEPRECATED] Use ForEach instead
/// </summary>
/// <typeparam name="T">The type of the state</typeparam>
/// <param name="state">The state to listen.</param>
/// <param name="action">The callback to invoke on each update of the state.</param>
/// <param name="caller"> For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="line">For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <returns>A <see cref="IDisposable"/> that can be used to remove the callback registration.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
#if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away
[Obsolete("Use ForEach")]
#endif
public static IDisposable ForEachAsync<T>(this IState<T?> state, AsyncAction<T?> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : struct
=> new StateForEach<T?>(state, action.SomeOrNone(), $"ForEachAsync defined in {caller} at line {line}.");
/// <summary>
/// Execute an async callback each time the state is being updated.
/// </summary>
/// <typeparam name="T">The type of the state</typeparam>
/// <param name="state">The state to listen.</param>
/// <param name="action">The callback to invoke on each update of the state.</param>
/// <param name="caller"> For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="line">For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <returns>An <see cref="IState"/> that can be used to chain other operations.</returns>
public static IState<T> ForEach<T>(this IState<T> state, AsyncAction<T?> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : notnull
{
_ = AttachedProperty.GetOrCreate(
owner: state,
key: action,
state: (caller, line),
factory: static (s, a, d) => new StateForEach<T>(s, a.SomeOrNone(), $"ForEach defined in {d.caller} at line {d.line}."));
return state;
}
/// <summary>
/// Execute an async callback each time the state is being updated.
/// </summary>
/// <typeparam name="T">The type of the state</typeparam>
/// <param name="state">The state to listen.</param>
/// <param name="action">The callback to invoke on each update of the state.</param>
/// <param name="caller"> For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="line">For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="disposable"> A <see cref="IDisposable"/> that can be used to remove the callback registration.</param>
/// <returns>An <see cref="IState"/> that can be used to chain other operations.</returns>
public static IState<T> ForEach<T>(this IState<T> state, AsyncAction<T?> action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : notnull
{
disposable = AttachedProperty.GetOrCreate(
owner: state,
key: action,
state: (caller, line),
factory: static (s, a, d) => new StateForEach<T>(s, a.SomeOrNone(), $"ForEach defined in {d.caller} at line {d.line}."));
return state;
}
/// <summary>
/// Execute an async callback each time the state is being updated.
/// </summary>
@ -213,17 +252,104 @@ partial class State
/// <param name="caller"> For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="line">For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <returns>A <see cref="IDisposable"/> that can be used to remove the callback registration.</returns>
public static IState<T?> ForEach<T>(this IState<T?> state, AsyncAction<T?> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : struct
{
_ = AttachedProperty.GetOrCreate(
owner: state,
key: action,
state: (caller, line),
factory: static (s, a, d) => new StateForEach<T?>(s, a.SomeOrNone(), $"ForEach defined in {d.caller} at line {d.line}."));
return state;
}
/// <summary>
/// Execute an async callback each time the state is being updated.
/// </summary>
/// <typeparam name="T">The type of the state</typeparam>
/// <param name="state">The state to listen.</param>
/// <param name="action">The callback to invoke on each update of the state.</param>
/// <param name="caller"> For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="line">For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="disposable"> A <see cref="IDisposable"/> that can be used to remove the callback registration.</param>
/// <returns>An <see cref="IState"/> that can be used to chain other operations.</returns>
public static IState<T?> ForEach<T>(this IState<T?> state, AsyncAction<T?> action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : struct
{
disposable = AttachedProperty.GetOrCreate(
owner: state,
key: action,
state: (caller, line),
factory: static (s, a, d) => new StateForEach<T?>(s, a.SomeOrNone(), $"ForEach defined in {d.caller} at line {d.line}."));
return state;
}
/// <summary>
/// [DEPRECATED] Use ForEachData instead
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
#if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away
[Obsolete("Use ForEachData")]
#endif
public static IDisposable ForEachDataAsync<T>(this IState<T> state, AsyncAction<Option<T>> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
=> new StateForEach<T>(state, action, $"ForEachDataAsync defined in {caller} at line {line}.");
/// <summary>
/// Execute an async callback each time the state is being updated.
/// </summary>
/// <typeparam name="T">The type of the state</typeparam>
/// <param name="state">The state to listen.</param>
/// <param name="action">The callback to invoke on each update of the state.</param>
/// <param name="caller"> For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="line">For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <returns>An <see cref="IState"/> that can be used to chain other operations.</returns>
public static IState<T> ForEachData<T>(this IState<T> state, AsyncAction<Option<T>> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
{
_ = AttachedProperty.GetOrCreate(
owner: state,
key: action,
state: (caller, line),
factory: static (s, a, d) => new StateForEach<T>(s, a, $"ForEachData defined in {d.caller} at line {d.line}."));
return state;
}
/// <summary>
/// Execute an async callback each time the state is being updated.
/// </summary>
/// <typeparam name="T">The type of the state</typeparam>
/// <param name="state">The state to listen.</param>
/// <param name="action">The callback to invoke on each update of the state.</param>
/// <param name="disposable"> A <see cref="IDisposable"/> that can be used to remove the callback registration.</param>
/// <param name="caller"> For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <param name="line">For debug purposes, the name of this subscription. DO NOT provide anything here, let the compiler fulfill this.</param>
/// <returns>An <see cref="IState"/> that can be used to chain other operations.</returns>
public static IState<T> ForEachData<T>(this IState<T> state, AsyncAction<Option<T>> action, out IDisposable disposable, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
{
disposable = AttachedProperty.GetOrCreate(
owner: state,
key: action,
state: (caller, line),
factory: static (s, a, d) => new StateForEach<T>(s, a, $"ForEachData defined in {d.caller} at line {d.line}."));
return state;
}
/// <summary>
/// [DEPRECATED] Use .ForEachAsync instead
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
#if DEBUG // To avoid usage in internal reactive code, but without forcing apps to update right away
[Obsolete("Use ForEachAsync")]
[Obsolete("Use ForEach")]
#endif
public static IDisposable Execute<T>(this IState<T> state, AsyncAction<T?> action, [CallerMemberName] string? caller = null, [CallerLineNumber] int line = -1)
where T : notnull
=> ForEachAsync(state, action, caller, line);
{
_ = ForEachAsync(state, action, caller, line);
return Disposable.Empty;
}
}

Просмотреть файл

@ -84,7 +84,9 @@ public static partial class State<T>
public static IState<T> Value<TOwner>(TOwner owner, Func<T> valueProvider)
where TOwner : class
// Note: We force the usage of delegate so 2 properties which are doing State.Value(this, () => 42) will effectively have 2 distinct states.
=> AttachedProperty.GetOrCreate(owner, valueProvider, static (o, v) => SourceContext.GetOrCreate(o).CreateState(Option.SomeOrNone(v())));
=> AttachedProperty.GetOrCreate(owner: owner,
key: valueProvider,
factory: static (o, v) => SourceContext.GetOrCreate(o).CreateState(Option.SomeOrNone(v())));
/// <summary>
/// Gets or creates a state from a static initial value.
@ -96,7 +98,9 @@ public static partial class State<T>
public static IState<T> Value<TOwner>(TOwner owner, Func<Option<T>> valueProvider)
where TOwner : class
// Note: We force the usage of delegate so 2 properties which are doing State.Value(this, () => 42) will effectively have 2 distinct states.
=> AttachedProperty.GetOrCreate(owner, valueProvider, static (o, v) => SourceContext.GetOrCreate(o).CreateState(v()));
=> AttachedProperty.GetOrCreate(owner: owner,
key: valueProvider,
factory: static (o, v) => SourceContext.GetOrCreate(o).CreateState(v()));
/// <summary>
/// Gets or creates a state from an async method.
@ -108,7 +112,9 @@ public static partial class State<T>
/// <returns>A feed that encapsulate the source.</returns>
public static IState<T> Async<TOwner>(TOwner owner, AsyncFunc<Option<T>> valueProvider, Signal? refresh = null)
where TOwner : class
=> AttachedProperty.GetOrCreate(owner, (valueProvider, refresh), static (o, args) => S(o, new AsyncFeed<T>(args.valueProvider, args.refresh)));
=> AttachedProperty.GetOrCreate(owner: owner,
key: (valueProvider, refresh),
factory: static (o, args) => S(o, new AsyncFeed<T>(args.valueProvider, args.refresh)));
/// <summary>
/// Gets or creates a state from an async method.
@ -132,7 +138,9 @@ public static partial class State<T>
/// <returns>A feed that encapsulate the source.</returns>
public static IState<T> Async<TOwner>(TOwner owner, AsyncFunc<T> valueProvider, Signal? refresh = null)
where TOwner : class
=> AttachedProperty.GetOrCreate(owner, (valueProvider, refresh), static (o, args) => S(o, new AsyncFeed<T>(args.valueProvider.SomeOrNoneWhenNotNull(), args.refresh)));
=> AttachedProperty.GetOrCreate(owner: owner,
key: (valueProvider, refresh),
factory: static (o, args) => S(o, new AsyncFeed<T>(args.valueProvider.SomeOrNoneWhenNotNull(), args.refresh)));
/// <summary>
/// Gets or creates a state from an async method.

Просмотреть файл

@ -14,10 +14,14 @@ internal sealed class StateForEach<T> : IDisposable
private readonly CancellationTokenSource _ct = new();
private readonly AsyncAction<Option<T>> _action;
private readonly string _name;
#pragma warning disable IDE0052 // Remove unread private members
private readonly Task _task; // Holds ref on the enumeration task. This is also accessed by reflection in tests!
#pragma warning restore IDE0052 // Remove unread private members
public StateForEach(ISignal<Message<T>> state, AsyncAction<Option<T>> action, string name = "-unnamed-")
{
ArgumentNullException.ThrowIfNull(state);
if (state is not IStateImpl impl)
{
throw new InvalidOperationException("Execute is supported only on internal state implementation.");