简体   繁体   中英

Xamarin.Forms MVVM TapGestureRecognizer to a Label in a ViewCell of ListView (in partial files)

I've searched a lot about this issue and frankly, I am stack on that one. I have a chat application. On this app there is a view where there are messages both from Me and other Chat members. Technically speaking it is a ListView with ItemTemplate with has a class Binded (DataTemplateSelector) which returns ViewCells basing on the rule (whether the message to display is MINE or OTHERS)

The message (Inbound or Outbound) is in separate files.

Currently, TapGestureRecognizer is not working and the command ChooseNameToMentionCommand is not firing

There is a lot of "similar" questions where TapGestureRecognizer is not working on ListView like this one:

TapGestureRecognizer not working inside ListView

The answer there (and on any other correlated topic) for Command not working is:

  • use Source={x:Reference MessagesListView} on your Command binding

But when I use this suggestion I am ending with:

Xamarin.Forms.Xaml.XamlParseException: 'Position 30:21. Can not find the object referenced by MessagesListView'

How can i use this in my case (with ViewCell defined in separate file) Important note - i am using MVVM approach and don't want to do anything in codebehind of ViewCell (then i could even use Tapped event. I've tested that. Of course this approach works:) )

Here is my code:

MainViewModel.cs

public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public ObservableRangeCollection<MessageModel> Messages { get; set; }

    public Command ChooseNameToMentionCommand { get; set; }
    
    public string NewMessage {get; set;}

    public MainViewModel()
    {
        Messages = new ObservableRangeCollection<MessageModel>();
        ChooseNameToMentionCommand = new Command<string>(async (t) => await ChooseNameToMention(t));
    }

    private Task ChooseNameToMention(string name)
    {
        this.NewMessage += $"@{name}";
    }

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
}

MainPage.xaml

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="Chat.ClientLibrary.MainPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:converters="clr-namespace:Chat.ClientLibrary.Converters"
    xmlns:local="clr-namespace:Chat.ClientLibrary.CustomCells"
    xmlns:partials="clr-namespace:Chat.ClientLibrary.Views.Partials"
    BackgroundColor="White"
    x:Name="MainChatPage">
    
<ContentPage.Resources>
    <ResourceDictionary>            
        <local:MyDataTemplateSelector x:Key="MessageTemplateSelector"/>
    </ResourceDictionary>
</ContentPage.Resources>

    /* REMOVED UNNECESSARY code */
    <Grid RowSpacing="0" ColumnSpacing="0">
    <Grid.RowDefinitions>
        <RowDefinition Height="50" />
        <RowDefinition Height="*" />
        <RowDefinition Height="1" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
        <ListView   
            Grid.Row="1"
            FlowDirection="RightToLeft"
            Rotation="180"    
            x:Name="MessagesListView" 
            ItemTemplate="{StaticResource MessageTemplateSelector}" 
            ItemsSource="{Binding Messages}" 
            HasUnevenRows="True" 
            ItemSelected="MyListView_OnItemSelected" 
            ItemTapped="MyListView_OnItemTapped" 
            SeparatorVisibility="None" />
        /* REMOVED UNNECESSARY code */
    </Grid>
</ContentPage>

MyDataTemplateSelector.cs

class MyDataTemplateSelector : DataTemplateSelector
{
    public MyDataTemplateSelector()
    {
        // Retain instances!
        this.incomingDataTemplate = new DataTemplate(typeof(IncomingViewCell));
        this.outgoingDataTemplate = new DataTemplate(typeof(OutgoingViewCell));
    }

    protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
    {
        var messageVm = item as MessageModel;
        if (messageVm == null)
            return null;
        return messageVm.IsOwnMessage ? this.incomingDataTemplate : this.outgoingDataTemplate;
    }

    private readonly DataTemplate incomingDataTemplate;
    private readonly DataTemplate outgoingDataTemplate;
}

IncomingViewCell.xaml (i will not post OutgoingViewCell - it is almost the same;) Different colors)

<?xml version="1.0" encoding="utf-8" ?>
<ViewCell xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Chat.ClientLibrary.Views.CustomCells.IncomingViewCell"
             xmlns:forms9patch="clr-namespace:Forms9Patch;assembly=Forms9Patch">
    <Grid ColumnSpacing="2"
          Padding="5"
          FlowDirection="LeftToRight"
          Rotation="180"
          >
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="40"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="40"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="Auto"></RowDefinition>
        </Grid.RowDefinitions>
        <Label Grid.Row="0"  Grid.Column="1" HorizontalTextAlignment="Start"  Text="{Binding UserName}" TextColor="Blue">
            <Label.GestureRecognizers>
                <TapGestureRecognizer 
                    Command="{Binding Path= BindingContext.ChooseNameToMentionCommand, Source={x:Reference MessagesListView}}" CommandParameter="{Binding UserName}" />
            </Label.GestureRecognizers>
        </Label>
        /* REMOVED UNNECESSARY code */
    </Grid>
</ViewCell>

[EDIT 12:12 01.10.2020] I forgot to put here MainPage.cs so here it is:

MainPage.cs

public partial class MainPage : ContentPage
{
    MainViewModel vm;

    public MainPage()
    {
        this.BindingContext = vm = new MainViewModel();

        InitializeComponent();
    }

    void MyListView_OnItemSelected(object sender, SelectedItemChangedEventArgs e)
    {
        MessagesListView.SelectedItem = null;
    }

    void MyListView_OnItemTapped(object sender, ItemTappedEventArgs e)
    {
        MessagesListView.SelectedItem = null;

    }
}

There is for the ListOnItemTapped event added (i forgot about it - because it was taken from some chat example. But i've don't suppose it breaks anything. When i've added OnTapped for Label directly - it did work.

Due to lack of the code, i make a similar sample for your reference to use TapGestureRecognizer in ListView ViewCell.

Xaml:

<ContentPage
x:Class="Selector.HomePage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Selector"
x:Name="MainPage">
<ContentPage.Resources>
    <ResourceDictionary>
        <DataTemplate x:Key="validPersonTemplate">
            <ViewCell>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="0.4*" />
                        <ColumnDefinition Width="0.3*" />
                        <ColumnDefinition Width="0.3*" />
                    </Grid.ColumnDefinitions>
                    <Label
                        FontAttributes="Bold"
                        Text="{Binding Name}"
                        TextColor="Green">
                        <Label.GestureRecognizers>
                            <TapGestureRecognizer Command="{Binding Path=BindingContext.TapCommand, Source={x:Reference MainPage}}" CommandParameter="false" />
                        </Label.GestureRecognizers>
                    </Label>
                    <Label
                        Grid.Column="1"
                        Text="{Binding DateOfBirth, StringFormat='{0:d}'}"
                        TextColor="Green" />
                    <Label
                        Grid.Column="2"
                        HorizontalTextAlignment="End"
                        Text="{Binding Location}"
                        TextColor="Green" />
                </Grid>
            </ViewCell>
        </DataTemplate>
        <DataTemplate x:Key="invalidPersonTemplate">
            <ViewCell>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="0.4*" />
                        <ColumnDefinition Width="0.3*" />
                        <ColumnDefinition Width="0.3*" />
                    </Grid.ColumnDefinitions>
                    <Label
                        FontAttributes="Bold"
                        Text="{Binding Name}"
                        TextColor="Red">
                        <Label.GestureRecognizers>
                            <TapGestureRecognizer Command="{Binding Path=BindingContext.TapCommand, Source={x:Reference MainPage}}" CommandParameter="false" />
                        </Label.GestureRecognizers>
                    </Label>
                    <Label
                        Grid.Column="1"
                        Text="{Binding DateOfBirth, StringFormat='{0:d}'}"
                        TextColor="Red" />
                    <Label
                        Grid.Column="2"
                        HorizontalTextAlignment="End"
                        Text="{Binding Location}"
                        TextColor="Red" />
                </Grid>
            </ViewCell>
        </DataTemplate>
        <local:PersonDataTemplateSelector
            x:Key="personDataTemplateSelector"
            InvalidTemplate="{StaticResource invalidPersonTemplate}"
            ValidTemplate="{StaticResource validPersonTemplate}" />
    </ResourceDictionary>
</ContentPage.Resources>
<StackLayout Margin="20">
    <Label
        FontAttributes="Bold"
        HorizontalOptions="Center"
        Text="ListView with a DataTemplateSelector" />
    <ListView
        x:Name="listView"
        Margin="0,20,0,0"
        ItemTemplate="{StaticResource personDataTemplateSelector}" />
</StackLayout>
</ContentPage>

PersonDataTemplateSelector.cs:

public class PersonDataTemplateSelector : DataTemplateSelector
{
    public DataTemplate ValidTemplate { get; set; }

    public DataTemplate InvalidTemplate { get; set; }

    protected override DataTemplate OnSelectTemplate (object item, BindableObject container)
    {
        return ((Person)item).DateOfBirth.Year >= 1980 ? ValidTemplate : InvalidTemplate;
    }
}

Person.cs:

 public class Person
{
    public string Name { get; set; }
    public DateTime DateOfBirth { get; set; }
    public string Location { get; set; }
}

Code Behind:

 public Command TapCommand
    {
        get
        {
            return new Command(val =>
            {
                DisplayAlert("Alert", val.ToString(), "OK");
            });
        }
        
    }
    public HomePage()
    {
        InitializeComponent();

        var people = new List<Person>
        {
            new Person { Name = "Kath", DateOfBirth = new DateTime(1985, 11, 20), Location = "France" },
            new Person { Name = "Steve", DateOfBirth = new DateTime(1975, 1, 15), Location = "USA" },
            new Person { Name = "Lucas", DateOfBirth = new DateTime(1988, 2, 5), Location = "Germany" },
            new Person { Name = "John", DateOfBirth = new DateTime(1976, 2, 20), Location = "USA" },
            new Person { Name = "Tariq", DateOfBirth = new DateTime(1987, 1, 10), Location = "UK" },
            new Person { Name = "Jane", DateOfBirth = new DateTime(1982, 8, 30), Location = "USA" },
            new Person { Name = "Tom", DateOfBirth = new DateTime(1977, 3, 10), Location = "UK" }
        };

        listView.ItemsSource = people;
        this.BindingContext = this;
    }

Screenshot:

在此处输入图片说明

Updated:

Use resource dictionaries to create the separate files. I change the binding path for the command in these two files.

MyResourceDictionary.xaml:

<?xml version="1.0" encoding="UTF-8" ?>
<ResourceDictionary
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Selector">

<DataTemplate x:Key="validPersonTemplate">
    <ViewCell x:Name="MyCell">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="0.4*" />
                <ColumnDefinition Width="0.3*" />
                <ColumnDefinition Width="0.3*" />
            </Grid.ColumnDefinitions>
            <Label
                FontAttributes="Bold"
                Text="{Binding Name}"
                TextColor="Green">
                <Label.GestureRecognizers>
                    <TapGestureRecognizer Command="{Binding Path=Parent.BindingContext.TapCommand, Source={x:Reference MyCell}}" CommandParameter="false" />
                </Label.GestureRecognizers>
            </Label>
            <Label
                Grid.Column="1"
                Text="{Binding DateOfBirth, StringFormat='{0:d}'}"
                TextColor="Green" />
            <Label
                Grid.Column="2"
                HorizontalTextAlignment="End"
                Text="{Binding Location}"
                TextColor="Green" />
        </Grid>
    </ViewCell>
</DataTemplate>

</ResourceDictionary>

MyResourceDictionary2.xaml:

<?xml version="1.0" encoding="UTF-8" ?>
<ResourceDictionary xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">

<DataTemplate x:Key="invalidPersonTemplate">
    <ViewCell x:Name="MyCell2">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="0.4*" />
                <ColumnDefinition Width="0.3*" />
                <ColumnDefinition Width="0.3*" />
            </Grid.ColumnDefinitions>
            <Label
                FontAttributes="Bold"
                Text="{Binding Name}"
                TextColor="Red">
                <Label.GestureRecognizers>
                    <TapGestureRecognizer Command="{Binding Path=Parent.BindingContext.TapCommand, Source={x:Reference MyCell2}}" CommandParameter="false" />
                </Label.GestureRecognizers>
            </Label>
            <Label
                Grid.Column="1"
                Text="{Binding DateOfBirth, StringFormat='{0:d}'}"
                TextColor="Red" />
            <Label
                Grid.Column="2"
                HorizontalTextAlignment="End"
                Text="{Binding Location}"
                TextColor="Red" />
        </Grid>
    </ViewCell>
</DataTemplate>
</ResourceDictionary>

Change the ContentPage:

<?xml version="1.0" encoding="UTF-8" ?>
<ContentPage
x:Class="Selector.HomePage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Selector">
<ContentPage.Resources>
    <ResourceDictionary> 
        <ResourceDictionary Source="MyResourceDictionary.xaml" />
        <ResourceDictionary Source="MyResourceDictionary2.xaml" />

        <local:PersonDataTemplateSelector
            x:Key="personDataTemplateSelector"
            InvalidTemplate="{StaticResource invalidPersonTemplate}"
            ValidTemplate="{StaticResource validPersonTemplate}" />

    </ResourceDictionary>
</ContentPage.Resources>
<StackLayout Margin="20">
    <Label
        FontAttributes="Bold"
        HorizontalOptions="Center"
        Text="ListView with a DataTemplateSelector" />
    <ListView
        x:Name="listView"
        Margin="0,20,0,0"
        ItemTemplate="{StaticResource personDataTemplateSelector}" />
</StackLayout>
</ContentPage>

No Changes with Selector and viewmodel, please check it. If you still have questions for this issue, please feel free to let me know.

Add a class ViewModelLocator, I use MVVM Light

public class ViewModelLocator
{
   public ViewModelLocator()
   {
      ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

      SimpleIoc.Default.Register<MainViewModel>();
   }
   public MainViewModel MainVM
   {
      get { return ServiceLocator.Current.GetInstance<MainViewModel>(); }
   }
}

Then you can use BindingContext without using page's reference

BindingContext="{Binding Path=MainVM, Source={StaticResource VMLocator}}"

App.Xaml code

xmlns:vm="clr-namespace:xxx.xx.ViewModels"

<Application.Resources>
<vm:ViewModelLocator x:Key="VMLocator" />
</Application.Resources>

Usage

Option 1:

You want to bind a label in a listview but listview's binding context points to a collection use this.

<Label.GestureRecognizers>
       <TapGestureRecognizer Command="{Binding YourVM.YourCommand,Source={StaticResource VMLocator}}" CommandParameter="{Binding UserName}" />
</Label.GestureRecognizers>

Option 2 :

You want to bind it to the current page's Viewmodel (with page's reference)

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" 
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="xxxx.xx.xx.App"
             x:Name="MyViewName">

<Label Text="My Text" IsVisible="{Binding Path=BindingContext.IsLoading,Source={x:Reference MyViewName},Mode=TwoWay}"/>

Option2 :

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM