<EventTrigger RoutedEvent="Binding.TargetUpdated"> not firing with ContentTemplateSelector

412 views Asked by At

I'm trying to animate the Background Color of a Border in a DataTemplate for a DataObject when a Child Property of the DataObject changes. The DataObject is a Class called Test with two Properties, Number and Text. I have an ObservableCollection of DataObjects called Numbers. In a Task I update the Number Property at a regular Interval.

<Window
  x:Class="WpfAnimationTest.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:local="clr-namespace:WpfAnimationTest"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  Title="MainWindow"
  Width="800"
  Height="450"
  DataContext="{Binding Main, Source={StaticResource Locator}}"
  mc:Ignorable="d">
    <Grid>
        <Grid.Resources>
            <DataTemplate x:Key="NumberTemplate">
                <TextBlock Text="{Binding NotifyOnTargetUpdated=True}" />
            </DataTemplate>

            <local:ValueTemplateSelector x:Key="TemplateSelector">
                <local:ValueTemplateSelector.NumberTemplate>
                    <DataTemplate>
                        <ContentControl Content="{Binding NotifyOnTargetUpdated=True}" ContentTemplate="{StaticResource NumberTemplate}" />
                    </DataTemplate>
                </local:ValueTemplateSelector.NumberTemplate>
            </local:ValueTemplateSelector>

            <DataTemplate DataType="{x:Type local:Test}">
                <Border x:Name="UpdateBorder" Background="Aqua">
                    <StackPanel Orientation="Horizontal">
                        <TextBlock
                          Width="50"
                          Margin="10"
                          Text="{Binding Text}" />
                        <ContentControl
                          Width="50"
                          Margin="10"
                          Content="{Binding Number}"
                          ContentTemplateSelector="{StaticResource TemplateSelector}" />

                        <!--
                            ContentTemplate="{StaticResource NumberTemplate}"
                        -->
                    </StackPanel>
                </Border>
                <DataTemplate.Triggers>
                    <EventTrigger RoutedEvent="Binding.TargetUpdated">
                        <EventTrigger.Actions>
                            <BeginStoryboard>
                                <Storyboard AutoReverse="True">
                                    <ColorAnimation
                                      FillBehavior="Stop"
                                      Storyboard.TargetName="UpdateBorder"
                                      Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                                      To="#C5AFFFAA"
                                      Duration="00:00:0.5" />
                                </Storyboard>
                            </BeginStoryboard>
                        </EventTrigger.Actions>
                    </EventTrigger>
                </DataTemplate.Triggers>
            </DataTemplate>
        </Grid.Resources>
        <ListBox ItemsSource="{Binding Numbers}" />
    </Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

namespace WpfAnimationTest
{
    public class Locator
    {
        public Locator()
        {
            Main = new Main();
        }

        public Main Main { get; set; }
    }

    public class Main
    {
        public ObservableCollection<Test> Numbers { get; set; } = new ObservableCollection<Test>();

        public Main()
        {
            var rnd = new Random(42);

            for (int i = 0; i < 10; i++)
            {
                Numbers.Add(new Test(){Number = i, Text = $"#: {i}"});
            }

            Task.Run(() =>
            {
                while (true)
                {
                    try
                    {

                        Application.Current?.Dispatcher.Invoke(() =>
                        {
                            Numbers[rnd.Next(9)] = new Test
                            {
                                Number = rnd.Next(30),
                                Text = $"# {rnd.Next(30) + 30}"
                            };
                        });
                        Thread.Sleep(1000);

                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(e);
                        throw;
                    }
                }
            });

        }
    }

    public class Test
    {
        public int Number { get; set; }

        public string Text { get; set; }
    }

    
    public class ValueTemplateSelector : DataTemplateSelector
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="item"></param>
        /// <param name="container"></param>
        /// <returns></returns>
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            return !(item is Test value)
                ? null
                : NumberTemplate;
        }
        /// <summary>
        /// 
        /// </summary>
        public DataTemplate NumberTemplate { get; set; }
        
        /// <summary>
        /// 
        /// </summary>
        public DataTemplate DefaultTemplate { get; set; }
    }
}

When using a ContentTemplate for the Number Property in the ContentControl the animation is working. But when I use a ContentTemplateSelector the Animation is not triggered anymore.

What am I missing here?

1

There are 1 answers

2
BionicCode On BEST ANSWER

Your data bindings are wrong, resulting in unhandled or unexpected data types.

Currently, your ContentControl.Content property (inside the DataTemplate for the type Test) binds to the Test.Number property of type int. Therefore, the object that is passed to the DataTemplateSelector is of type int and not Test. The condition in the DataTemplateSelector.SelectTemplate method will return false (as int is not Test is true) and make the DataTemplateSelector.SelectTemplate return null - no template. The XAML engine will call ToString on the int instance and is therefore able to display the value correctly (despite the lack of a DataTemplate).

Solution

You must fix the binding on the ContentControl.Content property to bind to the Test instance instead of binding to the Test.Number property:

<DataTemplate DataType="{x:Type local:Test}">
    <Border x:Name="UpdateBorder"
            Background="Aqua">
      <StackPanel Orientation="Horizontal">
        <TextBlock Width="50"
                    Margin="10"
                    Text="{Binding Text}" />
        <ContentControl Width="50"
                        Margin="10"
                        Content="{Binding}"
                        ContentTemplateSelector="{StaticResource TemplateSelector}" />
      </StackPanel>
    </Border>

    ...
</DataTemplate>

Now that that the DataTemplateSelector can work properly, you must also adjust the NumberTemplate, so that it can display the Test.Number property properly. Since we have modified the binding source for the ContentControl.Content property, the new data type is now Test (instead of int):

<DataTemplate x:Key="NumberTemplate"
              DataType="{x:Type Test}">
  <TextBlock Text="{Binding Number, NotifyOnTargetUpdated=True}" />
</DataTemplate>

Remarks

There is a lot of redundant code here. You will achieve the same results by dropping all the templates and the DataTemplateSelector by defining the ItemTemplate for the Test items properly.
The following drastically simplified version should also work:

<Grid>
  <Grid.Resources>
    <DataTemplate DataType="{x:Type local:Test}">
      <Border x:Name="UpdateBorder"
              Background="Aqua">
        <StackPanel Orientation="Horizontal">
          <TextBlock Width="50"
                      Margin="10"
                      Text="{Binding Text}" />
          <TextBlock Width="50"
                      Margin="10"
                      Text="{Binding Number, NotifyOnTargetUpdated=True}" />
        </StackPanel>
      </Border>
      <DataTemplate.Triggers>
        <EventTrigger RoutedEvent="Binding.TargetUpdated">
          <EventTrigger.Actions>
            <BeginStoryboard>
              <Storyboard AutoReverse="True">
                <ColorAnimation FillBehavior="Stop"
                                Storyboard.TargetName="UpdateBorder"
                                Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
                                To="#C5AFFFAA"
                                Duration="00:00:0.5" />
              </Storyboard>
            </BeginStoryboard>
          </EventTrigger.Actions>
        </EventTrigger>
      </DataTemplate.Triggers>
    </DataTemplate>
  </Grid.Resources>

  <ListBox ItemsSource="{Binding Numbers}" />
</Grid>