I have this Style:
<Style x:Key="BlinkStyle">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=BlinkForError, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type loc:DevicesRepositoryEditorUserControl}}}"
Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard Name="BlinkBeginStoryboard">
<Storyboard>
<ColorAnimation To="Red" Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)"
FillBehavior="Stop" Duration="0:0:0.4" RepeatBehavior="Forever" AutoReverse="True" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="BlinkBeginStoryboard" />
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
Whenever the bound dependency-property BlinkForError is set to True, it start blinking. It works great, like this:
<!-- When BlinkForError set to True, this TextBox, named "One", blinks: -->
<TextBox Name="One" Style="{StaticResource ResourceKey=BlinkStyle}"/>
Thing is that I want exactly the same thing, but bound to another dependency-property, say AnotherBlinkForError:
<!-- When AnotherBlinkForError set to True, this TextBox, named "Two", blinks: -->
<TextBox Name="Two" Style="{StaticResource ResourceKey=AnotherBlinkStyle}"/>
I can duplicate the whole style and only change the DataTrigger's Binding part.
Is there a way to avoid this duplication, reuse the same Style twice with two different bindings?
You could try and make use of the Tag properties on your TextBoxes and bind them to the BlinkForError and BlinkForAnotherError. In your style definition the binding will check the Tag value (you'd probably have to use RelativeSource and FindAncestor options) instead of the Blink properties.
But to be honest, if there are only two TextBoxes and respective error properties I would go with two separate styles as it's just less hassle.
Related
I have a Style for a ToggleButton which defines a ControlTemplate. My ToggleButton is animated when it changes states, but i don't want it to animate when i navigate to a new page. So, i added an EventTrigger on the Loaded event with SkipStoryboardToFill to avoid this behavior, and it does what i want.
My only issue now, is that when i add a new ToggleButton, it tries to skip storyboards which haven't been started, generating an Animation Warning ("Unable to perform action because the specified Storyboard was never applied to this object for interactive control.") which seems to impact my application's performance.
I could probably work around that but i'd rather solve the actual problem. Is there a way i could add a condition in my EventTrigger ?
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="ToggleButton.Loaded">
<SkipStoryboardToFill BeginStoryboardName="checkedSB" />
<SkipStoryboardToFill BeginStoryboardName="uncheckedSB" />
</EventTrigger>
<DataTrigger Binding="{Binding IsChecked, RelativeSource={RelativeSource Self}}"
Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard Name="checkedSB">
<Storyboard Storyboard.TargetName="Ellipse"
Storyboard.TargetProperty="Margin">
<ThicknessAnimation To="20 1 2 1" Duration="0:0:0.1" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard Name="uncheckedSB">
<Storyboard Storyboard.TargetName="Ellipse"
Storyboard.TargetProperty="Margin">
<ThicknessAnimation To="2 1 2 1" Duration="0:0:0.1"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</ControlTemplate.Triggers>
Is there a way i could add a condition in my EventTrigger ?
Short answer: No.
An EventTrigger always applies when the corresponding event is being raised.
If you want to trigger the animations conditionally, you should either switch to using a MultiDataTrigger or implement the animations programmatically.
I'm using a DataGrid to show some elements of a collection:
<DataGrid Name="grdItems" ItemsSource="{Binding Path=myItemsList}" [...]
what I want is that if a particular column of a Row is '1', that row starts to blink. I can achieve this behaviour in this way:
<DataGrid ... >
<DataGrid.Resources>
<Storyboard x:Key="rowBlink" x:Name="Blink" AutoReverse="True" RepeatBehavior="Forever" Timeline.DesiredFrameRate="40" SpeedRatio="1">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)">
<EasingColorKeyFrame KeyTime="00:00:01" Value="Orange" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</DataGrid.Resources>
</DataGrid>
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Style.Triggers>
<DataTrigger Binding="{Binding Acked}" Value="False">
<DataTrigger.EnterActions>
<BeginStoryboard x:Name="blinkStoryBoard" Storyboard="{StaticResource rowBlink}" />
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding Acked}" Value="True">
<DataTrigger.EnterActions>
<StopStoryboard BeginStoryboardName="blinkStoryBoard" />
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
The problem is that the animation are not in synch for each row of the grid. If a row is added later, it will blink not in phase with the others. Is there a way to synch the animation?
Not necessarily a solution to your exact question, but potentially a workaround that will achieve your intended effect:
Instead of having each row blink on its own, maybe you can have the background of the DataGrid blink? Those rows which shouldn't be animated can have their BackgroundColor set to your default color, and the animated rows can have their background set to Transparent (so you can see the background).
There may be a few issues, such as having edges of the DataGrid showing the BackgroundColor animation as it blinks, but it may be worth a try. I'd run it myself, but I don't have my environment set up on this machine...if I get a chance tonight I'll run a test and post here again.
I have a WPF application which contains a UserControl, whose border is animated:
<UserControl x:Class="CarSystem.CustomControls.AlarmItem"
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:cs="clr-namespace:CarSystem.CustomControls"
mc:Ignorable="d"
DataContext="{Binding Path=Alarm, RelativeSource={RelativeSource Self}}">
<UserControl.Resources>
<Style TargetType="{x:Type cs:AlarmItem}">
<Setter Property="IsFlashing" Value="False" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
<Setter Property="IsFlashing" Value="True" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsPending}" Value="True">
<Setter Property="IsFlashing" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Border HorizontalAlignment="Center"
Margin="5"
Height="100"
Name="Border"
VerticalAlignment="Center"
Width="100">
<Border.Resources>
<Storyboard x:Key="FlashingStoryboard"
AutoReverse="True"
RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:00.5"
Storyboard.TargetName="Border"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)">
<DiscreteColorKeyFrame KeyTime="00:00:00.25" Value="Black" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Border.Resources>
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderBrush" Value="Black" />
<Setter Property="BorderThickness" Value="2" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
<Setter Property="BorderThickness" Value="4" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsPending}" Value="True">
<Setter Property="BorderThickness" Value="4" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="FlashStates">
<VisualState x:Name="FlashingOn"
Storyboard="{StaticResource ResourceKey=FlashingStoryboard}" />
<VisualState x:Name="FlashingOff" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Grid.Row="0"
Name="AlarmImage"
Source="{Binding Path=Image, RelativeSource={RelativeSource AncestorType={x:Type cs:AlarmItem}}}"
Stretch="Fill" />
<cs:ResponseTimer Expired="Timer_Expired"
Grid.Row="1"
HideIfExpired="True"
IsTabStop="False"
MinHeight="10"
x:Name="TheTimer"
TimeoutPeriod="00:02:30"
VerticalAlignment="Bottom" />
</Grid>
</Border>
</UserControl>
The application receives data from a proprietary device my company manufactures. The objects are received and loaded into a View Model class instance. A new instance of this control is created for each object received, a reference to the view model is put into the new control instance's DataContext property, and the new control is added to a ListBox in a Window.
The animation is supposed to run while the object's status property is a particular value. There's a second status value where the animation is also supposed to run, and the status changes to that value after a fixed time interval in which the user has not responded to that item. The animation is only supposed to stop when the status takes on a value that can only be set by user interaction.
When the first object is received & displayed, the animation works fine. If no further objects are received, the animation keeps running & the border color changes as intended when the timer expires.
However, the animation just stops, on its own, after 2 or more objects are received. This is both before the timer that changes the status that causes the border color to change expires and before any user action is taken. Note that it doesn't always stop on the second object, it sometimes takes receiving 3 or 4 objects before the animation stops.
Does anyone have any idea why the animation stops? How do I keep each one running to the end? Is there a better way to get the same effect that doesn't have this problem?
The closest scenario I've had to this is triggering an animation based upon a value in the view model. I'm taking the 'better way' option here so apologies if this doesn't fit with your solution, I'm just basing it on what I see.
Now you mention a new control is created for each alert that comes from your external device? So I'm going to assume this control is bound to a single instance and presented in something like a listbox.
If you're still with me, your control instance has it's own view model instance and by the looks of it, your triggering from an IsPending and IsExpired property such that when either is true, make it flash.
First thing I'd do is simplify that binding from the ViewModel adding as IsAlertRequired property which you set when you require the alert - can be updated in the setter for you existing properties. The reason here is that a single trigger is way easier than a multibinding.
Then use DataTrigger to start your storyboard. In your app add a reference to Microsoft.Expression.Interactions and then import them in your XAML:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
Then add a trigger before your layoutroot element:
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding ViewModel.IsAlertRequired}" Value="True">
<ei:ControlStoryboardAction ControlStoryboardOption="Play" Storyboard="{StaticResource FlashingAnimation}"/>
</ei:DataTrigger>
That should start your animation based upon a view model data binding.
Assuming you get that far and it works for starting your animation, if it still stops then I would look into whether something within your viewmodel or external component is blocking the UI thread.
Hope this helps.
Thanks to information from kidshaw, I was able to figure out a solution that works. Essentially, the animation is supposed to switch the Border control's BorderBrush back and forth between two colors, black and a color that depends upon the properties of the object in the DataContext. This logic is in the code behind because the logic depends on two different properties of the data context object and I couldn't come up with a XAML trigger that would work properly. It wasn't included in the original question because it didn't seem pertinent.
As mentioned in the comments to kidshaw's answer, the problem was that the animation was losing track of what color the BorderBrush was supposed to be when additional alerts came into the window. Instead of switching back & forth between Red & Black, for example, it would think it had to switch between black & black or red & red. So it looked like no animation was taking place, when it actually was.
To fix the problem, and because of the need to choose the color based on properties of the object in the data context, I ended up adding a second DiscreteColorKeyFrame to the animation for the other color. I tried to use a bind the Value property of the DiscreteColorKeyFrame to a DependencyProperty I added to the control which would be set by the code behind logic, but that didn't work. The animation kept switching back & forth between black & transparent, while the Output window in VS kept recording errors about CanFreeze being false.
So I ended up making one animation for each different color and adding one VisualState to the VisualStateManager for each color. There were changes in the code behind, too, bu tI got it all working.
Here's the XAML:
<UserControl x:Class="CarSystem.CustomControls.AlarmItem"
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:cs="clr-namespace:CarSystem.CustomControls"
mc:Ignorable="d"
DataContext="{Binding Path=Alarm, RelativeSource={RelativeSource Self}}">
<UserControl.Resources>
<Style TargetType="{x:Type cs:AlarmItem}">
<Setter Property="IsFlashing" Value="False" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
<Setter Property="IsFlashing" Value="True" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsPending}" Value="True">
<Setter Property="IsFlashing" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Border HorizontalAlignment="Center"
Margin="5"
Height="100"
Name="Border"
VerticalAlignment="Center"
Width="100">
<Border.BorderBrush>
<SolidColorBrush x:Name="AnimatedBrush" Color="Black" />
</Border.BorderBrush>
<Border.Resources>
<Storyboard x:Key="ExpiredAnimation"
AutoReverse="False"
RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:01"
Storyboard.TargetName="AnimatedBrush"
Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="00:00:00" Value="#FFFFFF78" />
<DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="HistoricalAnimation"
AutoReverse="False"
RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:01"
Storyboard.TargetName="AnimatedBrush"
Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="00:00:00" Value="#FFFFFF78" />
<DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="PendingAnimation"
AutoReverse="False"
RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:01"
Storyboard.TargetName="AnimatedBrush"
Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="00:00:00" Value="Red" />
<DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="WhiteListAnimation"
AutoReverse="False"
RepeatBehavior="Forever">
<ColorAnimationUsingKeyFrames BeginTime="00:00:00"
Duration="00:00:01"
Storyboard.TargetName="AnimatedBrush"
Storyboard.TargetProperty="Color">
<DiscreteColorKeyFrame KeyTime="00:00:00" Value="#FF5819" />
<DiscreteColorKeyFrame KeyTime="00:00:00.5" Value="Black" />
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Border.Resources>
<Border.Style>
<Style TargetType="Border">
<Setter Property="BorderThickness" Value="2" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsExpired}" Value="True">
<Setter Property="BorderThickness" Value="4" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsPending}" Value="True">
<Setter Property="BorderThickness" Value="4" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="FlashStates">
<VisualState x:Name="ExpiredState" Storyboard="{StaticResource ResourceKey=ExpiredAnimation}" />
<VisualState x:Name="HistoricalState" Storyboard="{StaticResource ResourceKey=HistoricalAnimation}" />
<VisualState x:Name="PendingState" Storyboard="{StaticResource ResourceKey=PendingAnimation}" />
<VisualState x:Name="WhiteListState" Storyboard="{StaticResource ResourceKey=WhiteListAnimation}" />
<VisualState x:Name="FlashingOff" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Image Grid.Row="0"
Name="AlarmImage"
Source="{Binding Path=Image, RelativeSource={RelativeSource AncestorType={x:Type cs:AlarmItem}}}"
Stretch="Fill" />
<cs:ResponseTimer Expired="Timer_Expired"
Grid.Row="1"
HideIfExpired="True"
IsTabStop="False"
MinHeight="10"
x:Name="TheTimer"
TimeoutPeriod="00:02:30"
VerticalAlignment="Bottom" />
</Grid>
</Border>
</UserControl>
And here's the code from the backend that picks & starts the correct animation.
private void StartStatusAnimation() {
if ( condition1 ) {
// It is. Display the WhiteListAnimation.
if ( !VisualStateManager.GoToElementState( Border, "WhiteListState", true ) ) {
// Log error
}
} else if ( condition2 ) {
if ( !VisualStateManager.GoToElementState( Border, "ExpiredState", true ) ) {
// Log error
}
} else if ( condition3 ) {
if ( !VisualStateManager.GoToElementState( Border, "HistoricalState", true ) ) {
// Log error
}
} else if ( condition4 ) {
if ( !VisualStateManager.GoToElementState( Border, "PendingState", true ) ) {
// Log error
}
} else {
// We don't know what state this is. Stop flashing now
if ( !VisualStateManager.GoToElementState( Border, "FlashingOff", true ) ) {
// Log error
}
}
}
Note that the values of the data context objects properties can change due to user interaction, or a timer expiring, which would either stop the flashing all together, in the first case, or cause the current animation to stop and another one to start, in the second. In the second case, the VisualSTateManager is just set to the FlashingOff state when the trigger in the Style sets the control's IsFlashing to false, and in the second, the above method is called again when the timer expires.
I would like to make WPF Window that contains DataGrid control and enables following scenario in C# WPF DataGrid: Data is loaded in DataGrid, application validates data in background (parallel async operations), when row is determined to be valid its bacground color becomes green, red otherwise. What is cleanest way to program this behaviour? Is there any built-in functionality in DataGrid and WPF to do this kind of validation?
EDIT:
For now I have manged to perform this by using RowStyle, but this makes application non responsive because validation takes time for each row, so I would like to make this async and parallel.
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="{Binding BgColor}">
</Setter>
</Style>
</DataGrid.RowStyle>
EDIT2:
Here is progress:
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=BgColor}" Value="DarkRed">
<Setter Property="Background" Value="DarkRed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
Code behind looks like this:
Func<List<bool>> func = () => data.AsParallel().Select(x => File.Exists(x.FullPath)).ToList();
List<bool> res = null;
IAsyncResult ar = func.BeginInvoke(new AsyncCallback(x=>
{
res = ((Func<List<bool>>)((AsyncResult)x).AsyncDelegate).EndInvoke(x);
for (int i = 0; i < res.Count; ++i)
if (!res[i])
data[i].BgColor = Brushes.DarkRed;
}), null);
Remaining problem is that row background color is refreshed only when row is redrawn (moved out of view and than into view again). Any clean and easy way to fix this?
EDIT3:
Finally everything works exactly as required, only thing missing in EDIT2 was to implement INotifyPropertyChanged in data source class.
Well the best approach would be using a DataTrigger in the style of the DataGridItems and provide a property (bool?) in the ViewModel which is bound to the DataTrigger. In the DataTrigger you could declare the visual for all three states Null, True, False
For additional information on DataTrigger, please have a look here.
Edit
Hmm, any chance to put the highlighting functionality in a DataTemplate? I implemented a highlighting for the selection state of an entity. And it works as expected.
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Value="true">
<!-- Expand -->
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard Storyboard.TargetName="CommandPanel">
<DoubleAnimation Duration="0:0:0.200" Storyboard.TargetProperty="Opacity" To="1" />
<DoubleAnimation Duration="0:0:0.150" Storyboard.TargetProperty="Height"
To="{StaticResource TargetHeightCommandPanel}" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<!-- Collapse -->
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard Storyboard.TargetName="CommandPanel">
<DoubleAnimation Duration="0:0:0.100" Storyboard.TargetProperty="Opacity" To="0" />
<DoubleAnimation Duration="0:0:0.150" Storyboard.TargetProperty="Height" To="0" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</DataTemplate.Triggers>
Btw, have you ever heard of MVVM?
below i have a control template that comprises of two text box. The control template is used as a grid's cell display template and the datacontext is a bindinglist of my model object (which implement INotifyPropertyChanged). The second text box is initially Collapsed. However when "Ask" and "Bid" price updates happen, i want to 'flash" this text box by toggling visibility for about 1 second.
The problem i'm seeing is that on initial load of the form, i do see the 2nd text box flash, but after that...nothing. Interestingly if i click on a cell grid (which activates the edit template) then click out of the cell (which reverts back to the display template, which is the template shown below), the 2nd textbox then flashes.
Can anyone see an issue why the 2nd textbox would not 'flash' when AskPrice or BidPrice changes? I do see that the converter is being invoked as expected, but the update is not triggering the storyboard animation.
<Style x:Key="PriceSpreadAlertStyle" TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed" />
<Setter Property="Background" Value="Red" />
<Style.Triggers>
<EventTrigger RoutedEvent="Binding.TargetUpdated" >
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00.2" Value="{x:Static Visibility.Visible}"/>
<DiscreteObjectKeyFrame KeyTime="00:00:00.4" Value="{x:Static Visibility.Collapsed}"/>
<DiscreteObjectKeyFrame KeyTime="00:00:00.6" Value="{x:Static Visibility.Visible}"/>
<DiscreteObjectKeyFrame KeyTime="00:00:00.8" Value="{x:Static Visibility.Collapsed}"/>
<DiscreteObjectKeyFrame KeyTime="00:00:01.0" Value="{x:Static Visibility.Visible}"/>
<DiscreteObjectKeyFrame KeyTime="00:00:01.2" Value="{x:Static Visibility.Collapsed}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
</Style>
<ControlTemplate x:Key="QuoteDisplayTemplate" >
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding QuotePrice}" />
<TextBlock Style="{StaticResource PriceSpreadAlertStyle}">
<TextBlock.Text>
<MultiBinding Converter="{StaticResource AskBidToSpreadConverter}">
<Binding Path="AskPrice" UpdateSourceTrigger="PropertyChanged" NotifyOnTargetUpdated="True" />
<Binding Path="BidPrice" UpdateSourceTrigger="PropertyChanged" NotifyOnTargetUpdated="True" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
</ControlTemplate>
Check to see that its 'getting' your value in the code behind when you do a NotifyPropertyChanged.
For the same reason, check for any binding expression errors when you initially start up.
Also, you could try changing the binding modes to oneway, I didn't think so but they could be getting set to a onetime binding preventing it from handling the changed event.
EDIT:
Have you tried using the SourceUpdated RoutedEvent instead of the TargetUpdated?
The solution that i found was to move 'NotifyOnTargetUpdated="True"' from the Binding element to the MultiBinding element.