Usage in applications of feeds
The recommended use for feeds is only in models.
The Reactive framework allows you to design state-less models, focusing on the presentation logic.
They are expected to expose some IFeed
, IListFeed
, IState
or IListState
.
A bindable friendly and highly performant view model is then automatically generated by the Reactive framework.
It is this class that will hold the state and which has to be set as DataContext
of your page.
Note
The ViewModel of a model is created when the class name matches the regex "Model$".
You can customize that behavior using the [ImplicitBindables("MyApp\.Presentation\.\.*Model$")]
on the assembly
or the [ReactiveBindable]
attribute on you model itself.
Note
To ease models declaration, public properties are also accessible on the view model class.
You can also access the model itself through the Model
property.
Display some data in the UI (Model to View)
To render a feed in the UI, you only have to expose it through a public property:
public IFeed<Product[]> Products = Feed.Async(_productService.GetProducts);
Then in your page, you can add a FeedView
:
<Page x:Class="MyProject.MyPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uer="using:Uno.Extensions.Reactive.UI">
<uer:FeedView Source="{Binding Products}">
<DataTemplate>
<ListView ItemsSource="{Binding Data}" />
</DataTemplate>
</uer:FeedView>
Refreshing a data
The FeedView
exposes a Refresh
command directly in the data context of its content.
You can use this command to trigger a refresh from the view, like a "pull to refresh".
<Page x:Class="MyProject.MyPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uer="using:Uno.Extensions.Reactive.UI">
<uer:FeedView Source="{Binding Products}">
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<Grid.RowDefinitions>
<Button Grid.Row="0"
Content="Refresh"
Command="{Binding Refresh}" />
<ListView Grid.Row="1"
ItemsSource="{Binding Data}" />
</Grid>
</DataTemplate>
</uer:FeedView>
Refreshing using a RefreshContainer (a.k.a. pull to refresh)
The RefreshContainer
does not have a Command
property to which you can data-bind the command exposed by the FeedView
.
This is because the RefreshContainer
needs to know when the refresh has completed in order to remove the loading wheel.
However, Uno.Extensions defines the IAsyncCommand.IsExecuting
property which can be used to track the completion of the refresh.
Uno.Extensions also defines a RefreshContainerExtensions.Command
attached property which allow to directly data-bind the refresh command
(or any generated feed command) to the RefreshContainer
.
<Page x:Class="MyProject.MyPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:uer="using:Uno.Extensions.Reactive.UI">
<uer:FeedView Source="{Binding Products}"
RefreshingState="None">
<DataTemplate>
<RefreshContainer uer:RefreshContainerExtensions.Command="{Binding Refresh}">
<ListView ItemsSource="{Binding Data}" />
</RefreshContainer>
</DataTemplate>
</uer:FeedView>
Tip
When you use a RefreshContainer
to trigger the refresh of your feed, as the RefreshContainer
do have its own loading indicator,
you might have 2 loading indicators, one from the RefreshContainer
itself, and a second one from the FeedView
.
In order to avoid that, you should instruct your FeedView
to not use the loading state in case of refresh by setting RefreshingState="None"
.
Pagination
When your source supports pagination (like ListFeed.Paginated
), you can data-bind the collection exposed by the generated view model to a ListView
to automatically enable pagination.
<uer:FeedView Source="{Binding Items}">
<DataTemplate>
<ListView ItemsSource="{Binding Data}" />
</DataTemplate>
</uer:FeedView>
The collection exposed for binding by the generated view models also exposes some extended properties, so you can enhance the UX of the pagination, like display a loading indicator while loading the next page.
<uer:FeedView Source="{Binding Items}">
<DataTemplate>
<ListView Grid.Row="1"
ItemsSource="{Binding Data}">
<ListView.Footer>
<ProgressBar Visibility="{Binding Data.ExtendedProperties.IsLoadingMoreItems}"
IsIndeterminate="{Binding Data.ExtendedProperties.IsLoadingMoreItems}"
HorizontalAlignment="Stretch" />
</ListView.Footer>
</ListView>
</DataTemplate>
</uer:FeedView>
Selection
The reactive framework has embedded support of selected item of a ListFeed
data-bound to a Selector
, like the ListView
.
It is not required to data-bind (and synchronize) the ListView.SelectedItem
, it will instead be automatically pushed from and to the view.
<uer:FeedView Source="{Binding Items}">
<DataTemplate>
<ListView ItemsSource="{Binding Data}" />
</DataTemplate>
</uer:FeedView>
And in the model:
public IListState<int> Items => ListView
.Value(this, () => ImmutableList.Create(1, 2, 3, 4, 5))
.Selection(SelectedItem);
public IState<int> SelectedItem => State.Value(this, () => 3);
Project selected item into another entity
It is possible to synchronize the selected item key into another aggregate root object, for instance, assuming Profile
and Country
records:
public record Profile(string? FirstName, string? CountryId);
public record Country(string Id, string Name);
The selected item automatically stored into the Profile.CountyId
by doing:
public IState<Profile> Profile => State.Async(this, _svc.GetProfile);
public IListState<Country> Countries => ListState
.Async(this, _svc.GetCountries)
.Selection(Profile, p => p.CountryId);
<uer:FeedView Source="{Binding Countries}">
<DataTemplate>
<ComboBox ItemsSource="{Binding Data}" />
</DataTemplate>
</uer:FeedView>
Note
As the IState<Porfile> Profile
state might be None
when an item is being selected, the Profile
class
must have either a parameter-less contructor, either a constructor that accepts only nullable parameters.
Commands
The generated view model of a model will automatically re-expose as ICommand
public methods that are compatible (cf. "general rules" below).
For instance, in your model:
public async ValueTask Share()
{
}
This will be exposed into an ICommand
that can be data-bound to the Command
property of a Button
<Button Command="{Binding Share}"
Content="Share" />
By default, if the method has a parameter T myValue
and there is a property Feed<T> MyValue
on the same class (matching type and name), that parameter will automatically be filled from that feed.
For instance, in your model:
public IFeed<string> Message { get; }
public async ValueTask Share(string message)
{
}
Can be used with or without any CommandParameter
from the view:
<!-- Without CommandParameter -->
<!-- 'message' arg in the 'Share' method will be the current value of the Message _feed_ -->
<Button Command="{Binding Share}"
Content="Share" />
<!-- With CommandParameter -->
<!-- 'message' arg in the 'Share' method will be "hello world" -->
<Button Command="{Binding Share}"
CommandParameter="hello world"
Content="Share" />
You can also use both "feed parameters" and "view parameter" (i.e. the value of the CommandParameter
):
public IFeed<MyEntity> Entity { get; }
public async ValueTask Share(MyEntity entity, string origin)
{
}
<Button Command="{Binding Share}"
CommandParameter="command_bar"
Content="Share" />
General rules for methods to be re-exposed as commands are:
- At most one parameter that cannot be resolved from a Feed property in your VM (a.k.a the
CommandParameter
); - At most one
CancellationToken
; - Method can be sync, or async
Note
The automatic parameters resolution be configured using attributes:
[ImplicitFeedCommandParameters(is_enabled)]
on assembly or class to enable or disable the implicit parameters resolution.[FeedParameter("MyEntity")]
to explicit the property to use for a given parameter, e.g.public IFeed<string> Message { get; } public async ValueTask Share([FeedParameter(nameof(Message))] string msg) { }