Friday, August 28, 2015

Extending the ListView to make it clickable

The problem

The ListView is a fantastic control to use in Xamarin Forms. It has one big drawback, however and that is that if you navigate away from a selected item in the list and then navigate back, the same item is still selected and nothing happens when you tap it again.

The solution

We want to add a new command to the ListView and we start by adding a new class that inherits from the original ListView control. I usually create a folder called "Controls" in the Forms PCL and put stuff I want to use in the XAML in there.

   public class ListView : Xamarin.Forms.ListView
    {
        public static BindableProperty ItemClickCommandProperty = 
            BindableProperty.Create<ListView, ICommand>(x => x.ItemClickCommand, null);

        public ListView()
        {
            this.ItemTapped += this.OnItemTapped;
        }


        public ICommand ItemClickCommand
        {
            get { return (ICommand)this.GetValue(ItemClickCommandProperty); }
            set { this.SetValue(ItemClickCommandProperty, value); }
        }


        private void OnItemTapped(object sender, ItemTappedEventArgs e)
        {
            if (e.Item != null && this.ItemClickCommand != null && this.ItemClickCommand.CanExecute(e))
            {
                this.ItemClickCommand.Execute(e.Item);
            }

            this.SelectedItem = null;
        }
    }


The key here is the ItemClickCommand property. This will be the command that gets executed when we tap on an item. We also listen to the OnItemTapped event and the magic goes here by simply setting the SelectedItem to null again, allowing for the item to be tapped again.

Enter a ViewModel

Since MVVM is the way to go we need a view model. We are omitting the INotifyPropertyChanged stuff to keep it simple. What we have is an ObservableCollection of Duck objects that simple has a property called Name. We also define a command where we put the code that we want to execute when an item is tapped. In this case we navigate to a new page that simply displays the name of the duck. In the real world you should use IoC and also create a view model for the Duck view.

    // INotifyPropertyChanged implementation omitted - use fody!
    public class MainViewModel
    {
        public MainViewModel()
        {
            Ducks = new ObservableCollection<Duck>()
            {
                    new Duck() { Name = "Bob" },
                    new Duck() { Name = "Tommy" },
                    new Duck() { Name = "Donald" }
            };
        }

        public INavigation Navigation { get; set; }

        public ObservableCollection<Duck> Ducks
        {
            get;
            set;
        }

        public Command<Duck> DuckSelected
        {
            get
            {
                return new Command<Duck>(async (d) =>
                    {
                        var duckView = new DuckView();
                        duckView.LoadData(d);
                        await Navigation.PushAsync(duckView);
                    });
            }
        }

    }

    public class Duck
    {
        public string Name { get; set; }
    }

And the View 

This binds it all together. We assign the source of items from our view model as well as the command that we want to execute when an item is clicked.

xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:FancyList.Controls;assembly=FancyList"
             x:Class="FancyList.MainView" Padding="20">
    <ContentPage.Content>
        <local:ListView ItemsSource="{Binding Ducks}" ItemClickCommand="{Binding DuckSelected}">
            <ListView.ItemTemplate>
                <DataTemplate>
                      <ViewCell>
                        <ViewCell.View>
                              <Label Text="{Binding Name}" />
                          </ViewCell.View>
                      </ViewCell>
                </DataTemplate>
             </ListView.ItemTemplate>     
        </local:ListView>

    </ContentPage.Content>
</ContentPage>

Code behind

We also need to set the BindingContext in the code behind for the view. There are several navigation patterns out there and in this sample we take the easy path by simply passing in the Navigation object from the page (view) itself.

    public partial class MainView : ContentPage
    {
        public MainView()
        {
            InitializeComponent();

            var viewModel = new MainViewModel();
            viewModel.Navigation = this.Navigation;

            BindingContext = viewModel;
        }
    }

Summary

It's easy to extend controls in Xamarin Forms. This sample does however omit a lot of good practices like IoC and Fody. 

Resources

No comments:

Post a Comment