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

4 comments:

  1. To fix the problem you mentioned, you can just set

    SelectedItem = null;

    http://developer.xamarin.com/guides/cross-platform/xamarin-forms/user-interface/listview/interactivity/

    ReplyDelete
    Replies
    1. Yes, you are definitely correct and that's what I'm doing in the custom control. The problem is to do that you need a reference to the control from code behind and that would break the MVVM pattern I hold so dear. It's also not very reusable since you need to do that for every list view that you'd like that behavior from. I'll adjust my post to make sure to point out that this is the MVVM way to do it and it also demonstrates how to extend controls.

      That said, thanks for the comment and for reading the post!

      Best regards

      Delete
  2. Hello,

    I created a ListView this way, to hook up the ItemTapped event to items populated in the ListView. I want to navigate to another page, when the user selects an item in the ListView. However, I am getting an exception when I attempt to implement the Navigation.PushAsync method. I am debugging an IOS app and the exception is thrown in the Main.cs method of the IOS project, when it hits the UIApplication.Main(args,null,"AppDelegate") method. My ViewModel inherits from XLabs.Forms.Mvvm.ViewModel. Any idea why this would be happening? I copied the exception below:
    System.NullReferenceException: Object reference not set to an instance of an object
    at OptSt.ViewModels.SearchPagesViewModel+<>c__async2.MoveNext () [0x00038] in /Users/lw/Projects/OptSt/OptSt/ViewModels/SearchPagesViewModel.cs:92
    at --- End of stack trace from previous location where exception was thrown ---
    at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000b] in /Users/builder/data/lanes/2077/d8e9592a/source/maccore/_build/Library/Frameworks/Xamarin.iOS.framework/Versions/git/src/mono/mcs/class/corlib/System.Runtime.ExceptionServices/ExceptionDispatchInfo.cs:61
    at System.Runtime.CompilerServices.AsyncMethodBuilderCore.m__0 (System.Object state) [0x00000] in /Users/builder/data/lanes/2077/d8e9592a/source/maccore/_build/Library/Frameworks/Xamarin.iOS.framework/Versions/git/src/mono/external/referencesource/mscorlib/system/runtime/compilerservices/AsyncMethodBuilder.cs:1006
    at UIKit.UIKitSynchronizationContext+c__AnonStorey0.<>m__0 () [0x00000] in /Users/builder/data/lanes/2077/d8e9592a/source/maccore/src/UIKit/UIKitSynchronizationContext.cs:24
    at Foundation.NSAsyncActionDispatcher.Apply () [0x00000] in /Users/builder/data/lanes/2077/d8e9592a/source/maccore/src/Foundation/NSAction.cs:163
    at at (wrapper managed-to-native) UIKit.UIApplication:UIApplicationMain (int,string[],intptr,intptr)
    at UIKit.UIApplication.Main (System.String[] args, IntPtr principal, IntPtr delegate) [0x00005] in /Users/builder/data/lanes/2077/d8e9592a/source/maccore/src/UIKit/UIApplication.cs:74
    at UIKit.UIApplication.Main (System.String[] args, System.String principalClassName, System.String delegateClassName) [0x00038] in /Users/builder/data/lanes/2077/d8e9592a/source/maccore/src/UIKit/UIApplication.cs:58
    at OptSt.iOS.Application.Main (System.String[] args) [0x00008] in /Users/lw/Projects/OptSt/iOS/Main.cs:17

    ReplyDelete
    Replies
    1. Hard to say without knowing how the project is set up? Are you using MVVM? Navigating from a ViewModel, if so is the Navigation property set on the view model? Looks like MVVM, so check that the Navigation property is set.

      Delete