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?
CodePudding user response:
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>