将点击事件绑定到模板化控件/模板控件中动态生成的按钮元素

Bind a click event to a dynamically generated button element in a templated control / template control

我的 UWP 应用程序中有一个包含 ListView 的模板化控件。 ListView 在运行时填充。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Renderer"
    xmlns:triggers="using:Microsoft.Toolkit.Uwp.UI.Triggers">
    <Style x:Key="RendererDefaultStyle" TargetType="local:Renderer" >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:Renderer">
                    <Grid>
                    ....
                        <ListView x:Name="AnnotsList" Margin="0,12,0,0" SelectionMode="None" Grid.Row="1" VerticalAlignment="Stretch" IsItemClickEnabled="True" Visibility="{Binding IsAnnotationsListOpen, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" ItemContainerStyle="{StaticResource AnnotationsListViewItemStyle}">
                            <ListView.ItemTemplate>
                                <DataTemplate>
                                    <Grid>
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition />
                                            <ColumnDefinition Width="Auto" />
                                        </Grid.ColumnDefinitions>
                                        <StackPanel Orientation="Vertical">
                                            <TextBlock Text="{Binding}" />
                                            <TextBlock Text="{Binding DisplayTitle}" Margin="20,0,0,10" FontSize="12" TextWrapping="WrapWholeWords" Visibility="Visible" />
                                        </StackPanel>
                                        <CommandBar Grid.Column="1">
                                            <CommandBar.SecondaryCommands>
                                                <AppBarElementContainer>
                                                    <StackPanel Orientation="Horizontal">
                                                        <Button x:Name="btn_RemoveFromList" DataContext="{Binding}">
                                                            <Button.Content>
                                                                <SymbolIcon Symbol="Delete" />
                                                            </Button.Content>
                                                            <ToolTipService.ToolTip>
                                                                <ToolTip Content="Delete" Placement="Mouse" />
                                                            </ToolTipService.ToolTip>
                                                        </Button>
                                                    </StackPanel>
                                                </AppBarElementContainer>
                                            </CommandBar.SecondaryCommands>
                                        </CommandBar>
                                    </Grid>
                                </DataTemplate>
                            </ListView.ItemTemplate>
                            <ListView.GroupStyle>
                                <GroupStyle >
                                    <GroupStyle.HeaderTemplate>
                                        <DataTemplate>
                                            <Border AutomationProperties.Name="{Binding Key}">
                                                <TextBlock Text="{Binding Key}" Style="{ThemeResource TitleTextBlockStyle}"/>
                                            </Border>
                                        </DataTemplate>
                                    </GroupStyle.HeaderTemplate>
                                </GroupStyle>
                            </ListView.GroupStyle>
                        </ListView>
                    ....
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <Style TargetType="local:Renderer" BasedOn="{StaticResource RendererDefaultStyle}"/>
</ResourceDictionary>

我试过像这样将点击事件绑定到按钮,但由于它是动态生成的,所以不起作用。

public sealed class Renderer: Control, IDisposable
{
  ....
  private void UpdateAnnotationsListView() 
  {
    (GetTemplateChild("AnnotsList") as ListView).ItemsSource = null;

    var source = AnnotationAdapter.GetGroupedAnnotations(); // ObservableCollection<ListViewGroupInfo>

    var viewSource = new CollectionViewSource 
    {
      IsSourceGrouped = true, Source = source
    };
    (GetTemplateChild("AnnotsList") as ListView).ItemsSource = viewSource.View;

    if (viewSource.View.Count > 0) 
    {
      (GetTemplateChild("btn_RemoveFromList") as Button).Click -= null;
      (GetTemplateChild("btn_RemoveFromList") as Button).Click += async delegate(object sender, RoutedEventArgs e) 
      {
        await OpenRemoveConfirmationAsync();
      };
    }
  }
  ....
}

列表源是 ObservableCollection 类型

public class ListViewGroupInfo: List < object >
{
  public ListViewGroupInfo() {}

  public ListViewGroupInfo(IEnumerable < object > items): base(items) {}

  public object Key 
  {
    get;
    set;
  }
}

列表源的结构使我可以相应地对列表项进行分组。

这是呈现的 ListView 的示例,以提供更多上下文。

“删除”按钮是我在这里尝试使用的按钮。

我想为ListView中那些按钮的点击事件绑定一个方法。

我无法使用名称属性,因为随着列表的增长可能会有多个按钮。

