ListView with MVVM - TapGestureRecognizer not working

DevologyDevology GBMember ✭✭✭

Hi,

I'm trying to go the MVVM route and avoid doing stuff directly in the code-behind, I'm finding that trying to detect a row being clicked to invoke a command painful to implement. It would seem that there's no ItemSelected that takes a Command (but it's okay if I wanted to invoke an action in my code-behind), which is a shame, so I've tried to use the TapGestureRecognizer with no success...

XAML...

        <ListView 
            CachingStrategy="RecycleElement"
            ItemsSource="{Binding FilteredMessages}" 
            HasUnevenRows = "true"
            SelectedItem = "{Binding SelectedMessage}">


            <ListView.ItemTemplate>
              <DataTemplate>
                <ViewCell>
                    <StackLayout Orientation="Vertical">
                        <StackLayout Orientation="Horizontal">
                            <Label Text="{Binding Scheduled}"/>
                        </StackLayout>
                        <StackLayout Orientation="Horizontal">
                            <Label Text="{Binding Message}"/>
                            <Label.GestureRecognizers>
                                <TapGestureRecognizer Command="{Binding TapCommand}"></TapGestureRecognizer>
                            </Label.GestureRecognizers>
                        </StackLayout>
                    </StackLayout>
                </ViewCell>
              </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>

The View Model...

    public ContactDetailViewModel()
    {
    ...
        TapCommand = new Command(HandleAction);
    }


    void HandleAction (object obj)
    {
        Console.WriteLine("***** DOESNT GET CALLED :-( *****");
    }

I've tried to put the GestureRecognizers at different layers in the xaml (i.e. at the StackLayout, ViewCell and ListView), but none invoke my callback method, but it would appear that it's capturing the event, because the cell doesn't go grey; which is the default behaviour when I click the cell.

Any ideas what I'm overlooking here?

I could fallback to using an ItemSelected in the code-behind and delegate a call through to the ViewModel, but that's not clean.

I appreciate any insights.

Thanks,
Rob.

