Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement FluentAPI #2520

Merged
merged 13 commits into from
Aug 27, 2024
36 changes: 18 additions & 18 deletions doc/Learn/Mvux/Advanced/Selection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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);
}

...
Expand All @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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.
10 changes: 5 additions & 5 deletions doc/Learn/Mvux/ListStates.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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));

...

Expand All @@ -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.
Expand Down Expand Up @@ -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).
40 changes: 28 additions & 12 deletions doc/Learn/Mvux/States.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -111,23 +111,23 @@ 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"
xmlns:local="using:SliderApp">
<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>
Expand All @@ -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):

Expand All @@ -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.

Expand All @@ -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)
Expand All @@ -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.
Expand Down
40 changes: 40 additions & 0 deletions src/Uno.Extensions.Reactive.Messaging/ListStateExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
5 changes: 4 additions & 1 deletion src/Uno.Extensions.Reactive.Messaging/MessengerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading