Merge pull request #2528 from unoplatform/docs/update.bindable.proxy.to.vm

docs: update mvux bindable proxy generated name
This commit is contained in:
Andres Pineda 2024-08-27 15:49:58 -04:00 коммит произвёл GitHub
Родитель 1e08674c6a 498d7a7851
Коммит 9079fb1e55
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
16 изменённых файлов: 167 добавлений и 158 удалений

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

@ -84,12 +84,12 @@ Change the *MainPage* to have a different content as the sample bellow.
And now we need to add the DataContext to the Page.
> Notice that we are using the class BindableWeatherModel auto generated by the MVUX.
> Notice that we are using the class WeatherViewModel auto generated by the MVUX.
```csharp
this.DataContext = new BindableWeatherModel(new WeatherService());
this.DataContext = new WeatherViewModel(new WeatherService());
this.DataContext<BindableWeatherModel>((page, vm) => page
this.DataContext<WeatherViewModel>((page, vm) => page
);
```
@ -100,12 +100,12 @@ Change the *MainPage* to have a different content as the sample bellow.
new FeedView()
.Source(() => vm.CurrentWeather)
//.Source(x => x.Bind(() => vm.CurrentWeather))
//You can use the Function
.DataTemplate((sample) => GetDataTemplate())
//Or you can use direct the Element
.ProgressTemplate<StackPanel>((sample) =>
.ProgressTemplate<StackPanel>((sample) =>
new StackPanel().Children(
new TextBlock().Text("Loading...")
)
@ -141,7 +141,7 @@ Change the *MainPage* to have a different content as the sample bellow.
{
this.InitializeComponent();
this.DataContext = new BindableWeatherModel(new WeatherService());
this.DataContext = new WeatherViewModel(new WeatherService());
}
```
@ -160,9 +160,9 @@ Change the *MainPage* to have a different content as the sample bellow.
public MainPage()
{
this.DataContext = new BindableWeatherModel(new WeatherService());
this.DataContext = new WeatherViewModel(new WeatherService());
this.DataContext<BindableWeatherModel>((page, vm) => page
this.DataContext<WeatherViewModel>((page, vm) => page
.Background(ThemeResource.Get<Brush>("ApplicationPageBackgroundThemeBrush"))
.Content(
new StackPanel()
@ -174,12 +174,12 @@ Change the *MainPage* to have a different content as the sample bellow.
new FeedView()
.Source(() => vm.CurrentWeather)
.Source(x => x.Bind(() => vm.CurrentWeather))
//You can use the Function
.DataTemplate((sample) => GetDataTemplate())
//Or you can use direct the Element
.ProgressTemplate<StackPanel>((sample) =>
.ProgressTemplate<StackPanel>((sample) =>
new StackPanel().Children(
new TextBlock().Text("Loading...")
)

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

@ -6,57 +6,66 @@ uid: Uno.Extensions.Mvux.Advanced.Commands
This page covers the following topics:
- [**What are commands**](#what-are-commands)
- [**Implicit command generation**](#implicit-command-generation)
- [**Basic Commands**](#basic-commands)
- [**Using the CommandParameter**](#using-the-commandparameter)
- [**Additional Feed parameters**](#additional-feed-parameters)
- [**Command generation rules**](#command-generation-rules)
- [**Configuring command generation using attributes**](#configuring-command-generation-using-attributes)
- [**Using XAML behaviors to execute a command when an event is raised**](#using-xaml-behaviors-to-execute-a-command-when-an-event-is-raised)
- [**Explicit command creation**](#explicit-command-creation)
- [Commands](#commands)
- [What are commands](#what-are-commands)
- [Command generation](#command-generation)
- [Implicit command generation](#implicit-command-generation)
- [Basic commands](#basic-commands)
- [Using the CommandParameter](#using-the-commandparameter)
- [Additional Feed parameters](#additional-feed-parameters)
- [Command generation rules](#command-generation-rules)
- [Configuring command generation using attributes](#configuring-command-generation-using-attributes)
- [ImplicitCommands attribute](#implicitcommands-attribute)
- [Command attribute](#command-attribute)
- [ImplicitFeedCommandParameter attribute](#implicitfeedcommandparameter-attribute)
- [FeedParameter attribute](#feedparameter-attribute)
- [Using XAML behaviors to execute a command when an event is raised](#using-xaml-behaviors-to-execute-a-command-when-an-event-is-raised)
- [Explicit command creation](#explicit-command-creation)
- [Command.Async factory method](#commandasync-factory-method)
- [Create \& Create\<T\>](#create--createt)
- [Example](#example)
## What are commands
Commands provide a way to expose code within an application that performs an action (typically a method) so that it can be invoked by a user interaction with a UI element, such as clicking a `Button`.
Commands provide a way to expose code within an application that performs an action (typically a method) so that it can be invoked by a user interaction with a UI element, such as clicking a `Button`.
For example the `MainModel` includes a public `Save` method:
```csharp
public partial record MainModel()
{
{
public void Save() { ... }
}
```
The `Save` method will be exposed as an `IAsyncCommand` (an MVUX interface that extends the `ICommand` interface) on the generated bindable proxy for the `MainModel`:
The `Save` method will be exposed as an `IAsyncCommand` (an MVUX interface that extends the `ICommand` interface) on the generated ViewModel for the `MainModel`:
```csharp
public partial class BindableMainModel
public partial class MainViewModel
{
public IAsyncCommand Save { get; }
}
}
```
The `Command` property on a `Button` can be bound to the `Save` command on the `BindableMainModel`. When the `Button` is clicked, the `Save` command will be executed, which will invoke the `Save` method on `MainModel`.
The `Command` property on a `Button` can be bound to the `Save` command on the `MainViewModel`. When the `Button` is clicked, the `Save` command will be executed, which will invoke the `Save` method on `MainModel`.
```xml
<Button Command="{Binding Save}">Save</Button>
```
During the execution of the `Save` method, the `Button` will automatically be disabled, making it clear that the method is running.
During the execution of the `Save` method, the `Button` will automatically be disabled, making it clear that the method is running.
## Command generation
By default, MVUX will generate a command in the bindable proxy for each public method in a Model via [**Implicit command generation**](#implicit-command-generation). This behavior can be customized using attributes, or alternatively, can be disabled in favor of [**Explicit command creation**](#explicit-command-creation).
By default, MVUX will generate a command in the ViewModel for each public method in a Model via [**Implicit command generation**](#implicit-command-generation). This behavior can be customized using attributes, or alternatively, can be disabled in favor of [**Explicit command creation**](#explicit-command-creation).
### Implicit command generation
#### Basic commands
By default, a command property will be generated in the bindable proxy for each method on the Model that has no return value or is asynchronous (e.g. returns `ValueTask` or `Task`).
By default, a command property will be generated in the ViewModel for each method on the Model that has no return value or is asynchronous (e.g. returns `ValueTask` or `Task`).
The asynchronous method on the Model may take a single `CancellationToken` parameter, which will be cancelled if the bindable proxy is disposed of whilst commands are running. Although a `CancellationToken` parameter is not mandatory, it's a good practice to add one, as it enables the cancellation of the asynchronous operation.
The asynchronous method on the Model may take a single `CancellationToken` parameter, which will be cancelled if the ViewModel is disposed of whilst commands are running. Although a `CancellationToken` parameter is not mandatory, it's a good practice to add one, as it enables the cancellation of the asynchronous operation.
For example, if the Model contains a method in any of the following signatures:
@ -69,7 +78,7 @@ For example, if the Model contains a method in any of the following signatures:
1. A method returning `ValueTask`, with a `CancellationToken` parameter:
```csharp
public ValueTask DoWork(CancellationToken ct);
public ValueTask DoWork(CancellationToken ct);
```
1. A method returning `ValueTask`, without a `CancellationToken` parameter:
@ -78,7 +87,7 @@ For example, if the Model contains a method in any of the following signatures:
public ValueTask DoWork();
```
a `DoWork` command will be generated in the bindable proxy:
a `DoWork` command will be generated in the ViewModel:
```csharp
public IAsyncCommand DoWork { get; }
@ -91,11 +100,11 @@ In some scenarios, you may need to use the method only, without a command genera
public ValueTask DoWork();
```
When command generation is switched off, the methods under the scope which has been switched off will be generated in the bindable proxy as regular methods rather than as commands, meaning that they are still available to be data-bound or invoked via the bindable proxy.
When command generation is switched off, the methods under the scope which has been switched off will be generated in the ViewModel as regular methods rather than as commands, meaning that they are still available to be data-bound or invoked via the ViewModel.
#### Using the CommandParameter
An additional parameter can be added to the method on the Model, which is then assigned with the value of the `CommandParameter` received from the View. For example, when a `Button` is clicked, the [`Button.CommandParameter`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.primitives.buttonbase.commandparameter) value will be passed to the command.
An additional parameter can be added to the method on the Model, which is then assigned with the value of the `CommandParameter` received from the View. For example, when a `Button` is clicked, the [`Button.CommandParameter`](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.primitives.buttonbase.commandparameter) value will be passed to the command.
The `CommandParameter` value is first passed to the `CanExecute` method on the command to determine if the command can be executed. The command checks both that the `CommandParameter` value can be cast to the correct type, and that there's not already an invocation of the command for that `CommandParameter` value. Assuming `CanExecute` returns true, when the `Button` is clicked the `Execute` method on the command is invoked which routes the call, including the `CommandParameter` value (correctly cast to the appropriate type), to the method on the Model.
In this example the Model defines a method, `DoWork`, that accepts a parameter, `param`:
@ -104,7 +113,7 @@ In this example the Model defines a method, `DoWork`, that accepts a parameter,
public void DoWork(double param) { ... }
```
The corresponding command in the bindable proxy looks the same as before. However, the implementation, which you can inspect in the generated code, includes logic to validate the type of the `CommandParameter`, and subsequently passes the cast value to the `DoWork` method on the Model:
The corresponding command in the ViewModel looks the same as before. However, the implementation, which you can inspect in the generated code, includes logic to validate the type of the `CommandParameter`, and subsequently passes the cast value to the `DoWork` method on the Model:
```csharp
public IAsyncCommand DoWork { get; }
@ -117,7 +126,7 @@ The command can be consumed in the View by setting the `CommandParameter` on the
<Button Command="{Binding DoWork}" CommandParameter="{Binding Value, ElementName=slider}"/>
```
If the `CommandParameter` is null, or if its type doesn't match the parameter type of the method, the button will remain disabled.
If the `CommandParameter` is null, or if its type doesn't match the parameter type of the method, the button will remain disabled.
On the other hand, in case the `CommandParameter` is specified in the View but the method in the Model doesn't have a parameter, the View's `CommandParameter` value will just be disregarded.
It's also still recommended to include a `CancellationToken`, which will allow the method to be cancelled. For example, the preferred definition for the `DoWork` method would be asynchronous and include the `CancellationToken` parameter.
@ -139,7 +148,7 @@ public async ValueTask DoWork()
}
```
However, MVUX commands also enable consuming the current value of Feed properties in the Model, using parameter names in the Model method, with a name and type matching the Feed property.
However, MVUX commands also enable consuming the current value of Feed properties in the Model, using parameter names in the Model method, with a name and type matching the Feed property.
The name matching is NOT case-sensitive.
For example:
@ -177,11 +186,11 @@ Here is a recap of the rules the Model method must comply with for an `IAsyncCom
#### ImplicitCommands attribute
By default, implicit command generation is enabled when the MVUX package is referenced. That means that any method in the Model that matches the [command generation rules](#command-generation-rules) will have an accompanying command wrapper generated for it.
By default, implicit command generation is enabled when the MVUX package is referenced. That means that any method in the Model that matches the [command generation rules](#command-generation-rules) will have an accompanying command wrapper generated for it.
However, you may choose to enable, or disable, implicit command generation for a specific class, or assembly. Conversely, when implicit command generation has been disabled for an assembly, it can be enabled for specific classes.
Enabling, or disabling, implicit command generation can be achieved using the [`ImplicitCommands`](https://github.com/unoplatform/uno.extensions/blob/main/src/Uno.Extensions.Reactive/Config/ImplicitCommandsAttribute.cs) attribute.
Enabling, or disabling, implicit command generation can be achieved using the [`ImplicitCommands`](https://github.com/unoplatform/uno.extensions/blob/main/src/Uno.Extensions.Reactive/Config/ImplicitCommandsAttribute.cs) attribute.
Here is an example of disabling implicit command generation throughout an entire assembly:
@ -198,7 +207,7 @@ public partial record MyModel(...)
#### Command attribute
In addition to the [`ImplicitCommands`](https://github.com/unoplatform/uno.extensions/blob/main/src/Uno.Extensions.Reactive/Config/ImplicitCommandsAttribute.cs) attribute which controls implicit command generation of a class or assembly, you can explicitly enable or disable the command generation for an individual method using the [`Command`](https://github.com/unoplatform/uno.extensions/blob/main/src/Uno.Extensions.Reactive/Presentation/Commands/CommandAttribute.cs) attribute. When command generation is disabled for a method, that method will still be generated in the bindable proxy for the Model as a pass-through to the original method on the Model.
In addition to the [`ImplicitCommands`](https://github.com/unoplatform/uno.extensions/blob/main/src/Uno.Extensions.Reactive/Config/ImplicitCommandsAttribute.cs) attribute which controls implicit command generation of a class or assembly, you can explicitly enable or disable the command generation for an individual method using the [`Command`](https://github.com/unoplatform/uno.extensions/blob/main/src/Uno.Extensions.Reactive/Presentation/Commands/CommandAttribute.cs) attribute. When command generation is disabled for a method, that method will still be generated in the ViewModel for the Model as a pass-through to the original method on the Model.
Assuming the `ImplicitCommands` attribute is used to disable implicit command generation for an assembly, or class, a command can be generated for the method by decorating it with the `Command` attribute (with its default value `true`):
@ -211,7 +220,7 @@ public async ValueTask DoWork()
}
```
Or on the contrary, if implicit command generation is enabled for an assembly or class, using the `Command` attribute will prevent the generation of a command. Instead, a regular method, in this case called `DoWork` will be created on the bindable proxy.
Or on the contrary, if implicit command generation is enabled for an assembly or class, using the `Command` attribute will prevent the generation of a command. Instead, a regular method, in this case called `DoWork` will be created on the ViewModel.
```csharp
[assembly:ImplicitCommands(true)]
@ -224,16 +233,16 @@ public async ValueTask DoWork()
The `Command` attribute has precedence over the `ImplicitCommands` attribute. If a method is decorated with this attribute, whether or not a command is generated will depend solely on the `Command` attribute value.
One example of when you'd want to switch off command generation is if you are using [x:Bind Event Binding](https://learn.microsoft.com/windows/uwp/xaml-platform/x-bind-markup-extension#event-binding), you will want to opt-out from command generation for the bound method so that you can bind directly to the method on the bindable proxy from the View.
One example of when you'd want to switch off command generation is if you are using [x:Bind Event Binding](https://learn.microsoft.com/windows/uwp/xaml-platform/x-bind-markup-extension#event-binding), you will want to opt-out from command generation for the bound method so that you can bind directly to the method on the ViewModel from the View.
In this example, a Save method with the same signature as the Save method on the Model will be created on the bindable proxy.
In this example, a Save method with the same signature as the Save method on the Model will be created on the ViewModel.
```csharp
[Command(false)]
public async ValueTask Save () { ... }
```
The `Click` event on the `Button` can then be bound using x:Bind to the Save method on the bindable proxy.
The `Click` event on the `Button` can then be bound using x:Bind to the Save method on the ViewModel.
```xml
<Button Click="{x:Bind Save}">Save</Button>
@ -241,11 +250,11 @@ The `Click` event on the `Button` can then be bound using x:Bind to the Save met
#### ImplicitFeedCommandParameter attribute
You can opt-in or opt-out of implicit matching of Feeds and command parameters by decorating the current assembly or class with the [`ImplicitFeedCommandParameters`](https://github.com/unoplatform/uno.extensions/blob/main/src/Uno.Extensions.Reactive/Config/ImplicitFeedCommandParametersAttribute.cs) attribute:
You can opt-in or opt-out of implicit matching of Feeds and command parameters by decorating the current assembly or class with the [`ImplicitFeedCommandParameters`](https://github.com/unoplatform/uno.extensions/blob/main/src/Uno.Extensions.Reactive/Config/ImplicitFeedCommandParametersAttribute.cs) attribute:
```csharp
[assembly:ImplicitFeedCommandParameter(false)]
[ImplicitFeedCommandParameter(true)]
public partial record MyModel
```
@ -256,7 +265,7 @@ You can also explicitly match a parameter with a Feed even if the names don't ma
```csharp
public IFeed<string> Message { get; }
public async ValueTask Share([FeedParameter(nameof(Message))] string msg) { ... }
```
@ -278,7 +287,7 @@ The `TextBlockDoubleTapped` method will be generated as a command, which you can
```xml
<Page
...
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:interactions="using:Microsoft.Xaml.Interactions.Core">
<TextBlock x:Name="textBlock" Text="Double-tap me">
@ -293,12 +302,12 @@ The `TextBlockDoubleTapped` method will be generated as a command, which you can
</Page>
```
When the `TextBlock` is double-tapped (or double-clicked), the `TextBlockDoubleTapped` command which is generated in the bindable proxy will be executed, and in turn, the `TextBlockDoubleTapped` method in the Model will be invoked. The text 'Double-tap me' will be passed in as the command parameter.
When the `TextBlock` is double-tapped (or double-clicked), the `TextBlockDoubleTapped` command which is generated in the ViewModel will be executed, and in turn, the `TextBlockDoubleTapped` method in the Model will be invoked. The text 'Double-tap me' will be passed in as the command parameter.
### Explicit command creation
Adding commands via code generation is sufficient enough to cover most scenarios. However, sometimes you may need to have more control over the command creation, which is where explicit command creation is useful.
Adding commands via code generation is sufficient enough to cover most scenarios. However, sometimes you may need to have more control over the command creation, which is where explicit command creation is useful.
Commands can be created manually using the static class [`Command`](https://github.com/unoplatform/uno.extensions/blob/main/src/Uno.Extensions.Reactive/Presentation/Commands/Command.cs), which provides factory methods for creating commands.
#### Command.Async factory method
@ -319,7 +328,7 @@ The `Command.Async` method will create a command that when executed will run the
#### Create & Create\<T>
To create a command you can use the API provided in the `Command.Create` factory methods. The `Command.Create` provides an `ICommandBuilder` parameter which you can use to configure the command in a fluent-API fashion.
To create a command you can use the API provided in the `Command.Create` factory methods. The `Command.Create` provides an `ICommandBuilder` parameter which you can use to configure the command in a fluent-API fashion.
This API is intended for Uno Platform's internal use but can come in handy if you need to create custom commands.
`ICommandBuilder` provides the three methods below.
@ -327,17 +336,17 @@ This API is intended for Uno Platform's internal use but can come in handy if yo
- ##### Given
This method initializes a command from a Feed (or a State). The command will be triggered whenever a new value is available to the Feed. It takes a single `IFeed<T>` parameter.
```csharp
public IFeed<int> PageCount => ...
public IAsyncCommand MyCommand => Command.Create(builder => builder.Given(PageCount));
```
- ##### When
Defines the 'can execute' of the command. It accepts a predicate of `T`, where `T` is the type the command has been created with. When this is configured, the command will be executed only if the condition is true.
```csharp
public IAsyncCommand MyCommand => Command.Create<int>(builder => builder.When(i => i > 10));
```
@ -364,8 +373,8 @@ This API is intended for Uno Platform's internal use but can come in handy if yo
Here's a complete example where the MyCommand is defined in the Model.
```csharp
public IAsyncCommand MyCommand =>
Command.Create(builder =>
public IAsyncCommand MyCommand =>
Command.Create(builder =>
builder
.Given(CurrentPage)
.When(currentPage => currentPage > 0)
@ -376,7 +385,7 @@ public IFeed<int> CurentPage => ...
public ValueTask NavigateToPage(int currentPage, CancellationToken ct) { ... }
```
As with implicitly created commands, the `Command` property on UI controls, such as a `Button`, can be bound to the command, `MyCommand`. However, since commands created using the fluent API are not replicated on the bindable proxy, the binding expression has to include the bindable proxy's `Model` property to access the Model instance.
As with implicitly created commands, the `Command` property on UI controls, such as a `Button`, can be bound to the command, `MyCommand`. However, since commands created using the fluent API are not replicated on the ViewModel, the binding expression has to include the ViewModel's `Model` property to access the Model instance.
```xml
<Button Command="{Binding Model.MyCommand}" Content="Execute my command" />

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

@ -4,8 +4,8 @@ uid: Uno.Extensions.Mvux.Advanced.InspectGeneratedCode
# Inspecting the generated code
MVUX makes extensive use of code generation. It's used to generate bindable proxies for models, entities, and commands.
Inspecting the generated code can give you a lot of insight into how the bindable proxies work under the hood as well as how MVUX operates behind the scenes.
MVUX makes extensive use of code generation. It's used to generate ViewModels for models, entities, and commands.
Inspecting the generated code can give you a lot of insight into how the ViewModels work under the hood as well as how MVUX operates behind the scenes.
Using Visual Studio, viewing the generated code can be achieved in several ways:
@ -13,7 +13,7 @@ Using Visual Studio, viewing the generated code can be achieved in several ways:
![Demonstration of hitting F12 in Visual Studio to see generated code](../Assets/InspectingGeneratedCode-1.gif)
1. Hitting <kbd>Ctrl</kbd>+<kbd>T</kbd> and typing in the Bindable type name:
1. Hitting <kbd>Ctrl</kbd>+<kbd>T</kbd> and typing in the ViewModel type name:
![Demonstration of hitting Control T in Visual Studio to see generated code](../Assets/InspectingGeneratedCode-2.gif)
@ -24,6 +24,6 @@ Using Visual Studio, viewing the generated code can be achieved in several ways:
1. Expand the project's *Dependencies* object.
2. Expand the current target platform (e.g. *net8.0windows10.0...*).
3. Expand the *Analyzers* sub menu and then *Uno.Extensions.Reactive.Generator*.
4. Under *Uno.Extensions.Reactive.Generator.FeedsGenerator* you'll find the generated bindable proxy types.
4. Under *Uno.Extensions.Reactive.Generator.FeedsGenerator* you'll find the generated ViewModel types.
![Screenshot of navigating Visual Studio Solution Explorer to inspect generated code](../Assets/InspectingGeneratedCode-3.png)

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

@ -6,7 +6,7 @@ uid: Uno.Extensions.Mvux.Advanced.Messaging
Messaging is the ability to send in-app messages between its components in a way that enables them to remain decoupled from one another.
In MVUX, we use feeds to pull entities from a service. When a command or an action is executed, we call methods on the service that apply changes to the data, for example, an entity creation, removal, or update.
In MVUX, we use feeds to pull entities from a service. When a command or an action is executed, we call methods on the service that apply changes to the data, for example, an entity creation, removal, or update.
But when the data is changed by the service, we need to ping back the feed and tell it that a change has taken place and that it should update the changed entities, but at the same time, we don't want the service to have a reference to the model or know about it; it's the model that uses the service, not the other way around.
This is where messaging comes in handy. The service sends a message about this entity change to a central messenger that publishes messages to anyone willing to listen. The model then subscribes to the messages it wants to listen to (filtered by type of entity and entity key) and updates its feeds with the updated entities received from the service.
@ -15,11 +15,11 @@ This is where messaging comes in handy. The service sends a message about this e
The Community Toolkit messenger is a common tool that can use to send and receive such messages between objects in the app. The messenger enables objects to remain decoupled from each other without keeping a strong reference between the sender and the receiver. The messages can also be sent over specific channels uniquely identified by a token or within certain areas of the application.
The core component of the messenger is the `IMessenger` object. Its main methods are `Register` and `Send`. `Register` subscribes to an object to start listening to messages of a certain type, whereas `Send` sends out messages to all listening parties.
The core component of the messenger is the `IMessenger` object. Its main methods are `Register` and `Send`. `Register` subscribes to an object to start listening to messages of a certain type, whereas `Send` sends out messages to all listening parties.
There are various ways to obtain the `IMessenger` object, but we'll use the most common one, which involves using [Dependency Injection](xref:Uno.Extensions.DependencyInjection.Overview) (DI) to register the `IMessenger` service in the app so it can then be resolved at the construction of other dependent types (e.g., ViewModels).
In the model, we obtain a reference to the `IMessenger` on the constructor, which is resolved by the DI's service provider.
The `Register` method has quite a few overloads, but for the sake of this example, let's use the one that takes the recipient and the message type as parameters. The first parameter is the recipient (`this` in this case), and the second one is a callback that is executed when a message has been received. Although `this` can be called within the callback, it's preferred that the callback doesn't make external references and that the `MyModel` is passed in as an argument.
The `Register` method has quite a few overloads, but for the sake of this example, let's use the one that takes the recipient and the message type as parameters. The first parameter is the recipient (`this` in this case), and the second one is a callback that is executed when a message has been received. Although `this` can be called within the callback, it's preferred that the callback doesn't make external references and that the `MyModel` is passed in as an argument.
`MessageReceived` is then called on the received recipient (which is the current `MyModel`), and the message is passed into it:
MVUX includes extension methods that enable the integration between the [Community Toolkit messenger](https://learn.microsoft.com/dotnet/communitytoolkit/mvvm/messenger) and [MVUX feeds](xref:Uno.Extensions.Mvux.Feeds). But before discussing how MVUX integrates with the Community Toolkit messenger, let's have a quick look at how the messenger works.
@ -30,7 +30,7 @@ using CommunityToolkit.Mvvm.Messaging;
public partial record MyModel
{
protected IMessenger Messenger { get; }
public MyModel(IMessenger messenger)
{
this.Messenger = messenger;
@ -94,7 +94,7 @@ In the example below, there is a model that displays a state-list of `Person` en
As you can gather from the code, the service interacts with an external source to load and save `Person` data. In the example below, we can see the use of two of its methods: `GetAllAsync` and `CreateNameAsync`.
There's also the `CreateNewPerson` method, which gets generated as a command in the bindable proxy, to be invoked from the View (refer to [commands](xref:Uno.Extensions.Mvux.Advanced.Commands) to learn about how MVUX generates commands). This method uses `CreateRandomName`, which generates a random name. Its implementation has been removed for brevity.
There's also the `CreateNewPerson` method, which gets generated as a command in the ViewModel, to be invoked from the View (refer to [commands](xref:Uno.Extensions.Mvux.Advanced.Commands) to learn about how MVUX generates commands). This method uses `CreateRandomName`, which generates a random name. Its implementation has been removed for brevity.
The line using the MVUX messaging extension method is the one calling `messenger.Observe`. Read the code, and this line will be explained thereafter.
@ -117,7 +117,7 @@ public partial record PeopleModel
messenger.Observe(state: People, keySelector: person => person.Name);
}
public async TaskValue CreateNewPerson(CancellationToken ct)
{
var randomName = CreateRandomName();
@ -191,8 +191,8 @@ They all share a common goal - to send and intercept entity-message messages to
public MyModel(IUserService userService)
{
UserService = userService;
messenger.Observe(CurrentUser, user => user.Id);
messenger.Observe(CurrentUser, user => user.Id);
}
public IState<User> CurrentUser => State.Async(this, UserService.GetCurrentUser);

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

@ -6,7 +6,7 @@ uid: Uno.Extensions.Mvux.Advanced.Pagination
There are several ways to paginate data.
> [!NOTE]
> [!NOTE]
> The source code for the sample app demonstrated in this section can be found [here](https://github.com/unoplatform/Uno.Samples/tree/master/UI/MvuxHowTos/PaginationPeopleApp).
## Incremental loading
@ -80,18 +80,18 @@ public partial record PeopleModel(IPeopleService PeopleService)
The `AsyncPaginated` method generates a `ListFeed` that supports pagination.
Whenever the user scrolls down to see additional data and is hitting the end of the collection displayed in a `ListView`, the pagination List-Feed is automatically triggered with a page request.
Whenever the user scrolls down to see additional data and is hitting the end of the collection displayed in a `ListView`, the pagination List-Feed is automatically triggered with a page request.
The parameter of `AsyncPaginated`, is a delegate taking in a [`PageRequest`](#the-pagerequest-type) value and a `CancellationToken` and returning an `IListFeed<T>` where `T` is `Person` in our case. This delegate is invoked when a page request comes in.
Inside this callback, we call the service by providing it with the following parameters:
- `pageSize`: the page size the `ListView` expects being able to display on the first page, which is received via the `DesiredSize` property of the `PageRequest`. This property is a nullable uint, and is null on the first call, as by the time of the first call the UI is initializing the data-binding and there's no data present yet to determine the number of items the UI should place in the space. On the first call, we'll instead use the constant value `DefaultPageSize` which is set to `20`.
- `pageSize`: the page size the `ListView` expects being able to display on the first page, which is received via the `DesiredSize` property of the `PageRequest`. This property is a nullable uint, and is null on the first call, as by the time of the first call the UI is initializing the data-binding and there's no data present yet to determine the number of items the UI should place in the space. On the first call, we'll instead use the constant value `DefaultPageSize` which is set to `20`.
- `firstItemIndex:` The index of the first item on the requested page. We use the current count (which is one-based) as the next one's index (which is zero-based).
### View
```xml
<Page
<Page
x:Class="PaginationPeopleApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@ -112,19 +112,19 @@ Inside this callback, we call the service by providing it with the following par
</Page>
```
In addition, make sure the `DataContext` of the `Page` is set to the generated bindable proxy called `BindablePeopleModel` (*MainPage.xaml.cs*):
In addition, make sure the `DataContext` of the `Page` is set to the generated ViewModel called `PeopleViewModel` (*MainPage.xaml.cs*):
```csharp
public MainPage()
{
this.InitializeComponent();
this.DataContext = new BindablePeopleModel(new PeopleService());
this.DataContext = new PeopleViewModel(new PeopleService());
}
```
> [!TIP]
> You can inspect the generated code by either placing the cursor on the word `BindablePeopleModel` and hitting <kbd>F12</kbd>, see other ways to inspect the generated code [here](xref:Uno.Extensions.Mvux.Advanced.InspectGeneratedCode).
> [!TIP]
> You can inspect the generated code by either placing the cursor on the word `PeopleViewModel` and hitting <kbd>F12</kbd>, see other ways to inspect the generated code [here](xref:Uno.Extensions.Mvux.Advanced.InspectGeneratedCode).
As you can see, there's nothing special in the XAML code as MVUX is taking advantage of the tools already implemented with the `ListView`.
@ -137,12 +137,12 @@ Here's what the app renders like:
![A video of an app that implements automatic incremental loading](../Assets/PaginationIncrementalLoading.gif)
> [!NOTE]
> [!NOTE]
> The source code for the sample app demonstrated in this section can be found [here](https://github.com/unoplatform/Uno.Samples/tree/master/UI/MvuxHowTos/PaginationPeopleApp).
## Offset pagination
Offset pagination is controlled via a State that stores the current page index in the Model, and the List-Feed depending on it, using [the `Select` operator](xref:Uno.Extensions.Mvux.Feeds#select-or-selectasync).
Offset pagination is controlled via a State that stores the current page index in the Model, and the List-Feed depending on it, using [the `Select` operator](xref:Uno.Extensions.Mvux.Feeds#select-or-selectasync).
When the user requests a new page, the current page index state is updated, thereby updating the dependent collection List-Feed.
Using the example started in [incremental loading above](#incremental-loading), we'll add another method to the service, which will disclose to the View how many items are there in total. Getting a count of items is more efficient than enumerating all entries. This is necessary to identify the total number of pages we have.
@ -187,8 +187,8 @@ public partial record PeopleModel(IPeopleService PeopleService)
}
```
`PeopleManual` is a Feed that reacts to changes in the `CurrentPage` property and projects the current page data according to its number.
To accomplish this, the [`SelectAsync` operator](xref:Uno.Extensions.Mvux.Feeds#select-or-selectasync) of Feeds is used.
`PeopleManual` is a Feed that reacts to changes in the `CurrentPage` property and projects the current page data according to its number.
To accomplish this, the [`SelectAsync` operator](xref:Uno.Extensions.Mvux.Feeds#select-or-selectasync) of Feeds is used.
The callback of this operator calls the service's `GetPeopleAsync` with the following arguments:
- `pageSize`: As with the [automatic incremental loading](#incremental-loading) example above we're passing the size of each page, except this time we are manually setting the page size to an arbitrary number via the `DefaultPageSize` constant, which is set to `20`.
@ -203,18 +203,18 @@ Replace the `ListView` from the previous example with this one:
ItemTemplate="{StaticResource PersonDataTemplate}">
<ListView.Footer>
<NumberBox
<NumberBox
HorizontalAlignment="Center"
Header="Current page:"
Minimum="1"
Minimum="1"
Maximum="{Binding PageCount}"
SpinButtonPlacementMode="Inline"
SpinButtonPlacementMode="Inline"
Value="{Binding CurrentPage, Mode=TwoWay}"/>
</ListView.Footer>
</ListView>
```
The `ListView`'s footer contains a `NumberBox` which increments/decrements the `CurrentPage` State it's bound to, via its `Value` property. The `Maximum` property is bound to `PageCount`, to disable navigating to a page that does not exist.
The `ListView`'s footer contains a `NumberBox` which increments/decrements the `CurrentPage` State it's bound to, via its `Value` property. The `Maximum` property is bound to `PageCount`, to disable navigating to a page that does not exist.
It's then propagated to the `PeopleManual` property as explained [above](#model).
When running the app, the `NumberBox` will be displayed and set with the first page:
@ -248,7 +248,7 @@ There are several caveats in using `Skip` and `Take` (Offset pagination) with an
- When we skip data records, the database might still have to process some of the skipped records on its way to the desired ones.
- If any updates have been applied to the records preceding the currently displayed page, and then the user moves to the next or previous page, there might be inconsistencies in showing the subsequent data, some of the entries might be skipped or shown twice.
An alternative way to paginate data is by using a cursor that points to a specific record and takes the number of desired records in a page onwards.
An alternative way to paginate data is by using a cursor that points to a specific record and takes the number of desired records in a page onwards.
This is referred to as 'keyset pagination' or 'seek-based pagination'.
To utilize this pagination style, MVUX provides another `ListFeed` factory overload, the `AsyncPaginatedByCursor`.
@ -301,7 +301,7 @@ The method returns a tuple with two components (same as `PageResult`, only keepi
- An `IImmutableList<Person>` which includes the entities of this page
- An `int?` which uses as a cursor to the beginning of the next page, if any.
> [!TIP]
> [!TIP]
> The cursor does not necessarily have to be an `int?` or the data type the collection contains, it can also be another key of an entity for the service to look up and return it along with its upcoming entries.
The fully implemented method in the service is as follows:

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.2 MiB

После

Ширина:  |  Высота:  |  Размер: 1.2 MiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.4 MiB

После

Ширина:  |  Высота:  |  Размер: 2.4 MiB

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

@ -49,7 +49,7 @@ Then, in the XAML:
</Page>
```
The `Source` property of the `FeedView` is data bound to the `CurrentContact` property on the bindable proxy (which will correlate to the `IFeed` property with the same name on the Model).
The `Source` property of the `FeedView` is data bound to the `CurrentContact` property on the ViewModel (which will correlate to the `IFeed` property with the same name on the Model).
In the above example, [`Data`](#data) is a property of the `FeedViewState` instance that the `FeedView` creates from the `IFeed` and sets as the `DataContext` for the various templates.

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

@ -6,13 +6,13 @@ uid: Uno.Extensions.Mvux.Feeds
Feeds are there to manage asynchronous operations (for example requesting data from a service) and expose the result to the View in an efficient manner.
They provide out of the box support for task-based methods as well as [Async-Enumerables](https://learn.microsoft.com/dotnet/api/system.collections.generic.iasyncenumerable-1) ones.
They provide out of the box support for task-based methods as well as [Async-Enumerables](https://learn.microsoft.com/dotnet/api/system.collections.generic.iasyncenumerable-1) ones.
Feeds include additional metadata that indicates whether the operation is still in progress, ended in an error, or if it was successful, whether the data that was returned contains any entries or was empty.
## Feeds are stateless
Feeds are typically used to request data from services and expose it in a stateless manner so that the resulting data can be displayed by the View.
Feeds are typically used to request data from services and expose it in a stateless manner so that the resulting data can be displayed by the View.
Feeds are stateless and do not provide support for reacting to changes the user makes to the data on the View. The data can only be reloaded and refreshed upon request which is when the underlying task or Async-Enumerable will be invoked and the data refreshed. In other words, a feed is a read-only representation of the data received from the server.
@ -76,7 +76,7 @@ public async IAsyncEnumerable<CounterValue> StartCounting([EnumeratorCancellatio
while (!ct.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(1), ct);
if (ct.IsCancellationRequested)
{
yield break;
@ -128,11 +128,11 @@ private async ValueTask SomeAsyncMethod()
#### Use feeds in an MVUX Model
The MVUX analyzers generate a bindable proxy for each of the models in your app (those with `Model` suffix). For the code generation to work, mark the Models and entities with the `partial` modifier.
The MVUX analyzers generate a ViewModel for each of the models in your app (those with `Model` suffix). For the code generation to work, mark the Models and entities with the `partial` modifier.
For every `public` feed property (returning `IFeed<T>` or `IListFeed<T>`) found in the model, a corresponding property is generated on the bindable proxy.
For every `public` feed property (returning `IFeed<T>` or `IListFeed<T>`) found in the model, a corresponding property is generated on the ViewModel.
MVUX recommends using plain [POCO](https://en.wikipedia.org/wiki/Plain_old_CLR_object) (Plain Old CLR Object) `record` types for the models in your app as they're immutable, and will not require any property change notifications to be raised. The generated bindable proxy and its properties ensure that data-binding will work, even though property change notifications aren't being raised by the models themselves.
MVUX recommends using plain [POCO](https://en.wikipedia.org/wiki/Plain_old_CLR_object) (Plain Old CLR Object) `record` types for the models in your app as they're immutable, and will not require any property change notifications to be raised. The generated ViewModel and its properties ensure that data-binding will work, even though property change notifications aren't being raised by the models themselves.
#### With regular data-binding
@ -148,9 +148,9 @@ The feed can be consumed directly from the View, by data binding to a property e
#### With the `FeedView` control
The `FeedView` control has been designed to work with feeds and is tailored to the additional metadata mentioned [earlier](#what-are-feeds) that are disclosed by the feed and respond to it automatically and efficiently.
The `FeedView` control has been designed to work with feeds and is tailored to the additional metadata mentioned [earlier](#what-are-feeds) that are disclosed by the feed and respond to it automatically and efficiently.
The `FeedView` has templates that change the visible contents based on the current state of the data, such as when the data request is still in progress, an error has occurred, or when the data contained no records.
The `FeedView` has templates that change the visible contents based on the current state of the data, such as when the data request is still in progress, an error has occurred, or when the data contained no records.
Built-in templates are included with the `FeedView` for these states, but they can all be customized.
Here's how to utilize the `FeedView` to display the same data as before:
@ -159,7 +159,7 @@ Here's how to utilize the `FeedView` to display the same data as before:
<Page
...
xmlns:mvux="using:Uno.Extensions.Reactive.UI">
<mvux:FeedView Source="{Binding CurrentCount}">
<DataTemplate>
<TextBlock DataContext="{Binding Data}" Text="{Binding Value}" />
@ -176,7 +176,7 @@ An `IFeed` supports some LINQ operators that can be used to apply a transform an
### Where
The `Where` extension method enables filtering a Feed. It returns a new Feed where the values of the parent one match the specified criteria.
The `Where` extension method enables filtering a Feed. It returns a new Feed where the values of the parent one match the specified criteria.
For example:
```csharp

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

@ -13,7 +13,7 @@ To better understand the need for MVUX, let us consider a weather application th
### Weather App Example - MVVM
For example, using a Model-View-ViewModel (MVVM) approach, the following `MainViewModel` initializes the `CurrentWeather` property with the information obtained from the weather service. The XAML binds the `CurrentWeather` property of the `DataContext` (an instance of the `MainViewModel`) to the Text property of a TextBlock
#### [MainViewModel](#tab/viewmodel)
```csharp
@ -68,7 +68,7 @@ public MainPage()
----
Here's this simple application running:
Here's this simple application running:
![Mvvm Weather App](Assets/MvvmWeather.jpg)
The code required to call the `GetCurrentWeather` and displaying the resulting `Temperature` using XAML is simple enough. However, there are a few things we should consider:
@ -196,7 +196,7 @@ The XAML includes four visual states which are triggered based on the value of `
----
Here's the updated application running, showing the progress ring in action while the data is loading and a Get Weather Button for refreshing the data.
![Mvvm Weather App](Assets/MvvmWeatherUpdated.jpg)
This simple example illustrates how quickly simple code can grow in complexity when we consider the various states that an application can be in.
@ -257,14 +257,14 @@ The `MainModel` (as distinct from the `MainViewModel` used in the MVVM example)
</Page>
```
The `DataContext` on the `MainPage` is set to be an instance of the `BindableMainModel`, which is a bindable proxy for the `MainModel` that is generated by MVUX to provide data binding support to the `MainModel`.
The `DataContext` on the `MainPage` is set to be an instance of the `MainViewModel`, which is the ViewModel for the `MainModel` that is generated by MVUX to provide data binding support to the `MainModel`.
```csharp
public MainPage()
{
this.InitializeComponent();
DataContext = new BindableMainModel(new WeatherService());
DataContext = new MainViewModel(new WeatherService());
}
```
@ -304,7 +304,7 @@ public partial record MainModel(IWeatherService WeatherService)
The `CurrentWeather` property returns an `IFeed` of `WeatherInfo` entities. An `IFeed` represents a stream, or sequence, of values. For those familiar with [Reactive](https://reactivex.io/) this is similar to an `IObservable`.
When the `CurrentWeather` property is accessed, an `IFeed` is created via the `Feed.Async` factory method, which will asynchronously call the `GetCurrentWeather` service, and return the result as a `WeatherInfo` entity.
When the `CurrentWeather` property is accessed, an `IFeed` is created via the `Feed.Async` factory method, which will asynchronously call the `GetCurrentWeather` service, and return the result as a `WeatherInfo` entity.
Feeds are covered in more detail in the [Feeds](xref:Uno.Extensions.Mvux.Feeds) documentation.
@ -324,26 +324,26 @@ In MVUX, the **View** is the UI, which can be written in XAML, C#, or a combinat
</TextBlock>
</StackPanel>
</Page>
```
```
Unlike MVVM where the `DataContext` of the page would be set to an instance of the ViewModel, in MVUX the `DataContext` is set to an instance of a generated bindable proxy that wraps the **Model**. In this case, the bindable proxy for MainModel is named `BindableMainModel`.
Unlike MVVM where the `DataContext` of the page would be set to an instance of the ViewModel, in MVUX the `DataContext` is set to an instance of a generated ViewModel that wraps the **Model**. In this case, the ViewModel for MainModel is named `MainViewModel`.
```csharp
public MainPage()
{
this.InitializeComponent();
DataContext = new BindableMainModel(new WeatherService());
DataContext = new MainViewModel(new WeatherService());
}
```
The generated `BindableMainModel` exposes the `IFeed` properties of the `MainModel` in a way that they can be data bound using simple data binding expressions, for example `{Binding CurrentWeather.Temperature}`.
The generated `MainViewModel` exposes the `IFeed` properties of the `MainModel` in a way that they can be data bound using simple data binding expressions, for example `{Binding CurrentWeather.Temperature}`.
What's unique to MVUX is the additional information that `IFeed` exposes. The `IFeed` includes information about the state of the asynchronous operation, such as whether the operation is in progress, whether the operation returned data, or not, and whether there was an error. This information can be used to display the appropriate UI to the user.
To simplify working with an `IFeed`, we can leverage the MVUX `FeedView` control. The `FeedView` has been designed to work with `IFeed` sources and exposes an simple way for developers to define what the layout should be for the different states of the asynchronous operation.
The following XAML shows how the `FeedView` can be used to display the current temperature. The `Source` property is bound to the `CurrentWeather` property of the `BindableMainModel`. Inside the `DataTemplate` the `Data` property contains the data returned by the `CurrentWeather` property, which will be a `WeatherInfo` entity.
The following XAML shows how the `FeedView` can be used to display the current temperature. The `Source` property is bound to the `CurrentWeather` property of the `MainViewModel`. Inside the `DataTemplate` the `Data` property contains the data returned by the `CurrentWeather` property, which will be a `WeatherInfo` entity.
```xml
<Page x:Class="WeatherApp.MainPage"
@ -373,7 +373,7 @@ The `FeedView` control has different visual states that align with the different
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mvux="using:Uno.Extensions.Reactive.UI">
<mvux:FeedView Source="{Binding CurrentWeather}">
<mvux:FeedView.ValueTemplate>
<DataTemplate>
@ -416,7 +416,7 @@ public partial record MainModel(IWeatherService WeatherService)
}
```
An `IState` is a special type of property that is used to store state. The bindable proxy generated for the `MainModel` will now include a `City` property that can be two-way data bound in order to accept user input.
An `IState` is a special type of property that is used to store state. The ViewModel generated for the `MainModel` will now include a `City` property that can be two-way data bound in order to accept user input.
```xml
<TextBox Text="{Binding City, Mode=TwoWay}" />
@ -446,15 +446,15 @@ This is just one example of how user input can be accepted in order to trigger a
### eXtended
In summary, MVUX is a set of abstractions that are designed to work well with the data binding engine. The use of `IFeed` and `IState` properties allow the **Model** to be expressed more declaratively. The source code generator then generates bindable proxies for each **Model**. The bindable proxies are used as a bridge that enables immutable entities to work with the data-binding engine.
In summary, MVUX is a set of abstractions that are designed to work well with the data binding engine. The use of `IFeed` and `IState` properties allow the **Model** to be expressed more declaratively. The source code generator then generates ViewModels for each **Model**. The ViewModels are used as a bridge that enables immutable entities to work with the data-binding engine.
#### MVVM vs MVUX Data Flow
The Model-View-ViewModel (MVVM) pattern is a popular pattern for building XAML applications. The MVVM pattern is a specialization of the Presentation Model pattern, where the **ViewModel** is responsible for exposing data from the **Model** to the **View**. The **ViewModel** is also responsible for handling user interactions and updating the **Model**. The **ViewModel** is often referred to as the glue between the **Model** and the **View**.
The Model-View-ViewModel (MVVM) pattern is a popular pattern for building XAML applications. The MVVM pattern is a specialization of the Presentation Model pattern, where the **ViewModel** is responsible for exposing data from the **Model** to the **View**. The **ViewModel** is also responsible for handling user interactions and updating the **Model**. The **ViewModel** is often referred to as the glue between the **Model** and the **View**.
![Mvvm](Assets/Mvvm.jpg)
The Model-View-Update-eXtended (MVUX) pattern still leverages data binding to present information to the user and capture input. However, instead of being bound directly to the **Model**, the **View** is data bound to bindable proxies that are generated by MVUX. It's the use of bindable proxies that ensure the single direction of flow of data. When an update occurs, either by the user triggering an action, or entering some data, this generates new instances of the **Model**. The bindable proxies then detect the change and update the **View** accordingly. The bindable proxies also ensure that the **Model** is updated in a thread-safe manner.
The Model-View-Update-eXtended (MVUX) pattern still leverages data binding to present information to the user and capture input. However, instead of being bound directly to the **Model**, the **View** is data bound to ViewModels that are generated by MVUX. It's the use of these ViewModels that ensure the single direction of flow of data. When an update occurs, either by the user triggering an action, or entering some data, this generates new instances of the **Model**. The ViewModels then detect the change and update the **View** accordingly. The ViewModels also ensure that the **Model** is updated in a thread-safe manner.
![Mvux](Assets/Mvux.jpg)
@ -477,8 +477,8 @@ You can then use the MVUX example above as a reference to create your own `IFeed
- Define your own Models
- MVUX recommends using record types for the Models in your app as they're immutable.
- The MVUX analyzers auto-generate a bindable proxy for each `partial` `class` or `record` named with a _Model_ suffix.
- For every public [`IFeed`](xref:Uno.Extensions.Mvux.Feeds) property found in the model, a corresponding property is generated on the bindable proxy.
- The MVUX analyzers auto-generate a ViewModel for each `partial` `class` or `record` named with a _Model_ suffix.
- For every public [`IFeed`](xref:Uno.Extensions.Mvux.Feeds) property found in the model, a corresponding property is generated on the ViewModel.
- You can use [`IState`](xref:Uno.Extensions.Mvux.States) properties to accepting input from the user.
### In the View

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

@ -10,8 +10,8 @@ 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).
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.
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).
@ -117,7 +117,7 @@ States are built to be cooperating with the data-binding engine. A State will au
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:SliderApp">
<Page.DataContext>
<local:BindableSliderModel />
<local:SliderViewModel />
</Page.DataContext>
<StackPanel>
@ -134,7 +134,7 @@ States are built to be cooperating with the data-binding engine. A State will au
</Page>
```
In this scenario, the `DataContext` is set to an instance of the `BindableSliderModel` class, which is the generated bindable proxy for the `SliderModel` record.
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:

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

@ -9,7 +9,7 @@ and displays a collection of items from a service, and enables refreshing the da
In this tutorial, you will learn how to create an MVUX project and the basic usage of a list-feed (`IListFeed<T>`), and the `FeedView` control.
- For our data, we're going to create a service that asynchronously provides a collection of `Person` entities upon request.
- For our data, we're going to create a service that asynchronously provides a collection of `Person` entities upon request.
- You'll learn how to use a feed to asynchronously request this data from the service.
- How to display the data on the UI
- How to use the `FeedView` control to display the data and automatically respond to the current feed status.
@ -60,38 +60,38 @@ You can find the code of this tutorial [here](https://github.com/unoplatform/Uno
```csharp
using Uno.Extensions.Reactive;
namespace PeopleApp;
public partial record PeopleModel(IPeopleService PeopleService)
{
public IListFeed<Person> People => ListFeed.Async(PeopleService.GetPeopleAsync);
}
```
> [!NOTE]
> Feeds (`IFeed<T>` and `IListFeed<T>` for collections) are used as a gateway to asynchronously request data from a service and wrap the result or an error if any in metadata to be displayed in the View in accordingly.
> [!NOTE]
> Feeds (`IFeed<T>` and `IListFeed<T>` for collections) are used as a gateway to asynchronously request data from a service and wrap the result or an error if any in metadata to be displayed in the View in accordingly.
Learn more about list-feeds [here](xref:Uno.Extensions.Mvux.HowToListFeed).
>
> [!TIP]
> Feeds are stateless and are there for when the data from the service is read-only and we're not planning to enable edits to it.
MVUX also provides stateful feeds. For that purpose States (`IState<T>` and `<IListState<T>` for collections) come in handy.
> [!TIP]
> Feeds are stateless and are there for when the data from the service is read-only and we're not planning to enable edits to it.
MVUX also provides stateful feeds. For that purpose States (`IState<T>` and `<IListState<T>` for collections) come in handy.
Refer to [this tutorial](xref:Uno.Extensions.Mvux.HowToSimpleState) to learn more about states.
## Data-bind the view
`PeopleModel` exposes a `People` property which is an `IListFeed<T>` where `T` is a `Person`.
`PeopleModel` exposes a `People` property which is an `IListFeed<T>` where `T` is a `Person`.
This is similar in concept to an `IObservable<IEnumerable<T>>`, where an `IListFeed<T>` represents a sequence of person-collections obtained from the service.
> [!TIP]
> [!TIP]
> An `IListFeed<T>` is awaitable, meaning that to get the value of the feed you would do the following in the model:
>
> ```csharp
> IImmutableList<Person> people = await this.People;
> ```
> ```
To make it possible to data bind to feeds, the MVUX analyzers read the `PeopleModel`
and generate a bindable proxy called `BindableWeatherModel`,
and generate a ViewModel called `PeopleViewModel`,
which exposes properties that the View can data bind to.
1. Open the file `MainView.xaml` and add the following namespace to the XAML:
@ -123,13 +123,13 @@ which exposes properties that the View can data bind to.
</mvux:FeedView>
```
> [!TIP]
> [!TIP]
> The `FeedView` wraps its source (in this case the `People` feed) in a `FeedViewState` object, and makes the current value of the feed accessible via its `Data` property as well as the `Refresh` property, which is a command that explicitly triggers reloading the data.
1. Press <kbd>F7</kbd> to navigate to open code-view, and in the constructor, after the line that calls `InitializeComponent()`, add the following line:
```csharp
this.DataContext = new BindablePeopleModel(new PeopleService());
this.DataContext = new PeopleViewModel(new PeopleService());
```
1. Click <kbd>F5</kbd> to run the project.
@ -142,9 +142,9 @@ which exposes properties that the View can data bind to.
![A screenshot of the loaded data](../Assets/ListFeed-1.jpg)
1. If you're using Visual-Studio 2022, Right-click the `PeopleApp` project, and navigate to *Dependencies*.
Open up *net8.0-windows10...**Analyzers*.
Under *Uno.Extensions.Reactive.Generator*, expand *Uno.Extensions.Reactive.FeedGenerator*.
1. If you're using Visual-Studio 2022, Right-click the `PeopleApp` project, and navigate to *Dependencies*.
Open up *net8.0-windows10...**Analyzers*.
Under *Uno.Extensions.Reactive.Generator*, expand *Uno.Extensions.Reactive.FeedGenerator*.
Here you'll be able to inspect all files MVUX has generated for you, and learn more about how MVUX runs behind the scenes.
![Screenshot of navigating Visual Studio Solution Explorer to inspect generated code](../Assets/InspectGeneratedCode.jpg)

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

@ -70,7 +70,7 @@ You can find the code of this tutorial [here](https://github.com/unoplatform/Uno
We're using a [record](https://learn.microsoft.com/dotnet/csharp/language-reference/builtin-types/record) for the `Stock` type on purpose, as records are immutable and ensure the purity of objects as well as other features.
The `GetCurrentMarket` emits a collection of stocks with updated values every 5 seconds.
The `GetCurrentMarket` emits a collection of stocks with updated values every 5 seconds.
The `IListFeed<T>` is a feed tailored for dealing with collections.
1. Create a class named *StockMarketModel.cs* replacing its content with the following:
@ -82,32 +82,32 @@ You can find the code of this tutorial [here](https://github.com/unoplatform/Uno
}
```
> [!NOTE]
> Feeds (`IFeed<T>` and `IListFeed<T>` for collections) are used as a gateway to asynchronously request data from a service and wrap the result or error if any in metadata to be displayed in the View accordingly.
> [!NOTE]
> Feeds (`IFeed<T>` and `IListFeed<T>` for collections) are used as a gateway to asynchronously request data from a service and wrap the result or error if any in metadata to be displayed in the View accordingly.
> Learn more about list-feeds [here](xref:Uno.Extensions.Mvux.HowToListFeed).
>
> [!TIP]
> [!TIP]
> Feeds are stateless
> and are there for when the data from the service is read-only and we're not planning to enable edits to it.
> and are there for when the data from the service is read-only and we're not planning to enable edits to it.
> MVUX also provides stateful feeds. For that purpose States (`IState<T>` and `<IListState<T>` for collections) come handy.
> Refer to [this tutorial](xref:Uno.Extensions.Mvux.HowToSimpleState) to learn more about states.
## Data bind the view
The `Stocks` property on `StockMarketModel` is an `IListFeed<T>` where `T` is `Stock`.
The `Stocks` property on `StockMarketModel` is an `IListFeed<T>` where `T` is `Stock`.
This is similar in concept to an `IObservable<IImmutableList<Stock>>`,
where an `IListsFeed<T>>` represents a sequence of collections pushed in whenever they become available,
signaling the UI about the new data.
> [!TIP]
> An `IListFeed<T>` is awaitable, meaning that to get the value of the feed you would execute the following in the model:
> An `IListFeed<T>` is awaitable, meaning that to get the value of the feed you would execute the following in the model:
>
> ```csharp
> StockMarket currentMarket = await this.Stocks;
> ```
> ```
To make it possible to data bind to feeds, the MVUX analyzers read the `StockMarketModel`
and generate a bindable proxy called `BindableStockMarketModel`, which exposes properties that the View can data bind to.
and generate a ViewModel called `StockMarketViewModel`, which exposes properties that the View can data bind to.
1. Open the file `MainView.xaml` and replace anything inside the `Page` element with the following code:
@ -127,7 +127,7 @@ and generate a bindable proxy called `BindableStockMarketModel`, which exposes p
1. Press <kbd>F7</kbd> to navigate to open code-view, and in the constructor, after the line that calls `InitializeComponent()`, add the following line:
```csharp
this.DataContext = new BindableStockMarketModel(new StockMarketService());
this.DataContext = new StockMarketViewModel(new StockMarketService());
```
1. Press <kbd>F5</kbd> to run the app.

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

@ -73,8 +73,8 @@ You can find the code for our weather app [here](https://github.com/unoplatform/
> WeatherInfo currentWeather = await this.CurrentWeather;
> ```
>
> To make it possible to data bind to a feeds, the MVUX analyzers read the `WeatherModel` and generate a bindable proxy called `BindableWeatherModel`, which exposes properties that the View can data bind to.
In this case the `BindableWeatherModel` exposes a `CurrentWeather` property that can be uses in a data binding expression the same way you would with a regular property that returns a `WeatherInfo` entity.
> To make it possible to data bind to a feeds, the MVUX analyzers read the `WeatherModel` and generate a ViewModel called `WeatherViewModel`, which exposes properties that the View can data bind to.
In this case the `WeatherViewModel` exposes a `CurrentWeather` property that can be uses in a data binding expression the same way you would with a regular property that returns a `WeatherInfo` entity.
1. Open the file `MainView.xaml` and replace the `Page` contents with the following:
@ -85,7 +85,7 @@ In this case the `BindableWeatherModel` exposes a `CurrentWeather` property that
1. Press <kbd>F7</kbd> to navigate to open code-view, and in the constructor, after the line that calls `InitializeComponent()`, add the following line:
```csharp
this.DataContext = new BindableWeatherModel(new WeatherService());
this.DataContext = new WeatherViewModel(new WeatherService());
```
1. Press <kbd>F5</kbd> to run the app. The app will load with a default `WeatherInfo` value, with a `Temperature` of `0`:
@ -99,7 +99,7 @@ In this case the `BindableWeatherModel` exposes a `CurrentWeather` property that
Note that this is a random value and may be different on your machine.
> [!NOTE]
> It's worth noting that the `CurrentWeather` feed will only be invoked once, and the value captured in the bindable proxy.
> It's worth noting that the `CurrentWeather` feed will only be invoked once, and the value captured in the ViewModel.
> The captured value will be returned to all binding expressions that use CurrentWeather.
> This means that it's OK to use a lambda expression when defining the IFeed (`=>`), so that it can accessing the local `WeatherService` in `Feed.Async(WeatherService.GetCurrentWeatherModel)`.
> The `WeatherService` property wouldn't have been available in a regular assignment context (`=`).

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

@ -125,9 +125,9 @@ The difference of States is that they provide update operators and enable manipu
![A screenshot of a breakpoint added in Visual Studio](../Assets/SimpleState-2.jpg)
MVUX's analyzers will read the `HallCrowdednessModel` and will generate a special bindable proxy called `BindableHallCrowdednessModel`, which provides binding capabilities for the View and performs all Update message for us, to keep the `IState` up to date.
MVUX's analyzers will read the `HallCrowdednessModel` and will generate a special ViewModel called `HallCrowdednessViewModel`, which provides binding capabilities for the View and performs all Update message for us, to keep the `IState` up to date.
In addition, MVUX reads the `Save` method, and generates in the bindable Model a command named `Save` that can be used from the View, which is invoked asynchronously.
In addition, MVUX reads the `Save` method, and generates in the ViewModel a command named `Save` that can be used from the View, which is invoked asynchronously.
1. In the XAML file, after the `TextBox`, add the following `Button` code:
@ -146,10 +146,10 @@ The difference of States is that they provide update operators and enable manipu
1. Press <kbd>F7</kbd> to navigate to open code-view, and in the constructor, after the line that calls `InitializeComponent()`, add the following line:
```csharp
this.DataContext = new BindableHallCrowdednessModel(new HallCrowdednessService());
this.DataContext = new HallCrowdednessViewModel(new HallCrowdednessService());
```
The `BindableHallCrowdednessModel` is a special MVUX-generated bindable proxy class that represents a mirror of the `HallCrowdednessModel` adding binding capabilities, for MVUX to be able to recreate and renew the model when an update message is sent by the view.
The `HallCrowdednessViewModel` is a special MVUX-generated ViewModel bindable proxy class that represents a mirror of the `HallCrowdednessModel` adding binding capabilities, for MVUX to be able to recreate and renew the model when an update message is sent by the view.
1. Click <kbd>F5</kbd> to run the project

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

@ -198,7 +198,7 @@ In order to make a record editable field per field (e.g. a user profile where yo
### Dispatching and threading considerations
The users `ViewModel` are expected to run on background thread for performance considerations. Commands and state updates from generated bindable classes are always ran on a background thread.
The users `ViewModel` are expected to run on background thread for performance considerations. Commands and state updates from generated ViewModel classes are always ran on a background thread.
To avoid strong dependency on a dispatcher (neither by injection nor by the need to create VM on UI thread), all generated classes are auto resolving the dispatcher when an event handler is registered or when a public method is used, using the internal `LazyDispatcherResolver` and `EventManager`. Those classes are then completing their initialization once the first dispatcher is resolved.