Posts

  • adamkempadamkemp USInsider, Developer Group Leader mod

    Rather than using a tap gesture you can use an event handler to forward the ItemSelected event to a command. I have an example here.

  • DevologyDevology GBMember ✭✭✭

    Thanks for the feedback Adam, but I've seen plenty of posts that alude that this should work, so I'd rather find a way that doesn't use the code behind if at all possible, however, to unblock what I'm working on then this does help.

    The fact it changes the behaviour of the cell not being selected then it looks like it's meant to be working and might be a bug, but I'm just not sure.

    It seems otherwise that it's a big oversight that buttons etc can have a command associated but not a list view with a custom view cell; yet this must be a very common use case.

  • NMackayNMackay GBInsider, University mod
    edited December 2015

    @Devology

    You can do it using Xamarin.Forms.Behaviours nuget package but a word of warning, it doesn't work with XamlC as I found out earlier.

    <ListView Grid.Row="1"
                  ItemsSource="{Binding Orders}"
                  VerticalOptions="Fill"
                  x:Name="ListviewOrder">
          <ListView.ItemTemplate>
            <DataTemplate>
              <ImageCell
                  ImageSource="MyImage"
                  Text="{Binding OrderName}"
                  Detail="{Binding OrderNo}">
                <b:Interaction.Behaviors>
                  <b:BehaviorCollection>
                    <b:EventToCommand CommandNameContext="{b:RelativeContext Details}"
                                      EventName="Tapped"
                                      CommandName="SelectCommand"
                                      CommandParameter="{Binding .}" />
                  </b:BehaviorCollection>
                </b:Interaction.Behaviors>
              </ImageCell>
            </DataTemplate>
          </ListView.ItemTemplate>
        </ListView>
    

    If you want to use XamlC you'd need to do codeBehind....

    ListView Grid.Row="1"
                      ItemsSource="{Binding Orders}"
                      VerticalOptions="Fill"
                      ItemTapped="ListviewOrder_OnItemTapped"
                      x:Name="ListviewOrder">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <ImageCell
                            ImageSource="MyImage"
                            Text="{Binding OrderName}"
                            Detail="{Binding OrderNo}">
                        </ImageCell>
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
    
    
    private void ListviewOrder_OnItemTapped(object sender, ItemTappedEventArgs e)
            {
                ((StartViewModel)BindingContext).SelectCommand.Execute(e.Item as Order);
            }
    

    I wish EventToCommand existed in the Forms framework.

  • NMackayNMackay GBInsider, University mod
  • JulienRosenJulienRosen CAMember ✭✭✭✭
    edited December 2015

    you already have SelectedItem = "{Binding SelectedMessage}" in your ListView xaml declaration.

    You can monitor SelectedMessage for changes and fire any events you need.

    With Fody, you simply create a method called SelectedMessageChanged and voila, good to go. With stock c#, you could perform whatever you need in the set of SelectedMessage

  • adamkempadamkemp USInsider, Developer Group Leader mod

    Using a tap gesture recognizer on a cell in a list view is not a good idea. Anyone telling you otherwise is probably hacking around something they don't understand. You really don't want to go down that route.

    It seems otherwise that it's a big oversight that buttons etc can have a command associated but not a list view with a custom view cell; yet this must be a very common use case.

    I agree with this. ListView should have a command for the item selected event. That's why I added it in my example. Either that or the behaviors suggestion from Norman is probably the best thing to do.

  • Beachside_JasonBeachside_Jason USMember ✭✭

    @adamkemp said:
    Using a tap gesture recognizer on a cell in a list view is not a good idea. Anyone telling you otherwise is probably hacking around something they don't understand. You really don't want to go down that route.

    Adam, am I understanding you right here? Are you saying that if I have a ListView that has several images in each ViewCell and I want the user to be able to click on one of those images and then I want to invoke a Command for that specific image (say full-screen view the clicked/tapped image), is that a supported use-case with XAML alone?

  • adamkempadamkemp USInsider, Developer Group Leader mod

    I'm not saying never use a tap gesture anywhere in a list view, but don't use one for the whole row to mimic the row selection behavior that is already built in.

  • Beachside_JasonBeachside_Jason USMember ✭✭
    edited February 2016

    Holy moly I was ready to flip...all of my troubles were because this was missing this (I declared the variable, not the getter):
    public Command ViewPicFullScreenCommand { get; };

    Thank you Gents, sorry for the trouble. For anyone else that comes along here is a succinct bit of XAML and MVVM for this case that works, and here is the final product before the code - also attached (in case this link dies):
    image

    public class App : Application
    {
        public App ()
        {
            MainPage = new ListViewPage () {
                BindingContext = new MainViewModel ()
            };
        }
    }
    

    ListViewPage.xaml:

    <?xml version="1.0" encoding="UTF-8"?>
    <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="ListViewCommand.ListViewPage">
        <ContentPage.Padding>
            <OnPlatform x:TypeArguments="Thickness" iOS="0,20,0,0" />
        </ContentPage.Padding>
        <ListView ItemsSource="{Binding Pics}" HasUnevenRows="true" BackgroundColor="Transparent" SeparatorVisibility="None">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <ViewCell>
                        <StackLayout Orientation="Horizontal">
                            <Label Text="{Binding Name}"/>
                            <Image Source="{Binding Pic}" HeightRequest="150" VerticalOptions="CenterAndExpand">
                                <Image.GestureRecognizers>
                                    <TapGestureRecognizer Command="{Binding MakePicFullScreenCommand}"/>
                                </Image.GestureRecognizers>
                            </Image>
                            <Label IsVisible="{Binding Success}" Text="You Win!!!!"/>
                        </StackLayout>
                    </ViewCell>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </ContentPage>
    

    MainViewModel.cs:

    public class MainViewModel
    {
        public ObservableCollection<ListItemViewModel> Pics {get;} = new ObservableCollection<ListItemViewModel>(){
            new ListItemViewModel("Adam", "https://us.v-cdn.net/5019960/uploads/userpics/678/nR9POTS6S1BDR.jpg"),
            new ListItemViewModel("James", "https://us.v-cdn.net/5019960/uploads/userpics/503/n7NBAVSU0EEUQ.jpg"),
        };
    }
    

    ListItemViewModel.cs:

    [ImplementPropertyChanged]
    public class ListItemViewModel
    {
        public string Name { get; }
    
        public ImageSource Pic { get; }
    
        public Command MakePicFullScreenCommand { get; }
    
        public bool Success { get; private set; } = false;
    
        public ListItemViewModel (string name, string picUrl)
        {
            Name = name;
            Pic = ImageSource.FromUri (new Uri (picUrl));
            MakePicFullScreenCommand = new Command (new Action (() => {
                Success = true;
                Debug.WriteLine ("Yay");
            }));
        }
    }
    

    If you have any questions about Fody's 'ImplementPropertyChanged' (I don't like writing boilerplate code...who does), see this.

  • khanzzirfankhanzzirfan NZMember ✭✭

    @JamesMontemagno Thank you, that solved my tap gesture inside the list view, which is inside the Tabbed page data template.

  • alexq.oliveiraalexq.oliveira BRMember

    @JamesMontemagno said:
    You can, but the problem here is that you are binding to the Command that is in your ViewModel, not on the actual it on the list. You can add the command to your Model or do a reference binding to the page:

    give a x:Name="PageName" in your content page and then do:


    This saved my day! thank you!

  • marcnegrimarcnegri CHMember ✭✭

    I have the same issue, the difference is : I have a listview on MainPage and this listview use a DataTemplateSelector in order to select two types of ViewCell. I succeed in binding Field, but I can't launch Command with GestureRecognizer...

    Do I have to implement a CustomViewCellViewModel ? And bind a list of CustomViewCellViewModel on my MainPageViewModel ?

    Many thanks for your answers

  • IamAmitSrivastavIamAmitSrivastav USMember ✭✭

    @JamesMontemagno
    Hi i have a same issue but the situation is like i have a complex view cell so i put it into c# file not in xaml so how can i do this.

  • IamAmitSrivastavIamAmitSrivastav USMember ✭✭

    Hi i want this one to be converted into c#.

  • ZoliZoli NLMember ✭✭✭

    @JamesMontemagno said:

                            <Label.GestureRecognizers>
                                <TapGestureRecognizer Command="{Binding Path=BindingContext.TapCommand, Source={x:Reference PageName}}"/>
                            </Label.GestureRecognizers>
    

    ```

    Adding GestureRecognizers to a control in your viewcell stops the ItemSelected event calling (in 3.0.0_pre) on the listview if you click on other controls.

  • _Jai_Jai USMember ✭✭

    @JamesMontemagno said:
    You can, but the problem here is that you are binding to the Command that is in your ViewModel, not on the actual it on the list. You can add the command to your Model or do a reference binding to the page:

    give a x:Name="PageName" in your content page and then do:


    thank you for the solution.

  • Liêm_NguyễnLiêm_Nguyễn USMember ✭✭✭✭

    @JamesMontemagno said:
    You can, but the problem here is that you are binding to the Command that is in your ViewModel, not on the actual it on the list. You can add the command to your Model or do a reference binding to the page:

    give a x:Name="PageName" in your content page and then do:


    How to use your way when ItemTemplate is another XAML file (Custom ViewCell), not in the same file with ContentPage!
    Thank!

Sign In or Register to comment.