Xamarin.Forms MVVM TapGestureRecognizer 到 ListView 的 ViewCell 中的标签(在部分文件中)

Posted

技术标签:

【中文标题】Xamarin.Forms MVVM TapGestureRecognizer 到 ListView 的 ViewCell 中的标签(在部分文件中)【英文标题】:Xamarin.Forms MVVM TapGestureRecognizer to a Label in a ViewCell of ListView (in partial files) 【发布时间】:2021-01-16 23:14:18 【问题描述】:

我已经对这个问题进行了很多搜索,坦率地说,我对此很感兴趣。我有一个聊天应用程序。在这个应用程序上有一个视图,其中有来自我和其他聊天成员的消息。从技术上讲,它是一个带有 ItemTemplate 的 ListView,它有一个 Binded 类(DataTemplateSelector),它根据规则返回 ViewCells(无论要显示的消息是 MINE 还是 OTHERS)

消息(入站或出站)位于单独的文件中。

目前,TapGestureRecognizer 不工作,ChooseNameToMentionCommand 命令未触发

有很多“类似”的问题是 TapGestureRecognizer 不能像这样在 ListView 上工作:

TapGestureRecognizer not working inside ListView

那里(以及任何其他相关主题)命令不起作用的答案是:

在您的命令绑定上使用Source=x:Reference MessagesListView

但是当我使用这个建议时,我的结尾是:

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

我如何在我的情况下使用它(ViewCell 在单独的文件中定义)重要说明 - 我正在使用 MVVM 方法并且不想在 ViewCell 的代码隐藏中做任何事情(然后我什至可以使用 Tapped 事件。我已经测试过了。当然这种方法有效:) )

这是我的代码:

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(我不会发布 OutgoingViewCell - 它几乎是一样的;)不同的颜色)

<?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>

[编辑 12:12 01.10.2020] 我忘了把 MainPage.cs 放在这里,所以这里是:

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;

    

添加了 ListOnItemTapped 事件(我忘了它 - 因为它取自一些聊天示例。但我不认为它会破坏任何东西。当我直接为 Label 添加 OnTapped 时 - 它确实有效.

【问题讨论】:

发布示例 git repo 的链接以查看 它只在私人仓库中。我已经发布了代码中最重要的部分。 你可以做一个示例回购,我无法从这里得到问题 @ShubhamTyagi - 问题完全来自下面的响应(Wendy Zang - MSFT)在他发布的状态下 - 它正在工作。但是,如果您将这些模板复制粘贴到单独的文件中(例如,您将拥有数百个模板,并且您不想在主 xaml 中创建混乱),那么它不起作用 - 给出消息:“Xamarin.Forms.Xaml。 XamlParseException: '位置 30:21。找不到 MessagesListView 引用的对象'" 【参考方案1】:

由于缺少代码,我制作了一个类似的示例供您参考,以便在 ListView ViewCell 中使用TapGestureRecognizer

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; 

代码隐藏:

 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;
    

截图:

更新:

使用资源字典创建单独的文件。我在这两个文件中更改了命令的绑定路径。

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>

更改内容页面:

<?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>

选择器和视图模型没有变化,请检查。如果您对此问题仍有疑问,请随时告诉我。

【讨论】:

太棒了。现在复制粘贴那些validPersonTemplateinvalidPersonTemplate 到单独的文件(作为部分),然后再试一次:) 有什么更新吗?你解决了这个问题吗?如果您对此仍有疑问,请随时告诉我。 我确实给你写过。只有在同一个 XAML 中有两个 DataTemplates 时,您的解决方案才有效。我确实给你写过。尝试删除它们并将它们移动到单独的文件中。我确实发布了我的 DataTemplateSelector 的样子。试试这个方法,你会发现你的 anwser 失败了。 我们可以通过Stand-alone resource dictionaries 做到这一点。并用绑定做一些改变。我会更新我的回复。 我已经更新了我的回复。请检查一下。如果您需要,我可以将我的项目上传到 github。【参考方案2】:

添加一个类ViewModelLocator,我用的是MVVM Light

public class ViewModelLocator

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

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

那么你可以不使用页面的引用使用BindingContext

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

App.Xaml 代码

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

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

用法

选项 1:

您想在列表视图中绑定标签,但列表视图的绑定上下文指向集合使用 this。

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

选项 2:

你想将它绑定到当前页面的 Viewmodel(带有页面的引用)

<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"/>

选项2:

【讨论】:

我可以尝试这种方法,但对我来说,使用 ViewModelLocator 感觉是一种反模式。也许只是因为 ServiceLocator 是众所周知的反模式。但目前没有其他选择,所以我会尝试;) @Piotr VM 定位器不是反模式,它引入了单例检查此链接以获取更多详细信息***.com/questions/14130327/… 好的,但还有一个问题。我在哪里设置这个 BindingContext?部分:IncomingViewCell.xaml 在 ViewCell 元素中?如果是这样,它将不起作用,因为我使用的是从 Message 中获取的 Text="Binding UserName"。它是 ListView 绑定的一个元素: ItemsSource="Binding Messages" 如果我​​理解正确 - 在更改 ViewCell 的 BindingContext 后,它将在 MainViewModel 上查找 UserName 属性(并且没有这样的属性) @Piotr 我已经更新了答案,如果它满足您的查询,请点赞/接受 ServiceLocator --> 应该在哪个nuget包/lib中?

以上是关于Xamarin.Forms MVVM TapGestureRecognizer 到 ListView 的 ViewCell 中的标签(在部分文件中)的主要内容,如果未能解决你的问题,请参考以下文章

在 Xamarin.Forms 项目中实现 MVVM

Xamarin.Forms:如何避免在 MVVM 绑定中硬编码字符串

从按钮命令 xamarin.forms MVVM 获取 ListView

关于xamarin.forms在MVVM情况下如何DisplayActionSheet

在 Xamarin.Forms 中使用 MVVM 进行页面导航

Xamarin.Forms 条目 - 自定义行为和 MVVM