Selection
MVUX has embedded support for both single item and multi-item selection.
Any control that inherits Selector
(e.g. ListView
, GridView
, ComboBox
, FlipView
), has automatic support for updating a List-State with its current selection.
Binding to the SelectedItem
property is not even required, as this works automatically.
To synchronize to the selected value in the Model
side, use the Selection
operator of the IListFeed
.
Recap of the PeopleApp example
We'll be using the PeopleApp example which we've built step-by-step in this tutorial.
The PeopleApp uses an IListFeed<T>
where T
is a Person
record with the properties FirstName
and LastName
.
It has a service that has the following contract:
public interface IPeopleService
{
ValueTask<IImmutableList<Person>> GetPeopleAsync(CancellationToken ct);
}
It is then used by the PeopleModel
class which requests the service using a List-Feed.
public partial record PeopleModel(IPeopleService PeopleService)
{
public IListFeed<Person> People => ListFeed.Async(PeopleService.GetPeopleAsync);
}
The data is then displayed on the View using a ListView
:
<Page ...>
<ListView ItemsSource="{Binding People}">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="{Binding FirstName}"/>
<TextBlock Text="{Binding LastName}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Page>
Note
The use of the FeedView
is not necessary in our example, hence the ListView
has been extracted from it, and its ItemsSource
property has been directly data-bound to the Feed.
Implement selection in the PeopleApp
MVUX has two extension methods of IListFeed<T>
, that enable single or multi-selection.
Note
The source code for the sample app demonstrated in this section can be found here.
Single-item selection
A Feed doesn't store any state, so the People
property won't be able to hold any information, nor the currently selected item.
To enable storing the selected value in the model, we'll create an IState<Person>
which will be updated by the Selection
operator of the IListFeed<T>
(it's an extension method).
Let's change the PeopleModel
as follows:
public partial record PeopleModel(IPeopleService PeopleService)
{
public IListFeed<Person> People =>
ListFeed
.Async(PeopleService.GetPeopleAsync)
.Selection(SelectedPerson);
public IState<Person> SelectedPerson => State<Person>.Empty(this);
}
The SelectedPerson
State is initialized with an empty value using State<Person>.Empty(this)
(we still need a reference to the current instance to enable caching).
Note
Read this to learn more about States and the Empty
factory method.
The Selection
operator was added to the existing ListFeed.Async(...)
line, it will listen to the People
List-Feed and will affect its selection changes onto the SelectedPerson
State property.
In the View side, wrap the ListView
element in a Grid
, and insert additional elements to reflect the currently selected value via the SelectedPerson
State.
We'll also add a separator (using Border
) to be able to distinguish them.
The View code shall look like the following:
<Page ...>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel DataContext="{Binding SelectedPerson}" Orientation="Horizontal" Spacing="5">
<TextBlock Text="Selected person:" />
<TextBlock Text="{Binding FirstName}"/>
<TextBlock Text="{Binding LastName}"/>
</StackPanel>
<Border Height="2" Background="Gray" Grid.Row="1" />
<ListView Grid.Row="2" ItemsSource="{Binding People}">
</Grid>
</Page>
When running the app, the top section will reflect the item the user selects in the ListView
:
Note
The source code for the sample app can be found here.
Listening to the selected value
You can listen and detect selection changes by either creating a feed that projects the selection with the Select
operator or by subscribing to the selection feed using the ForEach
operator.
Using the Select operator
Using the example above, we can project the SelectedPerson
property to project or transform the current Person
, using the SelectedPerson
's Where
and Select
operators.
public IFeed<string> GreetingSelect => SelectedPerson.Select(person => person == null ? string.Empty : $"Hello {person.FirstName} {person.LastName}!");
A TextBlock
can then be added in the UI to display the selected value:
<TextBlock Text="{Binding GreetingSelect}"/>
Using the ForEachAsync operator
Selection can also be propagated manually to a State using the ForEachAsync
operator.
First, we need to create a State with a default value, which will be used to store the processed value once a selection has occurred.
public IState<string> GreetingForSelectedPerson => State.Value(this, () => string.Empty);
In the constructor, we can then subscribe to the value in the following manner:
public partial record PeopleModel
{
private IPeopleService _peopleService;
public PeopleModel(IPeopleService peopleService)
{
_peopleService = peopleService;
SelectedPerson.ForEachAsync(action: SelectionChanged);
}
...
public async ValueTask SelectionChanged(Person? selectedPerson, CancellationToken ct)
{
if (selectedPerson == null)
return;
await GreetingForSelectedPerson.Set($"Hello {selectedPerson.FirstName} {selectedPerson.LastName}!", ct);
}
}
The ForEach
operator listens to a selection occurrence and invokes the SelectionChanged
callback with the newly available data, in this case, the recently selected Person
entity.
Tip
MVUX takes care of the lifetime of the subscription, so it will be disposed of along with its declaring Model
being garbage-collected.
On-demand using a Command parameter
Another option is using a Button
which when clicked, invokes a command which checks the current selection, this can be achieved via its parameters:
public ValueTask CheckSelection(Person selectedPerson)
{
// selectedPerson points to the recent selection
}
In the above example, since selectedPerson
has the same name as the SelectedPerson
feed, it will be automatically evaluated and provided as a parameter on the command execution.
Tip
This behavior can also be controlled using attributes. To learn more about commands and how they can be configured using attributes, refer to the Commands page.
Multi-item selection
The Selection
operator has another overload that enables selecting multiple items. An IListState<Person>
is needed for multi-selection instead of the IState<Person>
used above.
In the PeopleModel
, we'll modify the SelectedPerson
property to look like the following:
public IState<IImmutableList<Person>> SelectedPeople => State<IImmutableList<Person>>.Empty(this);
Then change .Selection(SelectedPerson)
to .Selection(SelectedPeople)
.
This is what's changed in the PeopleModel class:
public partial record PeopleModel(IPeopleService PeopleService)
{
public IListFeed<Person> People =>
ListFeed
.Async(PeopleService.GetPeopleAsync)
.Selection(SelectedPeople);
public IState<IImmutableList<Person>> SelectedPeople => State<IImmutableList<Person>>.Empty(this);
}
Head to the View and enable multi-selection in the ListView
by changing its SelectionMode
property to Multiple
.
Note
The source code for the sample app can be found here.
Manual selection
The options above explained how to subscribe to selection that has been requested in the View by a Selector control (i.e. ListView
).
If you want to manually select an item or multiple items, rather use a List-State instead of a List-Feed to load the items, so that you can update their selection state. You can then use the List-State's selection operators to manually select items.
Refer to the selection operators section in the List-State page for documentation on how to use manual selection.