Search code examples
xamluwpuwp-xamltemplate-control

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


I have a templated control in my UWP application which contains a ListView. The ListView is populated in the runtime.

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

I tried to bind a click event to the button like this but since it is dynamically generated it doesn't work.

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();
      };
    }
  }
  ....
}

List source is a ObservableCollection of type

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

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

  public object Key 
  {
    get;
    set;
  }
}

List source is structured in such a way where I can group the list items accordingly.

This is a sample of the rendered ListView for more context. enter image description here

The Delete buttons are the ones I'm trying to work with here.

I want to bind a method to the click event of those buttons in the ListView.

I Cannot use the name attribute since there can be multiple buttons as the list grows.

Since this button is in a templated control & generated in the runtime, I couldn't find a way to bind a method to the click event.

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.

I did not use MVVM pattern in the templated control.

Could anyone help me with this? Any help is much appreciated.


Solution

  • After a whole bunch of research & trial and error, I ended up with a different approach as @nico-zhu-msft suggested.

    Basically, I moved the ListView to a separate user control & observed property changes from the parent template control. In order to bind data to the ListView used a view-model.

    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)); } }
        }
    }
    

    Replaced the ListView with the user control Renderer.cs like this.

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

    In the parent control class Renderer.cs (template control) got a reference to the AnnotationsList control like this when parent is first rendered & bound the PropertyChanged event.

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

    Added the following code to trigger on property changes in the AnnotationsList control.

    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;
        }
    }
    

    For now it is configured to trigger on ItemClick event of the ListViewItems.

    Hope this helps someone who might be looking for a similar solution.