由于此按钮位于模板化控件中并在运行时生成,我找不到将方法绑定到单击事件的方法。

我的猜测是我必须将命令绑定到按钮。但是我也找不到办法。

我没有在模板化控件中使用 MVVM 模式。

谁能帮我解决这个问题?非常感谢任何帮助。

My guess is that I will have to bind a command to the button. But I couldn't find a way to do that either.

更好的方法是使用命令来接近,我将在下面分享详细步骤,您可以参考。请注意,您需要将当前页面数据上下文设置为 this.DataContext = this;。它可以确保您可以从 DataTemplate.

后面的代码中访问命令所在的位置

Xaml代码

<Grid>
    <ListView x:Name="MyListView" ItemsSource="{x:Bind Items}">
        <ListView.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition Width="Auto" />
                    </Grid.ColumnDefinitions>
                    <StackPanel Orientation="Vertical">
                        <TextBlock Text="{Binding Header}" />
                        <TextBlock
                            Margin="20,0,0,10"
                            FontSize="12"
                            Text="{Binding DisplayTitle}"
                            TextWrapping="WrapWholeWords"
                            Visibility="Visible" />
                    </StackPanel>
                    <CommandBar Grid.Column="1">
                        <CommandBar.SecondaryCommands>
                            <AppBarElementContainer>
                                <StackPanel Orientation="Horizontal">
                                    <Button
                                        x:Name="btn_RemoveFromList"
                                        Command="{Binding DataContext.DeleteCommand, ElementName=MyListView}"
                                        CommandParameter="{Binding}">
                                        <Button.Content>
                                            <SymbolIcon Symbol="Delete" />
                                        </Button.Content>
                                        <ToolTipService.ToolTip>
                                            <ToolTip Content="Delete" Placement="Mouse" />
                                        </ToolTipService.ToolTip>
                                    </Button>
                                </StackPanel>
                            </AppBarElementContainer>
                        </CommandBar.SecondaryCommands>
                    </CommandBar>
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</Grid>

代码隐藏

public sealed partial class ListPage : Page
{
    public ListPage()
    {
        this.InitializeComponent();
        this.DataContext = this;
    }
    private ObservableCollection<Model> Items { set; get; }
   
    public ICommand DeleteCommand
    {
        get
        {
            return new CommadEventHandler<Model>((s) =>
            {
                Items.Remove(s);

            });
        }
    }
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);

        MakeDataSource();
    }
    private void MakeDataSource()
    {
        Items = new ObservableCollection<Model>();
        for (int i = 0; i < 10; i++)
        {
            Items.Add(new Model()
            {
                Header = $"header{i}",
                DisplayTitle= $"DisplayTitle{i}"
            });
        }
     
    }
}
public class Model
{
    public string Header { get; set; }
    public string DisplayTitle { get; set; }
}

class CommadEventHandler<T> : ICommand
{
    public event EventHandler CanExecuteChanged;

    public Action<T> action;
    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        this.action((T)parameter);
    }
    public CommadEventHandler(Action<T> action)
    {
        this.action = action;

    }
}

经过大量研究和反复试验,我最终采用了@nico-zhu-msft 建议的不同方法。

基本上,我将 ListView 移动到一个单独的用户控件并观察到父模板控件的 属性 更改。为了将数据绑定到 ListView 使用了视图模型。

AssnotationsList.xaml

<UserControl
    x:Class="PDF.Renderer.AnnotationsList"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:PDF.Renderer"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:viewmodels="using:PDF.Renderer.ViewModels"
    mc:Ignorable="d"
    d:DesignHeight="300"
    d:DesignWidth="400">

    <UserControl.DataContext>
        <viewmodels:AnnotationsListViewModel />
    </UserControl.DataContext>
    
    <UserControl.Resources>
        <Style x:Key="AnnotationsListViewItemStyle" TargetType="ListViewItem">
            <Setter Property="HorizontalContentAlignment" Value="Stretch" />
            <Setter Property="VerticalContentAlignment" Value="Stretch" />
            <Setter Property="VerticalAlignment" Value="Center"/>
        </Style>
    </UserControl.Resources>

    <ListView SelectionMode="None" VerticalAlignment="Stretch" IsItemClickEnabled="True" ItemContainerStyle="{StaticResource AnnotationsListViewItemStyle}" ItemsSource="{Binding AnnotationsList}" ItemClick="AnnotationListViewItemClick">
        <ListView.ItemTemplate>
            <DataTemplate>
                <StackPanel Orientation="Vertical">
                    <TextBlock Text="{Binding}" />
                    <TextBlock Text="{Binding DisplayTitle}" Margin="20,0,0,10" FontSize="12" TextWrapping="WrapWholeWords" Visibility="Visible" />
                </StackPanel>
            </DataTemplate>
        </ListView.ItemTemplate>

        <ListView.GroupStyle>
            <GroupStyle >
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <Border AutomationProperties.Name="{Binding Key}">
                            <TextBlock Text="{Binding Key}" Style="{ThemeResource TitleTextBlockStyle}"/>
                        </Border>
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </ListView.GroupStyle>
    </ListView>
