1

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.

Piotr
  • 1,155
  • 12
  • 29
  • post a link to sample git repo to look into – Shubham Tyagi Oct 01 '20 at 17:02
  • It is in private repo only. I've posted most sigificant part of the code already. – Piotr Oct 02 '20 at 06:38
  • you can make a sample repo , im not able to get the issue from here – Shubham Tyagi Oct 02 '20 at 07:42
  • @ShubhamTyagi - the issue is exactly coming from the response bellow (of Wendy Zang - MSFT) In the state, that he posted it - IT IS WORKING. but if you copy-paste those templates to separate files (because for example you will have hundreds of those and you don't want to create mess in main xaml) then it is NOT WORKING - giving message: "Xamarin.Forms.Xaml.XamlParseException: 'Position 30:21. Can not find the object referenced by MessagesListView'" – Piotr Oct 02 '20 at 14:05

2 Answers2

1

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:

enter image description here

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.

Wendy Zang - MSFT
  • 10,509
  • 1
  • 7
  • 17
  • great. Now copy paste those `validPersonTemplate` and `invalidPersonTemplate` to seprate files (as partials) and try again :) – Piotr Oct 02 '20 at 14:01
  • Any update? Have you solve this issue? If you still have questions about this, please feel free to let me know. – Wendy Zang - MSFT Oct 05 '20 at 09:23
  • i did write you. Your solution is only working if you have both DataTemplates in the same XAML. I did write you. Try removing those and moving them to separate files. I did post how my DataTemplateSelector looks like. Try this approach and you will see, that your anwser fails. – Piotr Oct 05 '20 at 20:53
  • We could do that with `Stand-alone resource dictionaries`. And do some change with the Binding. I would update my reply. – Wendy Zang - MSFT Oct 07 '20 at 08:11
  • I have updated my reply. Please check it. If you needed, i could upload my project on github. – Wendy Zang - MSFT Oct 07 '20 at 08:18
  • Hi @piotr, I am running into the same issue. Did you happen to solve it while keeping the datatemplates in separate files? – Ed S Jul 08 '22 at 09:03
0

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 :

Shubham Tyagi
  • 798
  • 6
  • 21
  • I can try this approach, but for me it feels like using ViewModelLocator is an anti-pattern. Maybe just because ServiceLocator is well-known anti-pattern. But for now there are no other alternatives, so i willl try that;) – Piotr Oct 05 '20 at 20:59
  • @Piotr VM locator is not anti pattern , it introduces singleton check this link for more details https://stackoverflow.com/questions/14130327/viewmodels-in-viewmodellocator-mvvm-light – Shubham Tyagi Oct 06 '20 at 07:18
  • Ok, but one more question. Where do i set this BindingContext? On the partial: `IncomingViewCell.xaml` in ViewCell element? If so, it will not work, because i am using Text="{Binding UserName}" which is taken from Message. And it is one element of ListView binded: ItemsSource="{Binding Messages}" If i understand correctly - after changing BindingContext for ViewCell it will look for UserName property on MainViewModel (and there is no such property) – Piotr Oct 06 '20 at 08:32
  • @Piotr I've updated the answer, please upvote/accept if it fulfills your query – Shubham Tyagi Oct 06 '20 at 09:20
  • And `ServiceLocator` --> should be in which nuget package/lib? – Piotr Oct 06 '20 at 18:52
  • My app is on .NET Core. If i understood correctly - class `SimpleIoC` comes from `MvvmLightLibs ` And it is not for .NET Core. And i get a warning: "Package 'MvvmLightLibs 5.4.1.1' was restored using '.NETFramework,Version=v4.6.1, .NETFramework,Version=v4.6.2, .NETFramework,Version=v4.7, .NETFramework,Version=v4.7.1, .NETFramework,Version=v4.7.2, .NETFramework,Version=v4.8' instead of the project target framework '.NETStandard,Version=v2.1'. This package may not be fully compatible with your project." But i suppose i can use any DI solution (like Autofac) – Piotr Oct 06 '20 at 18:57
  • https://www.nuget.org/packages/CommonServiceLocator @Piotr – Shubham Tyagi Oct 06 '20 at 19:20
  • Yes, but in order for it to work, the whole app has to use DI, otherwise it will not work. I am currently creating a model for MainView as `this.BindingContext = vm = new MainViewModel();` I've posted my code:) So how it suppose to return an instance of MainViewModel in `ViewModelLocator` if it has not been registered by DI? – Piotr Oct 06 '20 at 21:46