From 49ccaf6fb4851d8f7967ad62d08e0df2112926f8 Mon Sep 17 00:00:00 2001 From: Kunal22shah Date: Fri, 30 Aug 2024 13:54:34 -0400 Subject: [PATCH 01/17] docs: refactoring listfeed --- doc/Learn/Mvux/ListFeeds.md | 102 ----------------------------- doc/Reference/Reactive/listfeed.md | 91 ++++++++++++++++++------- 2 files changed, 67 insertions(+), 126 deletions(-) delete mode 100644 doc/Learn/Mvux/ListFeeds.md diff --git a/doc/Learn/Mvux/ListFeeds.md b/doc/Learn/Mvux/ListFeeds.md deleted file mode 100644 index 963e3df655..0000000000 --- a/doc/Learn/Mvux/ListFeeds.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -uid: Uno.Extensions.Mvux.ListFeeds ---- - -# What are list-feeds? - -A `ListFeed` (`IListFeed`) is like a [Feed](xref:Uno.Extensions.Mvux.Feeds) which is stateless and keeps no track of changes but is specialized for handling collections. - -Unlike a feed, where each asynchronous operation returns one single item (or a series of single items in the case of an `IAsyncEnumerable`), a `ListFeed` returns a collection of items. - -A couple of points to note about list-feeds: - -- [Operators](#operators) are applied to the items within the returned collection, rather than on the entire collection. - -- When an empty collection is returned by the service, it's treated as an Empty message. The returned data axis Option will be `None`, even though the result was not `null`. This is because when a control of data items is displayed with an empty collection (for instance a `ListView`), there is no reason to display the `FeedView`'s `ValueTemplate` with an empty `ListView`. The "No data records" `NoneTemplate` makes much more sense in this case. For that reason, both a `null` result and an empty collection are regarded as `None`. - -- The `ListFeed` uses the _key equality_ to track multiple versions of the same entity within different messages of the `ListFeed`. -[Read more about _key equality_](xref:Uno.Extensions.KeyEquality.Concept). - -## How to create a list feed - -To create an `IListFeed`, use the static class `ListFeed` to call one of the same `Async`, `AsyncEnumerable`, and `Create` methods as in `Feed`. The only difference is that it expects the Task or `IAsyncEnumerable` to return an `IImmutableList` instead of `T`. - -A `ListFeed` can be created in several ways. - -### Async - -Using the Async factory method, the service returns a list of names on load/refresh - using a pull technique. The `ListFeed` is then created with the async call. - -- Service code: - -```csharp -public ValueTask> GetNames(CancellationToken ct = default); -``` - -- Model code: - -```csharp -public IListFeed Names => ListFeed.Async(service.GetNames); -``` - -### AsyncEnumerable - -In this way, a `ListFeed` is created with an Async Enumerable method that returns an `IImmutableList` of names when available - using the push technique. - -- Service code: - -```csharp -public IAsyncEnumerable> GetNames( - [EnumeratorCancellation] CancellationToken ct = default); -``` - -- Model code: - -```csharp -public IListFeed Names => ListFeed.AsyncEnumerable(service.GetNames); -``` - -Pull and push are explained more in the [feeds page](xref:Uno.Extensions.Mvux.Feeds#creation-of-feeds). - -### Convert from `Feed` of an item-collection - -A `ListFeed` can also be created from a `Feed` when the `Feed` exposes a collection (`IFeed>`): - -```csharp -public void SetUp() -{ - IFeed> stringsFeed = ...; - IListFeed stringsListFeed = stringsFeed.AsListFeed(); -} -``` - -## Support for selection and pagination - -MVUX also provides built-in support for Selection and Pagination. -See more information on [Selection](xref:Uno.Extensions.Mvux.Advanced.Selection) or [Pagination](xref:Uno.Extensions.Mvux.Advanced.Pagination). - -## Operators - -As mentioned, unlike a `Feed>`, operators on a `ListFeed` are directly interacting with the collection's items instead of the list itself. - -### Where - -This operator allows the filtering of the items. - -```csharp -public IListFeed LongNames => Names.Where(name => name.Length >= 10); -``` - -If all items of the collection are filtered out, the resulting `Feed` will go into `None` state. - -### AsFeed - -This operator enables converting an `IListFeed` to `IFeed>`: - -```csharp -public void SetUp() -{ - IListFeed stringsListFeed = ...; - IFeed> stringsFeed = stringsListFeed.AsFeed(); -} -``` diff --git a/doc/Reference/Reactive/listfeed.md b/doc/Reference/Reactive/listfeed.md index 5654647af2..23be25d5b5 100644 --- a/doc/Reference/Reactive/listfeed.md +++ b/doc/Reference/Reactive/listfeed.md @@ -4,19 +4,62 @@ uid: Uno.Extensions.Reactive.ListFeed # ListFeed The `IListFeed` is _feed_ specialized for handling collections. -It allows the declaration of an operator directly on items instead of dealing with the list itself. +It allows the declaration of an operator directly on items instead of dealing with the list itself. Unlike a feed, where each asynchronous operation returns one single item (or a series of single items in the case of an `IAsyncEnumerable`), a `ListFeed` returns a collection of items. A _list feed_ goes in `None` if the list does not have any elements. > [!NOTE] > `ListFeed` is using the _key equality_ to track multiple version of a same entity within different messages of the `ListFeed`. > [Read more about _key equality_.](../KeyEquality/concept.md). +A couple of points to note about list-feeds: + +- [Operators](#operators) are applied to the items within the returned collection, rather than on the entire collection. + +- When an empty collection is returned by the service, it's treated as an Empty message. The returned data axis Option will be `None`, even though the result was not `null`. This is because when a control of data items is displayed with an empty collection (for instance a `ListView`), there is no reason to display the `FeedView`'s `ValueTemplate` with an empty `ListView`. The "No data records" `NoneTemplate` makes much more sense in this case. For that reason, both a `null` result and an empty collection are regarded as `None`. + +- The `ListFeed` uses the _key equality_ to track multiple versions of the same entity within different messages of the `ListFeed`. +[Read more about _key equality_](xref:Uno.Extensions.KeyEquality.Concept). + ## Sources: How to create a list feed -To create an `IListFeed`, on the `ListFeed` class, the same `Async`, `AsyncEnumerable` and `Create` methods found on `Feed` can be used. +To create an `IListFeed`, use the static class `ListFeed` to call one of the following methods: + + **Async**: Creates a `ListFeed` using a method that returns a `Task>`. + +- Service code: + +```csharp +public ValueTask> GetNames(CancellationToken ct = default); +``` + +- Model code: + +```csharp +public IListFeed Names => ListFeed.Async(service.GetNames); +``` +**AsyncEnumerable**: Creates a `ListFeed` using an `IAsyncEnumerable>`. +- Service code: + +```csharp +public IAsyncEnumerable> GetNames( + [EnumeratorCancellation] CancellationToken ct = default); +``` + +- Model code: + +```csharp +public IListFeed Names => ListFeed.AsyncEnumerable(service.GetNames); +``` +Pull and push are explained more in the [feeds page](xref:Uno.Extensions.Mvux.Feeds#creation-of-feeds). + +**Create**: Provides custom initialization for a `ListFeed`. There are also 2 helpers that allow you to convert from/to a _feed_ to/from a _list feed_. +## Operators: How to interact with a list feed + +Unlike a `Feed>` operators on a _list feed_ are directly interacting with _items_ instead of the list itself. + ### PaginatedAsync This allows the creation of a feed of a paginated list. @@ -36,19 +79,15 @@ public IListFeed Cities => ListFeed.AsyncPaginated(async (page, ct) => _se > For such patterns, you can either just hard-code your _page size_ (e.g. `source.Skip(page.Index * 20).Take(20)`, > either use the `page.TotalCount` property (e.g. `source.Skip(page.TotalCount).Take(page.DesiredSize)`). -### AsListFeed +### Where -This allows the creation of a _list feed_ from a _feed of list_. +This operator allows the filtering of _items_. + +> [!WARNING] +> If all _items_ of the collection are filtered out, the resulting feed will go in _none_ state. ```csharp -public IListFeed Forecast => Feed - .Async(async ct => new [] - { - await _weatherService.GetWeatherForecast(DateTime.Today.AddDays(1), ct), - await _weatherService.GetWeatherForecast(DateTime.Today.AddDays(2), ct), - }) - .Select(list => list.ToImmutableList()) - .AsListFeed(); +public IListFeed LongNames => Names.Where(name => name.Length >= 10); ``` ### AsFeed @@ -56,20 +95,24 @@ public IListFeed Forecast => Feed This does the opposite of `AsListFeed` and converts a _list feed_ to a _feed of list_. ```csharp -public IFeed> ForecastFeed => Forecast.AsFeed(); +public void SetUp() +{ + IListFeed stringsListFeed = ...; + IFeed> stringsFeed = stringsListFeed.AsFeed(); +} ``` -## Operators: How to interact with a list feed - -Unlike a `Feed>` operators on a _list feed_ are directly interacting with _items_ instead of the list itself. - -### Where - -This operator allows the filtering of _items_. +### AsListFeed -> [!WARNING] -> If all _items_ of the collection are filtered out, the resulting feed will go in _none_ state. +A `ListFeed` can also be created from a `Feed` when the `Feed` exposes a collection (`IFeed>`): ```csharp -public IListFeed HighTempDays => ForecastFeed.Where(weather => weather.Temperature >= 28); -``` +public IListFeed Forecast => Feed + .Async(async ct => new [] + { + await _weatherService.GetWeatherForecast(DateTime.Today.AddDays(1), ct), + await _weatherService.GetWeatherForecast(DateTime.Today.AddDays(2), ct), + }) + .Select(list => list.ToImmutableList()) + .AsListFeed(); +``` \ No newline at end of file From 387081e577cff3997e7557967998faed9b8b638b Mon Sep 17 00:00:00 2001 From: Kunal22shah Date: Fri, 30 Aug 2024 15:44:46 -0400 Subject: [PATCH 02/17] docs: refactoring States.md --- doc/Learn/Mvux/States.md | 243 -------------------------------- doc/Reference/Reactive/state.md | 172 +++++++++++++++++++++- 2 files changed, 170 insertions(+), 245 deletions(-) delete mode 100644 doc/Learn/Mvux/States.md diff --git a/doc/Learn/Mvux/States.md b/doc/Learn/Mvux/States.md deleted file mode 100644 index c55c39b401..0000000000 --- a/doc/Learn/Mvux/States.md +++ /dev/null @@ -1,243 +0,0 @@ ---- -uid: Uno.Extensions.Mvux.States ---- - -# What are states - -## States are stateful feeds - -Like [feeds](xref:Uno.Extensions.Mvux.Feeds), states are used to manage asynchronous operations and wrap them in metadata that provides information about the current state of the operation, such as whether the operation is still in progress, when an error occurs, or if the result has no data. - -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 ViewModel for each Model, which holds the state information of the data, as well as a ViewModel for entities where needed, for instance, if the entities are immutable (e.g. records - the recommended type). -The ViewModels 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). - -Like a feed, states can be reloaded, which will invoke the asynchronous operation that is used to create the state. - -States and Feeds are different in the following: - -1. When subscribing to a state, the currently loaded value is going to be replayed. -2. A state provides the `Update` method that allows changing its current value. -3. States are attached to an owner and share the same lifetime as that owner. -4. The main usage of a state is for two-way bindings. - -## States are attached to their owner - -Besides holding the state information, a reference to the Model is shared with the states so that when the View is closed and disposed of, it tunnels down to the states and the Models and makes them available for garbage collection. States share the same lifetime as their owner. - -## How to use states - -### Creation of states - -#### From Tasks - -States are created slightly differently, they require a reference to the Model for caching and GC as mentioned above: - -```csharp -public record MainModel { - public IState MainContact => State.Async(this, ContactsService.GetMainContact); -} -``` - -Where `GetMainContact` is a `ValueTask`, and takes a parameter of `CancellationToken`. The `this` parameter is the owner of the state, which is the Model in this case. - -#### From Async-Enumerables - -A State can also be created from an Async Enumerable as follows: - -```csharp -public IState MyStockCurrentValue => State.AsyncEnumerable(this, ContactsService.GetMyStockCurrentValue); -``` - -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 - -You can setup a state without any values, values can always be set using the [`Set`](#set) or the [`Update`](#update) methods. - -```csharp -public IState CurrentCity => State.Empty(this); -``` - -#### Create a state with an initial value - -You can setup a state with a synchronous initial value. The state can be set later on using the [`Update`](#update) or the [`Set`](#set) methods. - -```csharp -public IState CurrentCity => State.Value(this, () => new City("Montreal")); -``` - -#### From a feed - -A state can easily be converted from a feed as follows: - -```csharp -public IFeed MyFeed => ... -public IState 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. -> This is intended for advanced users and is explained [here](xref:Uno.Extensions.Reactive.State#create). - -### Usage of States - -States are advanced Feeds. As such, they can also be awaited directly: - -```csharp -City currentCity = await this.CurrentCity; -``` - -#### How to bind the View to a State - -States are built to be cooperating with the data-binding engine. A State will automatically update its value when the user changes data in the View bound to this State. - -1. In an MVUX app (read [How to set up an MVUX project](xref:Uno.Extensions.Mvux.HowToMvuxProject)), add a Model class with a State as follows: - - ```csharp - public partial record SliderModel - { - // create a state with an initial random double value between 0 and 1, multiplied by 100. - public IState SliderValue => State.Value(this, () => Random.Shared.NextDouble() * 100); - } - ``` - -1. Replace all child elements in the _MainPage.xaml_ with the following: - - ```xml - - - - - - - - - - - - - - - - - - ``` - -In this scenario, the `DataContext` is set to an instance of the `SliderViewModel` class, which is the generated ViewModel for the `SliderModel` record. - -1. When you run the app, moving the `Slider` instantly affects the upper `TextBox`; the `Silder.Value` property has a two-way binding with the `SliderValue` State, so any change to the Slider immediately updates the State value, which in turn affects the data-bound `TextBlock` on top: - - ![A video of the previous slider app in action](Assets/SliderApp-1.gif) - -### Change data of a state - -#### Update - -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): - -```csharp -public async ValueTask IncrementSlider(CancellationToken ct = default) -{ - static double incrementValue(double currentValue) => - currentValue <= 99 - ? currentValue + 1 - : 1; - - await SliderValue.UpdateAsync(updater: incrementValue, ct); -} -``` - -The `updater` parameter of the `Update` method accepts a `Func`. The input parameter provides the current value of the State when called. The return value is the new value that will be applied as the new value of the State, in our case we use the `incrementValue` [local function](https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/local-functions) to increment `currentValue` by one (or return `1` if the value exceeds `100`). - -#### Set - -There are additional methods that update the data of a State such as `Set` and `UpdateMessage`, explained [here](xref:Uno.Extensions.Reactive.State#update-how-to-update-a-state). The `Set` method is the same as the `Update`, except that in `Set` there is no callback that provides the current value, instead a new value is provided directly and the old value is discarded: - -```csharp -public async ValueTask SetSliderMiddle(CancellationToken ct = default) -{ - await SliderValue.SetAsync(50, ct); -} -``` - -### Subscribing to changes - -The `ForEach` enables executing a callback each time the value of the `IState` 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. - -For example: - -```csharp -public partial record Model -{ - public IState MyState => ... - - public async ValueTask EnableChangeTracking() - { - MyState.ForEach(PerformAction); - } - - public async ValueTask PerformAction(string item, CancellationToken ct) - { - ... - } -} -``` - -Additionally, the `ForEach` method can be set using the Fluent API: - -```csharp -public partial record Model -{ - public IState 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. -In the `IncrementSlider` example [we've just used](#change-data-of-a-state), a special asynchronous Command will be generated that can be used in the View by a `Button` or other controls: - -Let's modify the XAML [above](#how-to-bind-the-view-to-a-state) with the following: - -```xml - ... - - - -