I have a DataGrid with rows representing a host I'm doing pings to and a column called Lost which represents lost ICMP packets which over time increases in value. I have the whole INotifyPropertyChanged thing down and I'm seeing value increase. What I want to do is write a Style that'll change a row's background color from white to dark red progressively relative to the Lost column's value.
I would like, if it were possible, to write a Trigger or DataTrigger with a setter value set to a ValueConverter which would calculate the color needed, but so far I've been unsuccessful in writing a style that will update every time a Lost cell's value changes. I only see a difference in color when I load a new data context and switch back (just for testing).
Here's what I've tried:
<Style x:Key="DownStyle" TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="{Binding Converter={StaticResource BackgroundConverter}}" />
</Style>
I can't see this working with a DataTrigger since you have to specify a value anyways and the values are infinite (or Int32.MaxValue I guess), really. Although I also tried specifying a ValueConverter for the value property and that didn't work either.
Btw, I want to try to avoid code-behind if possible.
Edit:
Rick: I had tried doing something like:
<Style x:Key="DownStyle" TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<DataTrigger Binding="{Binding Converter={StaticResource LostColumnValueConverter}}" Value="Somenumber">
<Setter Property="Background" Value="{Binding Converter={StaticResource BackgroundConverter}}" />
</DataTrigger>
</Style.Triggers>
</Style>
I think I understand the need to have the Trigger actually bind to something that's going to be changing (in this case I feel I'm forced to also use a ValueConverter to get the column's value, is there a more direct way?), but even if I do this, what do I specify as the DataTrigger's value?
Edit:
So in my case I went ahead and did the following (currently this would only modify the TextBlock's background):
<DataGridTemplateColumn Header="Lost" Width="Auto">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Lost, NotifyOnTargetUpdated=True}">
<TextBlock.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetProperty="Background">
<DiscreteObjectKeyFrame KeyTime="0" Value="{Binding Converter={StaticResource BackgroundConverter}}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</TextBlock.Triggers>
</TextBlock>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Which to me seems right, but only works once for some reason. I added a seperate TargetUpdated event handler right on the TextBlock definition to see if the event was indeed being called on every change and it is.
Something must be missing on my EventTrigger. Probably something to do with the Storyboard.
Anyways, this all seems incredibly verbose for something so simple, so I went ahead and went with the code-behind route.
I suppose Triggers and DataTriggers can't help in this case because datagrigger's value is unstable. And worse, it's not dependency property and you can't bind to it. It seems to me the better way is to use EventTrigger.
<Window.Resources>
<BeginStoryboard x:Key="bsbPing">
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="FontSize" To="{Binding Path=PingValue}" />
</Storyboard>
</BeginStoryboard>
</Window.Resources>
<Grid>
<StackPanel>
<TextBox Name="txbPingValue" Text="{Binding Path=PingValue}">
<TextBox.Triggers>
<EventTrigger RoutedEvent="TextBox.TextChanged">
<EventTrigger.Actions>
<StaticResource ResourceKey="bsbPing" />
</EventTrigger.Actions>
</EventTrigger>
</TextBox.Triggers>
</TextBox>
<Button Name="btnPing" Click="btnPing_Click">Ping</Button>
</StackPanel>
</Grid>
and code:
public partial class Window7 : Window
{
public Ping MyPing { get; set; }
public Window7()
{
InitializeComponent();
MyPing = new Ping { PingValue = 20.0 };
this.DataContext = MyPing;
}
private void btnPing_Click(object sender, RoutedEventArgs e)
{
MyPing.PingValue += 10;
}
}
public class Ping : INotifyPropertyChanged
{
private double pingValue;
public double PingValue
{
get
{
return pingValue;
}
set
{
pingValue = value;
NotifyPropertyChanged("PingValue");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
You are on the right track with the converter. The technique you are using is sometimes called "binning" which maps a large set of integers to a fixed small set of values. It is also used to create histograms.
As far as not updating, your binding expression has to be more specific to retrieve the Lost property, otherwise it won't be re-evaluated when Lost changes. Once you change it you'll also need to update your converter.
Related
I have a custom control with the following border definition:
<Border Width="10" Height="10"
CornerRadius="5"
Background="Red"
BorderBrush="White"
BorderThickness="1">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Opacity" Value="0.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding MyState}" Value="{x:Static my:MyStates.Initializing}">
<DataTrigger.EnterActions>
<StopStoryboard BeginStoryboardName="Animate"/>
<BeginStoryboard x:Name="Animate" HandoffBehavior="SnapshotAndReplace">
<Storyboard Duration="0:0:0.4">
<DoubleAnimation AutoReverse="True" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.2" FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
The intention is: when MyState changes to a value of MyStates.Initializing, the border should fade in and out again.
I did not define <DataTrigger.ExitActions>, because MyState will be changed again very quickly; nothing should happen (except the animation finishing) when MyState is set to a different value.
The weird thing is: this only works once. If I have two instances of the same control: both will fire once, but never again. If I then add another instance, it will not fire either.
I searched through countless links and suggestions, e.g.:
WPF Animation Only Firing Once
https://social.msdn.microsoft.com/Forums/vstudio/en-US/7e074dc8-e5da-4840-8b54-8fcb67b43329/storyboard-run-only-one-time-inside-datatriggers?forum=wpf
Animation inside DataTrigger won't run a second time
DataTrigger and Storyboard only getting executed once, related to order of declaration?
What am I missing?
EDIT:
To clarify (after reading a comment below), the animation works for the first time it is triggered, but then never again. If MyState changes to Initializing, it is triggered (apparently) correctly. Then (before the animation is finished, a few milliseconds or less later) MyState changes to Whatever and the animation finishes as desired. If MyState then is again changed to Initializing (later, long after the animation has finished), nothing happens.
Also: If I have two instances that respond the their respective MyState changing to Initializing (within a few milliseconds or less), both are triggered correctly. I can then add a completely new instance and that will not be triggered.
What I also checked is, if a second instance would be triggered correctly, if the trigger (setting MyState == Initialized) came after the animation of the first instance has finished. Yes, every instance existing when the trigger first fires will be triggered correctly once. After that: nothing...
EDIT:
Below is the code I wrote to isolate and test the issue. In a previous edit I had assumed that the code was ok and my error must be hidden somewhere else, but I had tested with a sleep duration (see code below) of 1000ms. If I reduce that to 10ms, my problem persists. So, it appears to be a data binding issue - and not an animation issue.
AnimationTestControl.xaml.cs:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace AnimationTest
{
public enum MyStates
{
None = 0,
Initializing = 1,
Ready = 2,
}
public class TestItem : INotifyPropertyChanged
{
private MyStates _myState;
public MyStates MyState
{
get => _myState;
set { _myState = value; OnPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public partial class AnimationTestControl : UserControl
{
public ObservableCollection<TestItem> TestItems { get; } = new ObservableCollection<TestItem>();
public AnimationTestControl()
{
InitializeComponent();
TestItems.Add(new TestItem());
}
private void ButtonStart_Click(object sender, RoutedEventArgs e)
{
Task.Run(() =>
{
foreach (var testItem in TestItems)
{
testItem.MyState = MyStates.Initializing;
Thread.Sleep(10); //1000ms = good, 10ms = bad
testItem.MyState = MyStates.Ready;
}
});
}
private void ButtonAdd_Click(object sender, RoutedEventArgs e)
{
TestItems.Add(new TestItem());
}
}
}
AnimationTestControl.xaml:
<UserControl x:Class="AnimationTest.AnimationTestControl"
x:Name="self"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:AnimationTest"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid DataContext="{Binding ElementName=self}">
<StackPanel Orientation="Vertical">
<ItemsControl ItemsSource="{Binding TestItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Border Width="10" Height="10"
CornerRadius="5"
Background="Red"
BorderBrush="White"
BorderThickness="1">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Opacity" Value="0.0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding MyState}" Value="{x:Static local:MyStates.Initializing}">
<DataTrigger.EnterActions>
<StopStoryboard BeginStoryboardName="Animate"/>
<BeginStoryboard x:Name="Animate" HandoffBehavior="SnapshotAndReplace">
<Storyboard Duration="0:0:0.4">
<DoubleAnimation AutoReverse="True" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.2" FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<TextBlock Text="{Binding MyState}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<StackPanel Orientation="Horizontal">
<Button Content="Start" Click="ButtonStart_Click" />
<Button Content="Add" Click="ButtonAdd_Click" />
</StackPanel>
</StackPanel>
</Grid>
</UserControl>
It turns out that the problem has nothing to do with the animation, but instead with the DataTrigger or WPF's throttling in general, see Is there a workaround for throttled WPF DataTrigger events?.
As the example code in the link above shows, the observed behavior (esp. regarding "first only" and additional instances) was not accurate or not as strictly reproducible as it seemed.
My problem seems to be the opposite of the one everybody else has. My WPF DataTrigger fires sometimes even when the viewmodel property to which it is tied does not change. I've put logging code in the property itself to make sure it's not changing.
Is there some easy to way determine WHY an animation is firing? Some event I can subscribe to let me put a breakpoint in my own code and look at the call stack at the moment the animation fires?
Here's the Storyboard. Just animating a double from 0 to 1 over 1 second. The double will be stored in FrameworkElement.Tag
<Storyboard x:Key="TagZeroToOne" x:Shared="False">
<DoubleAnimation Storyboard.TargetProperty="Tag" From="0" To="1" Duration="0:0:1.0" />
</Storyboard>
And here is the relevant bit of the style that uses it in the DataTrigger. (I've left out the item template and panel)
<!-- Style for ItemsControl that draws circular handles when ShapeVm is selected -->
<Style x:Key="ShapeHandlesItemsControlStyle" TargetType="{x:Type ItemsControl}">
<d:Style.DataContext>
<x:Type Type="gci:ShapeVm" />
</d:Style.DataContext>
<!-- Default style value of ItemsControl.Tag" property is 1.0 -->
<Setter Property="Tag">
<Setter.Value>
<sys:Double>1.0</sys:Double>
</Setter.Value>
</Setter>
<!--
We cannot animate Opacity directly because its max value depends upon
a binding but we be we can make it be dependent upon another property
"Tag" that we *do* animate. This requires a Math Converter as well
Picked up this technique from the following SO question
https://stackoverflow.com/questions/2186933/wpf-animation-binding-to-the-to-attribute-of-storyboard-animation/14164245#14164245
-->
<Setter Property="Opacity">
<Setter.Value>
<MultiBinding Converter="{StaticResource CvtMath}"
ConverterParameter="1.0 - (x *(1.0-y))"> <!-- Calculation -->
<Binding Path="Tag" RelativeSource="{RelativeSource Self}" /> <!-- Animated operand (0.0 to 1.0) -->
<Binding ElementName="Root" Path="ShapeHandleOpacity" /> <!-- Configured opacity operand -->
</MultiBinding>
</Setter.Value>
</Setter>
<!-- When ShapeVm.IsSelected becomes true, animate the ItemsControl.Tag from 0 to 1.0 -->
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource TagZeroToOne}" />
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
The idea is that when the user first selects the shape, my code-behind sets its IsSelected property to "true". That makes the animation fire and I see the visual effect tied to my animation. But that's supposed to be it.
Instead, it seems that almost every other time I click on the object in question, The animation fires and I see the effect. Yet I have verified (repeatedly) via logging and breakpoints that the IsSelected property is never changing after that. Nobody is setting it. Nobody is firing a PropertyChanged Notification for it and as far as I can see, nobody is even firing PropertyChanged(null).
And here is the ShapeVm.IsSelected property
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
if (value == _isSelected)
return;
_isSelected = value;
Debug.WriteLine($"Shape selected change to {value}");
}
}
So how do I determine why WPF is making this trigger fire?
I have four buttons and four text boxes where each button is linked to one of the textblocks. When the mouse is over the button I want the corresponding textblock to fade in (and out on mouse leave). There are plenty of examples of this showing a single button and textblock where you can simply bind a datatrigger to the button name in the textblock style.
Here's what I've got so far (all of this is in a textblock style):
<DataTrigger Binding="{Binding ElementName=UpdateButton, Path=IsMouseOver}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard TargetProperty ="Opacity" Duration="00:00:01">
<DoubleAnimation From="0" To="1" Duration="00:00:01"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard TargetProperty ="Opacity" Duration="00:00:01">
<DoubleAnimation From="1" To="0" Duration="00:00:01"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
As of right now, when I mouse over the Update Button, all of the textblocks show instead of just the one associated with the Update Button.
To fix this I could create styles for each textblock by their name and bind to the appropriate button, but this is a huge amount of repetition. I could likely used "BasedOn" to separate the button binding, but then we're still duplicating all of the code for the Storyboards and whatnot. But does anyone know a better way?
It would seem like there should be a way create this all in a single style using a single generic binding but link the specific buttons to their textblocks, so the button only triggers the Storyboard for it's linked textblock. Anyone know how to do this, or a better way?
A good way to handle this is to create a custom inherited TextBlock that can store reference to a button.
Example
using System.Windows;
using System.Windows.Controls;
//Custom TextBlock
public class SpecialTextBlock : TextBlock
{
//This will be the button reference
public Button BoundButton { get; set; }
//Register the BoundButton as a dependency to allow binding
public static readonly DependencyProperty ButtonProperty = DependencyProperty.Register
(
"BoundButton",
typeof(Button),
typeof(SpecialTextBlock),
new FrameworkPropertyMetadata(default(Button))
);
}
Now that your new SpecialTextBlock is set up, you can create a new style for it. Use your original style, but apply it to TargetType="local:SpecialTextBlock" instead of TargetType="TextBlock".
Then update your DataTrigger from your example within the style so that the trigger binds to itself (the SpecialTextBlock), and then looks at the referenced Button path.
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=BoundButton.IsMouseOver}" Value="True">
...
Now you are set up and can create your TextBlocks like so without having to restyle.
//Set your BoundButton binding to specify which button triggers the animation.
<local:SpecialTextBlock BoundButton="{Binding ElementName=UpdateButton}" />
<StackPanel>
<Button x:Name="MouseTarget"
Content="Mouse Over This"
/>
<Button Content="This one changes...">
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=MouseTarget, Path=IsMouseOver}" Value="True">
<Setter Property="Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
I like to use the ItemsControl to host ContentsControls. Each new ContentsControl is animating its contents when the item gets added and each ContentControl and overlays the previous one. The ItemsControl and the ContentControl Content is bound with Caliburn Micro using Naming conventions.
<ItemsControl x:Name="OverlayStackedItems" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Background="Transparent">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid x:Name="ItemsHost" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<cc:DummyContentControl cal:View.Model="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
The ContentControl is defined like this:
[ContentProperty("Content")]
public partial class DummyContentControl :ContentControl
{
public DummyContentControl()
{
}
static DummyContentControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(DummyContentControl), new FrameworkPropertyMetadata(typeof(ContentControl)));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
}
protected override void OnContentChanged(object oldContent, object newContent)
{
LayoutUpdated += (sender, e) =>
{
};
UpdateLayout();
base.OnContentChanged(oldContent, newContent);
}
void DummyContentControl_LayoutUpdated(object sender, EventArgs e)
{
throw new NotImplementedException();
}
protected override Size MeasureOverride(Size constraint)
{
return base.MeasureOverride(constraint);
}
}
So now finally my question. In the real ContentControl I like to animate the Content but
the ContentControl has the size of 0 when OnContentChange is called where my Animation gets created. The orders of calls when the ContentControl is hosted in the ItemsControl is:
OnContentChanged (Animation failes)
OnApplyTemplate
MeasureOverride
When the ContentControl runs by itself the order is:
OnApplyTemplate
MeasureOverride
OnContentChanged (Animation works)
The problem here is that the complete visual subtree of the new Item in the ItemsControl is 0 (DesiredSize,ActualSize = 0) therefore my animation code fails.
I hope that makes some sense to somebody,
Any help would be great, Thx,J
------------------------------Revision-------------------
Ok I added the OnLoaded eventhandler to the ctor of the DummyControl. The order of calles is
1. OnContentChanged (all sizes are 0)
2. OnApplyTemplate (all sizes are 0)
3. MeasureOverride (called several Times probably for all child controls hostet by the ContentControl)
4. Loaded event (Desired Size is set all other sizes are still 0)
Can sombody explain what the recommanded practice is on how to animate a ContentControl
hostet by an ItemsControl?
Just do everything in XAML and let the animation do it's thing, without calling MeasureOverride() and the rest of the hooks.
<ItemsControl>
<ItemsControl.ItemContainerStyle>
<Style>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border>
<TextBlock Text="Whatever your template should look like"/>
</Border>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard>
<Storyboard >
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleX)" Duration="0:0:0.5" From="0" To="1" />
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.ScaleY)" Duration="0:0:0.5" From="0" To="1" />
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.CenterX)" Duration="0:0:0.5" To="25" />
<DoubleAnimation Storyboard.TargetProperty="(Border.RenderTransform).(ScaleTransform.CenterY)" Duration="0:0:0.5" To="25" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
I'm trying to execute an animation on a cell in a datagrid when the value of the datagrid cell changes.
The datagrid itself is bound to an ObservableCollection of plain old CLR objects. In this case lets say the objects are 'Person' objects with properties for 'Firstname', 'Lastname' and 'Age'. The 'Person' class implements the INotifyPropertyChanged interface and each property has the appropriate call to onPropertyChanged in it's setter.
This is all fine. In the datagrid definition I've set my DataTemplate for drawing each cell and attached a datatrigger too ... as follows:
<DataGridTemplateColumn Header="FirstName">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Border Name="templateBorder">
<TextBlock Name="templateTextBlock" Text="{Binding Path=FirstName}" />
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=FirstName}" Value="Richard">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation Storyboard.TargetName="templateTextBlock" Storyboard.TargetProperty="Opacity" To=".1" Duration="0:0:.5" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
When an object in my ObservableCollection is updated (I changed the FirstName value) the datagrid is updated fine. As per the example above, if I changed the value of FirstName to 'Richard' then the animation is executed fine too.
My problem is that I need to run my animation regardless of what the new value of Firstname is. I've crawled the web but some far only seem to find examples of firing the trigger against a known value e.g. fire trigger when FirstName is 'Richard' as I've demonstrated in my example.
My question is how do I fire the datatrigger regardless of the value of the updated property? So basically how do I fire the datatrigger whenever the FirstName property is updated for a given row in the datagrid.
Many thanks.
Thanks to the pointers gained from the responses to this question I found the answer was to use an EventTrigger and the TargetUpdated RoutedEvent.
<DataTemplate>
<Border Name="templateBorder">
<TextBlock Name="templateTextBlock" Text="{Binding Path=FirstName, NotifyOnTargetUpdated=True}" />
</Border>
<DataTemplate.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard AutoReverse="True">
<DoubleAnimation Storyboard.TargetName="templateTextBlock" Storyboard.TargetProperty="Opacity" To=".1" Duration="0:0:.5" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Beyond the EventTrigger, the only other thing that was required was to set 'NotifyOnTargetUpdated=True' when setting up the binding for the textblock.
Thanks.
It looks like you need an EventTrigger "do X when an event occurs" instead of a DataTrigger.
Not tried this myself.. but it should be possible to raise your custom event FirstNameChanged and have the trigger-actions be executed in response to that.
<Storyboard x:Key="MessageStoryBoardEntry" FillBehavior="Stop">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
<EasingDoubleKeyFrame KeyTime="0:0:00.30" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:03" Value="0"/>
<EasingDoubleKeyFrame KeyTime="0:0:03.20" Value="1500"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="MessageStoryBoardExit" FillBehavior="Stop">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
<EasingDoubleKeyFrame KeyTime="0:0:0.001" Value="1500"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
You could try setting the DataContext of the TextBlock to the FirstName property, and then use the DataContextChanged event.
Or you could use the PropertyChanged event and filter for the property you want.
Either way I think you're going to have to use an event.
Could you hack something in with a value converter?
<DataTrigger Binding="{Binding Path=FirstName, Converter=FirstNameConverter}" Value="MakeItSo">
and
class FirstNameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return "MakeItSo";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
...
}
}
I guess it depends on whether WPF calls the converter on every property change, or whether it evaluates the value first. I've not tried it, it's just a thought...