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

313 Views Asked by At

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.

2

There are 2 best solutions below

0
On BEST ANSWER

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.

7
On

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.

The better way is using command to approach, I will share the detail steps below that you could refer to. please note you need to set current page datacontext as this this.DataContext = this;. it could make sure you can access command where in the code behind from DataTemplate.

Xaml Code

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

Code behind

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;

    }
}