0

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.

  • We need more context here, where is the command or method that you want to bind to the click event? The simplest way is to define the command as a property on the viewmodel for the element in the list, but your xaml fragment does not even describe the list source. Please include enough of the view model or code behind for where the click action is defined and the link between them. – Chris Schaller Aug 04 '21 at 06:20
  • @ChrisSchaller I have updated the question. Hope there is enough information now to understand the problem I'm having. – Madushan Amarasinghe Aug 04 '21 at 07:55
  • I see, can be done, but there are lots of ways, depending on what MVVM toolkit you are using in general. (because I don't want to use MVVMLight concepts if you are using basic or prism or have rolled your own). Ultimately it is best NOT to put that level of detail inside a template control, (the definition of list item contents) but instead template the individual list item elements. I'll see if I can put together a minimal solution. – Chris Schaller Aug 04 '21 at 14:40
  • Where do you placed List source ? – Nico Zhu Aug 05 '21 at 05:08
  • I bind the list source in the control class like this ```var source = AnnotationAdapter.GetGroupedAnnotations(); var viewSource = new CollectionViewSource { IsSourceGrouped = true, Source = source }; (GetTemplateChild("AnnotsList") as ListView).ItemsSource = viewSource.View; ``` – Madushan Amarasinghe Aug 05 '21 at 07:44
  • 1
    if you used usercontrol to approach, the better way is create dependency property. for more please refer to this [document](https://learn.microsoft.com/en-us/windows/uwp/xaml-platform/custom-dependency-properties). – Nico Zhu Aug 06 '21 at 01:27

2 Answers2

0

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;

    }
}
Nico Zhu
  • 32,367
  • 2
  • 15
  • 36
0

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.