Home / News / Migrating ListView To CollectionView in Xamarin.Forms - Interactivity
Migrating ListView To CollectionView in Xamarin.Forms - Interactivity

Migrating ListView To CollectionView in Xamarin.Forms - Interactivity

Introduction

In part three of our Migrating ListView to CollectionView serieswe will look at handling user interaction in CollectionView's by adding cell tap, swipe context actions, and pull to refresh.

Tapping Cells

Tapping cells on ListView we used the ItemTapped event which I will omit for brevity because I assume everyone already knows how to use it. Because CollectionView has more features than ListView, it's a little different to get item tap setup. I see a lot of people confused over this one because they expected an ItemTappedCommand. The CollectionView has more flexible cell selection features, so even though there's no tap command directly, it's still easy to setup.

To tap a cell with CollectionView we use single selection mode along with the SelectionChangedCommand. Then we bind the SelectedItem to a property on our view model to use in our command.

<CollectionView
	SelectionMode="Single"
	SelectedItem="{Binding SelectedItem}"
	SelectionChangedCommand="{Binding ItemSelectedCommand}"
	ItemsSource="{Binding FilteredItems}" VerticalOptions="FillAndExpand">
                
                ...

</CollectionView>
public ItemViewModel SelectedItem { get; set; }

public ICommand ItemSelectedCommand
{
    get
    {
        return new Command(_ =>
        {
            if (SelectedItem == null)
                return;

            Acr.UserDialogs.UserDialogs.Instance.Alert("You selected " + SelectedItem.Text);
            SelectedItem = null;
        });
    }
}

We set SelectedItem = null to remove the cell highlight, which is common to do if you're using cell tap for master-detal navigation.

It's worth noting that at the time of this writing I noticed that SwipeView seems to interfere with the cell selection (or rather change it's behavior selecting a cell on tap).

See full documentation here.

Pull To Refresh

ListView

ListView pull to refresh is supported through the bindable properties IsPullToRefreshEnabled, RefreshCommand, and IsRefreshing. It looked like this:

<ListView Grid.Row="2"
	..
	IsRefreshing="{Binding IsRefreshing}"
	IsPullToRefreshEnabled="true"
	RefreshCommand="{Binding RefreshCommand}">

By enabling pull to refresh, binding to our ItemsViewModel.IsRefreshing property and ItemsViewModel.RefreshCommand, the ListView will set IsRefreshing = true and execute the RefreshCommand. It's up to us to display a refresh view driven from our view model properties. In our example we re-use the EmptyView.

<?xml version="1.0" encoding="utf-8"?>
<StackLayout Orientation="Vertical"
	 VerticalOptions="Center"
	 HorizontalOptions="Center"
	 xmlns="http://xamarin.com/schemas/2014/forms"
	 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
	 x:Class="MigratingListViewToCollectionView.Controls.EmptyView"
	 xmlns:local="clr-namespace:MigratingListViewToCollectionView">
	 
	<ActivityIndicator IsRunning="true"
   		IsVisible="{Binding IsRefreshing}"/>
   		
	<Label Text="Your search returned no results"
		VerticalTextAlignment="Center"
		HorizontalTextAlignment="Center">
		<Label.Triggers>
			<DataTrigger TargetType="Label"
		       Binding="{Binding IsRefreshing}"
		       Value="true">
				<Setter Property="Text"
				    Value="Refreshing..."/>
			</DataTrigger>
		</Label.Triggers>
	</Label>
	
</StackLayout>

Here we show the running ActivityIndicator during refresh, and use a Label.Trigger to display a refresh message to the user.

CollectionView

To support pull to refresh on CollectionView we wrap it in a RefreshView. The RefreshView is more flexible in that it works on any control as long as it's scrollable, meaning you could just as easily use it with a ScrollView that is displaying a page of data instead of a collection.

<RefreshView IsRefreshing="{Binding IsRefreshing}" Command="{Binding RefreshCommand}" Grid.Row="2">
    <CollectionView ItemsSource="{Binding FilteredItems}" VerticalOptions="FillAndExpand">
        ...
        <CollectionView.EmptyView>
            <ContentView>
                <controls:EmptyView/>
            </ContentView>
        </CollectionView.EmptyView>
    </CollectionView>
</RefreshView>

The control will set IsRefreshing = true and execute the RefreshCommand when the user pulls to refresh. Like before, we re-use the EmptyView to display a loading indicator and message. This time, however, it's contained in our CollectionView.EmptyView property which is automatically shown during refresh. We can see in our ItemsViewModel.RefreshCommand that we set our Items = null which causes the EmptyView to be displayed.

public ICommand RefreshCommand
{
    get
    {
        return new Command(async () =>
        {
            try
            {
                Items = null;

                await Task.Delay(5000); // Only to demonstrate refresh views..

                UpdateItems();

                Acr.UserDialogs.UserDialogs.Instance.Toast("Items refreshed");
            }
            finally
            {
                IsRefreshing = false;
            }
        });
    }
}

See full documentation here.

Cell Context Actions

Swipe context actions on ListView cells were supports through an IList of ViewCell.ContextActions like so:

<ListView.ItemTemplate>
    <DataTemplate>
        <ViewCell>
            <ViewCell.ContextActions>
                <MenuItem Text="⭐️" Command="{Binding ItemFavourited}"/>
            </ViewCell.ContextActions>
            <controls:ItemView/>
        </ViewCell>
    </DataTemplate>
</ListView.ItemTemplate>

Here we have one MenuItem displaying "⭐️" and executing our ItemViewModel.ItemFavourited command when tapped. To migrate this to CollectionView we have to embed our ItemView in a SwipeView container:

<?xml version="1.0" encoding="utf-8"?>
<SwipeView xmlns="http://xamarin.com/schemas/2014/forms"
           xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
           x:Class="MigratingListViewToCollectionView.Controls.InteractiveItemView"
           x:Name="self"
           xmlns:controls="clr-namespace:MigratingListViewToCollectionView.Controls">
    <SwipeView.RightItems>
        <SwipeItems Mode="Reveal">
            <SwipeItemView Command="{Binding ItemFavourited}">
                <Label Padding="0,0,0,20"
                   Text="⭐️"
                   VerticalTextAlignment="Center"
                   HorizontalTextAlignment="End"
                   FontSize="48"
                   WidthRequest="300"/>
            </SwipeItemView>
        </SwipeItems>
    </SwipeView.RightItems>
    <controls:ItemView>
        <controls:ItemView.GestureRecognizers>
            <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
        </controls:ItemView.GestureRecognizers>
    </controls:ItemView>
</SwipeView>

The SwipeView is highly customizable. Here we implement our ListView feature by setting some right swipe items. When our Mode="Reveal" the items are revealed on swipe. The user has to then tap on the item to execute the bound ItemViewModel.ItemFavourited command. If Mode="Execute" the command is executed on swipe. Then by default the drawer closes after execution, but it can be customized to stay open. We can also supply our own layout to SwipeItemView as we do here, which is very nice.

The combination of items, directions, execution modes, and swipe behaviors make this a powerful control. See full documentation here

Summary

CollectionView interactivity is more flexible than ListView. Using the new APIs makes it easy to add context actions, pull to refresh, and single or multi-select.

About The Author

Derek is a certified Xamarin developer hailing from Montreal. For 5+ years he's had the opportunity to build enterprise and consumer-facing mobile apps with Xamarin.

https://github.com/winnicki
https://www.linkedin.com/in/link-to-derek/

0 comments

Leave a comment