【问题标题】:Bind a click event to a dynamically generated button element in a templated control / template control将点击事件绑定到模板化控件/模板控件中动态生成的按钮元素
【发布时间】:2021-10-09 05:53:34
【问题描述】:

我的 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 模式。

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

【问题讨论】:

  • 这里我们需要更多上下文,你想绑定到点击事件的命令或方法在哪里?最简单的方法是将命令定义为列表中元素的视图模型上的属性,但您的 xaml 片段甚至不描述列表源。请包含足够的视图模型或代码,用于定义单击操作的位置以及它们之间的链接。
  • @ChrisSchaller 我已经更新了这个问题。希望现在有足够的信息来了解我遇到的问题。
  • 我明白了,可以做到,但是有很多方法,具体取决于您通常使用的 MVVM 工具包。 (因为我不想使用 MVVMLight 概念,如果您使用的是 basic 或 prism 或已推出自己的)。最终,最好不要将该级别的详细信息放在模板控件中(列表项内容的定义),而是将各个列表项元素模板化。我会看看我是否可以提出一个最小的解决方案。
  • 你把List source放在哪里了?
  • 如果你使用用户控件来接近,更好的方法是创建依赖属性。更多信息请参考document。

标签: xaml uwp uwp-xaml template-control


【解决方案1】:

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

更好的方法是使用命令接近,我将分享下面的详细步骤,您可以参考。请注意,您需要将当前页面数据上下文设置为 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;

    }
}

【讨论】:

  • 我可以在模板控件中使用这个逻辑吗?
  • 当然,你可以在模板控件中使用它
  • 您能解释一下如何在模板控件中使用它吗?我似乎找不到设置 DataContext 的方法。
  • 您可以为您的模板控件创建命令dependency property,并将其与TemplateBinding绑定
  • 如果将列表源放在模板cs文件中,则可以在cs文件中设置datacontext。
【解决方案2】:

经过大量研究和反复试验,我最终采用了 @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}}" />

在父控件类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;
    }
}

现在它被配置为在ListViewItems 的ItemClick 事件上触发。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2010-12-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多