</UserControl>

AnnotationsList.xaml.cs

public sealed partial class AnnotationsList : UserControl, INotifyPropertyChanged
{
    public AnnotationsList()
    {
        this.InitializeComponent();
    }

    private BaseAnnotation selectedAnnotation = null;

    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;

        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }

    public ICollectionView AnnotationsListSource
    {
        get { return (ICollectionView)GetValue(AnnotationsListSourceProperty); }
        set { SetValue(AnnotationsListSourceProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty AnnotationsListSourceProperty =
        DependencyProperty.Register(nameof(AnnotationsListSourceProperty), typeof(ICollectionView), typeof(AnnotationsList), new PropertyMetadata(null, new PropertyChangedCallback(OnAnnotationsListSourceChanged)));

    private static void OnAnnotationsListSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (object.Equals(e.NewValue, e.OldValue) || e.NewValue is null)
            return;

        d.RegisterPropertyChangedCallback(AnnotationsListSourceProperty, CaptureAnnotationListSource);
    }

    private static void CaptureAnnotationListSource(DependencyObject sender, DependencyProperty dp) => (sender as AnnotationsList).SetAnnotationsListSource(sender.GetValue(dp) as ICollectionView);

    private void SetAnnotationsListSource(ICollectionView annotationsCollection) => (this.DataContext as AnnotationsListViewModel).AnnotationsList = annotationsCollection;

    public BaseAnnotation SelectedAnnotation
    {
        get { return selectedAnnotation; }
        set { if (value != selectedAnnotation && value != null) { selectedAnnotation = value; OnPropertyChanged(nameof(SelectedAnnotation)); }; }
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedAnnotationProperty =
        DependencyProperty.Register(nameof(SelectedAnnotationProperty), typeof(BaseAnnotation), typeof(AnnotationsList), new PropertyMetadata(null));

    private void AnnotationListViewItemClick(object sender, ItemClickEventArgs e) => SelectedAnnotation = e.ClickedItem as BaseAnnotation;
}

AnnotationsListViewModel.cs

class AnnotationsListViewModel : ViewModalBase
{
    private ICollectionView annotationsList = null;

    public ICollectionView AnnotationsList
    {
        get { return annotationsList; }
        set { if(value != annotationsList) { annotationsList = value; OnPropertyChanged(nameof(AnnotationsList)); } }
    }
}

用这样的用户控件替换了 ListView Renderer.cs

<local:AnnotationsList x:Name="ctrl_AnnotationsList" Margin="0,12,0,0" Grid.Row="1" VerticalAlignment="Stretch" Visibility="{Binding IsAnnotationsListOpen, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" />

在父控件中 class Renderer.cs(模板控件)在第一次呈现父控件并绑定 PropertyChanged 事件时得到了对 AnnotationsList 控件的引用.

AnnotationsList = GetTemplateChild("ctrl_AnnotationsList") as AnnotationsList;
AnnotationsList.PropertyChanged -= null;
AnnotationsList.PropertyChanged += OnAnnotationsListPropertyChanged;

添加了以下代码以触发 AnnotationsList 控件中的 属性 更改。

private void OnAnnotationsListPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    switch(e.PropertyName)
    {
        case "SelectedAnnotation":
            var annotation = (sender as AnnotationsList).SelectedAnnotation;
            if (annotation != null)
                GoToAnnotation(annotation).GetAwaiter();

            break;
        default:
            break;
    }
}

目前配置为在 ListViewItemsItemClick 事件上触发。

希望这对可能正在寻找类似解决方案的人有所帮助。