What are states
States are stateful feeds
Like 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 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).
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:
- When subscribing to a state, the currently loaded value is going to be replayed.
- A state provides the
Update
method that allows changing its current value. - States are attached to an owner and share the same lifetime as that owner.
- 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:
public record MainModel {
public IState<Person> MainContact => State.Async(this, ContactsService.GetMainContact);
}
Where GetMainContact
is a ValueTask<Person>
, 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:
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.
You can learn more about Async Enumerables in this article.
Start with an empty state
You can setup a state without any values, values can always be set using the Set
or the Update
methods.
public IState<City> CurrentCity => State<City>.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
or the Set
methods.
public IState<City> CurrentCity => State.Value(this, () => new City("Montreal"));
From a feed
A state can easily be converted from a feed as follows:
public IFeed<int> MyFeed => ...
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.
This is intended for advanced users and is explained here.
Usage of States
States are advanced Feeds. As such, they can also be awaited directly:
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.
In an MVUX app (read How to set up an MVUX project), add a Model class with a State as follows:
public partial record SliderModel { // create a state with an initial random double value between 0 and 1, multiplied by 100. public IState<double> SliderValue => State.Value(this, () => Random.Shared.NextDouble() * 100); }
Replace all child elements in the MainPage.xaml with the following:
<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> </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.
When you run the app, moving the
Slider
instantly affects the upperTextBox
; theSilder.Value
property has a two-way binding with theSliderValue
State, so any change to the Slider immediately updates the State value, which in turn affects the data-boundTextBlock
on top:
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):
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<T, T>
. 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 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. 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:
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.
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:
public partial record Model
{
public IState<string> MyState => ...
public async ValueTask EnableChangeTracking()
{
MyState.ForEachAsync(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, 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 with the following:
...
<TextBlock Text="Set state value:"/>
<Slider Value="{Binding SliderValue, Mode=TwoWay}" />
<Button Content="Increment slider" Command="{Binding IncrementSlider}" />
</StackPanel>
</Page>
When pressing the Increment slider button, the generated IncrementSlider
command will be executed invoking the IncrementSilder
method on the Model resulting in an incrementation of the value.
This is what the result will look like:
The source code for the sample app can be found here.
To learn more about Commands read the Commands section in this article.