How to Animate a ContentControl in a ItemsControl - wpf

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>

Related

Set Target of Storyboard to Button inside a DataTemplate WPF

We are trying to set Height and Width of the button based on the window size. We want to achieve this using Visual States. We want to set Storyboard Target to a button which is inside a DataTemplate. Directly setting target by name using StoryBoard.TargetName is not possible because of the namescope of DataTemplate. Is there any way to do this in XAML.
Refer to-
XAML:
<UserControl x:Class="ResizeSampleApp.Controls.ResizeUserControl"
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:ResizeSampleApp.Controls"
mc:Ignorable="d"
SizeChanged="CurrentWindow_SizeChanged"
x:Name="DashBoard"
>
<Grid x:Name="grid">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ResizeStates">
<VisualState x:Name="FirstHorizontalBreakpoint">
<Storyboard >
<DoubleAnimation To="116" Storyboard.TargetName="TargetBtn" Storyboard.TargetProperty="Height"></DoubleAnimation>
<DoubleAnimation To="182" Storyboard.TargetName="TargetBtn" Storyboard.TargetProperty="Width"></DoubleAnimation>
</Storyboard>
</VisualState>
<VisualState x:Name="MinSize">
<Storyboard>
<DoubleAnimation To="100" Storyboard.TargetName="TargetBtn" Storyboard.TargetProperty="Height"></DoubleAnimation>
<DoubleAnimation To="197" Storyboard.TargetName="TargetBtn" Storyboard.TargetProperty="Width"></DoubleAnimation>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<StackPanel Orientation="Vertical">
<Grid Margin="10">
<ItemsControl Name="icTodoList">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button x:Name="TargetBtn" Content="{Binding Title}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</StackPanel>
</Grid>
</UserControl>
C#:
public class TodoItem
{
public string Title { get; set; }
}
public partial class ResizeUserControl : UserControl
{
public ResizeUserControl()
{
InitializeComponent();
List<TodoItem> items = new List<TodoItem>();
items.Add(new TodoItem() { Title = "Testing 1 " });
items.Add(new TodoItem() { Title = "Testing 2" });
items.Add(new TodoItem() { Title = "Testing 3" });
icTodoList.ItemsSource = items;
}
public void CurrentWindow_SizeChanged(object sender, SizeChangedEventArgs sizeChangedEventArgs)
{
if (this.ActualWidth > 847)
{
VisualStateManager.GoToState(this.DashBoard, "FirstHorizontalBreakpoint", false);
}
else
{
VisualStateManager.GoToState(this.DashBoard, "MinSize", false);
}
}
}
Thanks in Advance.
It is not possible to use visual state animations to animate properties inside a DataTemplate. The DataTemplate is just a template, that is not part of the visual tree, the moment the Storyboard is defined. The template will be duplicated for each templated target e.g each item container. This means there will be multiple buttons with the name TargetBtn - when viewed from outside the template's scope. So you would have to move the animations to the DataTemplate scope, which means inside the template.
In addition to the control's visual state definitions, you should add properties to reflect the visual state as object state.
It is best practice and recommended by Microsoft Docs to do this for each state: add properties like IsMouseOver to indicate the MouseOver state or IsFocused which goes in tandem with the Focused state, just to name some framework examples.
This allows your control to express the visual state as real instance state, which can be consumed outside the context of the control's direct content - visual states are only consumable by the VisualStateManager.
You can then use this usually public read-only dependency properties to trigger e.g. a DataTrigger, which you define inside the DataTemplate to animate the template's content:
ResizeUserControl.xaml.cs
public partial class ResizeUserControl : UserControl
{
#region IsExpandedViewEnabled read-only dependency property
protected static readonly DependencyPropertyKey IsExpandedViewEnabledPropertyKey = DependencyProperty.RegisterReadOnly(
"IsExpandedViewEnabled",
typeof(bool),
typeof(ResizeUserControl),
new PropertyMetadata(default(bool)));
public static readonly DependencyProperty IsExpandedViewEnabledProperty = MyControl.IsExpandedViewEnabledPropertyKey.DependencyProperty;
public bool IsExpandedViewEnabled
{
get => (bool) GetValue(MyControl.IsExpandedViewEnabledProperty);
private set => SetValue(MyControl.IsExpandedViewEnabledPropertyKey, value);
}
#endregion IsExpandedViewEnabled read-only dependency property
public ResizeUserControl()
{
InitializeComponent();
}
#region Overrides of FrameworkElement
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
base.OnRenderSizeChanged(sizeInfo);
if (this.ActualWidth > 847)
{
VisualStateManager.GoToState(this, "FirstHorizontalBreakpoint", false);
this.IsExpandedViewEnabled = true;
}
else
{
VisualStateManager.GoToState(this, "MinSize", false);
this.IsExpandedViewEnabled = false;
}
}
}
ResizeUserControl.xaml
<UserControl>
<Grid x:Name="grid">
<StackPanel Orientation="Vertical">
<Grid Margin="10">
<ItemsControl Name="icTodoList"
ItemsSource="{Binding DataItems}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button x:Name="TargetBtn" Height="60"
Content="{Binding Title}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=IsExpandedViewEnabled}"
Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="116" Storyboard.TargetName="TargetBtn" Storyboard.TargetProperty="Height" />
<DoubleAnimation To="182" Storyboard.TargetName="TargetBtn" Storyboard.TargetProperty="Width" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="100" Storyboard.TargetName="TargetBtn" Storyboard.TargetProperty="Height" />
<DoubleAnimation To="197" Storyboard.TargetName="TargetBtn" Storyboard.TargetProperty="Width" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</StackPanel>
</Grid>
</UserControl>

Firing a storyboard in WPF?

I have many of theses:
<Image x:Name="Foo" Grid.Column="0" Grid.Row="0" Source="1.png" Style="{StaticResource imageStyle}"
ToolTipService.InitialShowDelay="0" ToolTipService.ShowDuration="360000" ToolTipService.BetweenShowDelay="10000" ToolTip="fffffff"/>
<Image x:Name="Foo2" Grid.Column="1" Grid.Row="0" Source="2.png" Style="{StaticResource imageStyle}"
ToolTipService.InitialShowDelay="0" ToolTipService.ShowDuration="360000" ToolTipService.BetweenShowDelay="10000" ToolTip="eeeeeeeee"/>
<Image x:Name="Foo3" Grid.Column="2" Grid.Row="0" Source="3.png" Style="{StaticResource imageStyle}"
ToolTipService.InitialShowDelay="0" ToolTipService.ShowDuration="360000" ToolTipService.BetweenShowDelay="10000" ToolTip="ddddddddddddd"/>
And right now my animation storyboard only fires when someone clicks the Foo image.
<Grid.Triggers>
<EventTrigger RoutedEvent="Image.MouseDown" SourceName="Foo">
<BeginStoryboard Name="mySlider">
<Storyboard>
<ThicknessAnimation Storyboard.TargetName="contentHolder"
Storyboard.TargetProperty="Margin"
Duration="0:0:1" From="0 0 0 0" To="-800 0 0 0" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Grid.Triggers>
How can I make the animation fire if someone clicks on any one of the three images above? The thing they share in common is the style, so maybe there's some way to use that?
Any suggestions?
If the Grid with trigger is a parent for all that images, then MouseDown event will bubble up to it from every Image anyway, so all you have to do is to remove SourceName setting from trigger.
Otherwise you can set SourceName to Image`s parent.
If I understand your problem correctly, it is that you don't want to repeat the same EventTrigger multiple times, once for each source. If you leave out the SourceName then your animation will start for any unhandled MouseDown routed event even if it is not from one of your images (since MouseDown is a very generic bubbling routed event).
One solution would be to have a custom Image class (perhaps called MyImage) which will detect when a MouseDown event happens, and in response fire a very custom RoutedEvent (say MyImageRoutedEvent). Your EventTrigger then could listen for MyImageRoutedEvent instead, since only MyImage can fire this event. Hence your animation only runs if the MouseDown event is from one of your MyImage instances.
Alternatively, you could achieve this behaviour through an attached behaviour. The idea would be that the behaviour is configured to intercept a specified event (via an attached property) and when that event is fired from an element participating in the behaviour, the event is marked as handled and a new custom event is fired instead. Your EventTrigger would then listen for the new custom event.
Example XAML:
<StackPanel>
<StackPanel.Resources>
<Style x:Key="rectangleStyle" TargetType="{x:Type Rectangle}">
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="100" />
<Setter Property="l:EventInterceptBehaviour.OriginalRoutedEvent" Value="UIElement.MouseDown" />
</Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ContentControl}">
<StackPanel>
<Rectangle Style="{StaticResource rectangleStyle}" Fill="Red" />
<Rectangle Style="{StaticResource rectangleStyle}" Fill="Green" />
<Rectangle Style="{StaticResource rectangleStyle}" Fill="Blue" />
<Border BorderBrush="Black" x:Name="contentBorder">
<ContentPresenter HorizontalAlignment="Center"/>
</Border>
</StackPanel>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="l:EventInterceptBehaviour.InterceptedEvent">
<BeginStoryboard Name="mySlider">
<Storyboard>
<ThicknessAnimation Storyboard.TargetName="contentBorder"
Storyboard.TargetProperty="BorderThickness"
Duration="0:0:1" To="10" FillBehavior="Stop" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
<ContentControl Content="Content Placeholder" />
</StackPanel>
Example behaviour:
public static class EventInterceptBehaviour
{
#region InterceptedEvent Attached Routed Event
public static readonly RoutedEvent InterceptedEventEvent = EventManager.RegisterRoutedEvent("InterceptedEvent",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(EventInterceptBehaviour));
public static void AddInterceptedEventHandler(DependencyObject d, RoutedEventHandler handler)
{
if (d is FrameworkElement)
{
var element = (FrameworkElement)d;
element.AddHandler(InterceptedEventEvent, handler);
}
}
public static void RemoveInterceptedEventHandler(DependencyObject d, RoutedEventHandler handler)
{
if (d is FrameworkElement)
{
var element = (FrameworkElement)d;
element.RemoveHandler(InterceptedEventEvent, handler);
}
}
#endregion
#region OriginalRoutedEvent Attached Dependency Property
public static void SetOriginalRoutedEvent(FrameworkElement element, RoutedEvent value)
{
element.SetValue(OriginalRoutedEventProperty, value);
}
public static RoutedEvent GetOriginalRoutedEvent(FrameworkElement element)
{
return (RoutedEvent)element.GetValue(OriginalRoutedEventProperty);
}
public static readonly DependencyProperty OriginalRoutedEventProperty =
DependencyProperty.RegisterAttached("OriginalRoutedEvent", typeof(RoutedEvent),
typeof(EventInterceptBehaviour),
new FrameworkPropertyMetadata(OnOriginalRoutedEventPropertyChanged));
#endregion
private static void OnOriginalRoutedEvent(object sender, RoutedEventArgs e)
{
var element = (FrameworkElement)sender;
element.RaiseEvent(new RoutedEventArgs(InterceptedEventEvent, element));
e.Handled = true;
}
private static void OnOriginalRoutedEventPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is FrameworkElement)
{
var element = (FrameworkElement)d;
element.AddHandler((RoutedEvent)e.NewValue, new RoutedEventHandler(OnOriginalRoutedEvent));
}
}
}
In this example, a style is applied to each Rectangle and the attached behaviour is configured to intercept the MouseDown routed event, and replace this event with the InterceptedEvent. The animation then only runs if an InterceptedEvent is fired.
Hope this helps!

Dependency Property Value Precedence and animations

I am animating some property using DoubleAnimation. Before animation is triggered any local or Setter changes are properly reflected in the property. After animation completes nothing seems to be able to change the value of the property. I have even tried ClearValue and InvalidateProperty as well set calling SetValue but the value leftover from animation persists. If animation is repeated, the property continues to be animated as expected so it only appears to be locked for non-animation changes.
Is there a way to rectify this behavior? I want to use the animation to change the property value but still be able to change it manually or via a Setter to anything else. I know a thing or two about Dependency Property Value Precedence but the behavior I am currently experiencing is a bit strange. I'd hate to have to use "manual animations".
EDIT: Added sample XAML + code.
<Window x:Class="WpfApplication7.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300"
x:Name="_this"
Background="Red">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Center" >
<Button Click="ToggleOnClick">Toggle!</Button>
<Button Click="SetHalfOnClick">Set to 0.5!</Button>
</StackPanel>
<TextBox DockPanel.Dock="Bottom" IsReadOnly="True" Text="{Binding ElementName=_viewbox,Path=Opacity}" />
<Viewbox x:Name="_viewbox">
<Viewbox.Style>
<Style TargetType="{x:Type FrameworkElement}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsToggled,ElementName=_this,Mode=OneWay}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="0.2"
Duration="0:0:0.5"
Storyboard.TargetProperty="(UIElement.Opacity)" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="1"
Duration="0:0:0.5"
Storyboard.TargetProperty="(UIElement.Opacity)" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Viewbox.Style>
<TextBlock Text="Sample!" />
</Viewbox>
</DockPanel>
</Window>
Here is the code:
using System.Windows;
namespace WpfApplication7
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1
{
public bool IsToggled
{
get { return (bool)GetValue(IsToggledProperty); }
set { SetValue(IsToggledProperty, value); }
}
public static readonly DependencyProperty IsToggledProperty = DependencyProperty.Register("IsToggled", typeof(bool), typeof(Window1), new UIPropertyMetadata(false));
public Window1()
{
InitializeComponent();
}
private void ToggleOnClick(object sender, RoutedEventArgs e)
{
IsToggled = !IsToggled;
}
private void SetHalfOnClick(object sender, RoutedEventArgs e)
{
_viewbox.Opacity = 0.5;
}
}
}
Edit 2 in response to comments:
In your example you can work around the problem by:
Setting FillBehaviour to Stop on the animation
Adding a handler in code to the Completed event:
<Storyboard Completed="FadeOut_Completed">
Finally, set the desired 'final' value in the Completed handler (either explicitly or by using the current value of the property
private void FadeOut_Completed(object sender, EventArgs e)
{
_viewbox.Opacity = _viewbox.Opacity; //this sets the DP value to the animated value
}
This works in your sample; hopefully it will work in your problem!
Original Answer
If you set the FillBehaviour property of the Storyboard to Stop (instead of the default value of HoldEnd) it will revert to the pre-animation value of the property once the animation completes. HoldEnd causes the animation to maintain its final value on the property
Update in response to comments:
As noted in the comments, the animation value will override the value set against the property when HoldEnd is specified as the FillBehaviour.
This makes it slightly tricky to set the value to something else.
I am not sure if there is a better way to achieve this, but the example below shows one way to work around it. Its hard to judge how applicable this is without a sample usage from the OP, but in this example I am animating the width of a Rectangle on load, and then resetting it to another value when a button is clicked:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Triggers>
<EventTrigger RoutedEvent="Window.Loaded">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Target" Storyboard.TargetProperty="Width"
From="10" To="100" Duration="0:00:01" FillBehavior="HoldEnd" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Window.Triggers>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Rectangle Height="10" Width="10" Fill="Red" x:Name="Target"/>
<Button Grid.Row="1" Content="Resize">
<Button.Triggers>
<EventTrigger RoutedEvent="Button.Click">
<BeginStoryboard>
<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="Target" Storyboard.TargetProperty="Width">
<DiscreteDoubleKeyFrame Value="50" KeyTime="0:00:00" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Button.Triggers>
</Button>
</Grid>
</Window>
This works because the new animation overrides the value set in the original.
You can use UIElement.BeginAnimation with the animation parameter set to null. It will clear all animations attached to your property.

WPF - Making an animation's execution conditional on a property of the bound data item

I have a data object -- a custom class called Notification -- that exposes a IsCritical property. The idea being that if a notification will expire, it has a period of validity and the user's attention should be drawn towards it.
Imagine a scenario with this test data:
_source = new[] {
new Notification { Text = "Just thought you should know" },
new Notification { Text = "Quick, run!", IsCritical = true },
};
The second item should appear in the ItemsControl with a pulsing background. Here's a simple data template excerpt that shows the means by which I was thinking of animating the background between grey and yellow.
<DataTemplate DataType="Notification">
<Border CornerRadius="5" Background="#DDD">
<Border.Triggers>
<EventTrigger RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
From="#DDD" To="#FF0" Duration="0:0:0.7"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</DataTemplate>
What I'm unsure about is how to make this animation conditional upon the value of IsCritical. If the bound value is false, then the default background colour of #DDD should be maintained.
The final part of this puzzle is... DataTriggers. All you have to do is add one DataTrigger to your DataTemplate, bind it to IsCritical property, and whenever it's true, in it's EnterAction/ExitAction you start and stop highlighting storyboard. Here is completely working solution with some hard-coded shortcuts (you can definitely do better):
Xaml:
<Window x:Class="WpfTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Notification Sample" Height="300" Width="300">
<Window.Resources>
<DataTemplate x:Key="NotificationTemplate">
<Border Name="brd" Background="Transparent">
<TextBlock Text="{Binding Text}"/>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsCritical}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard Name="highlight">
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="(Panel.Background).(SolidColorBrush.Color)"
Storyboard.TargetName="brd"
From="#DDD" To="#FF0" Duration="0:0:0.5"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<StopStoryboard BeginStoryboardName="highlight"/>
</DataTrigger.ExitActions>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ItemsControl ItemsSource="{Binding Notifications}"
ItemTemplate="{StaticResource NotificationTemplate}"/>
<Button Grid.Row="1"
Click="ToggleImportance_Click"
Content="Toggle importance"/>
</Grid>
</Window>
Code behind:
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
namespace WpfTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new NotificationViewModel();
}
private void ToggleImportance_Click(object sender, RoutedEventArgs e)
{
((NotificationViewModel)DataContext).ToggleImportance();
}
}
public class NotificationViewModel
{
public IList<Notification> Notifications
{
get;
private set;
}
public NotificationViewModel()
{
Notifications = new List<Notification>
{
new Notification
{
Text = "Just thought you should know"
},
new Notification
{
Text = "Quick, run!",
IsCritical = true
},
};
}
public void ToggleImportance()
{
if (Notifications[0].IsCritical)
{
Notifications[0].IsCritical = false;
Notifications[1].IsCritical = true;
}
else
{
Notifications[0].IsCritical = true;
Notifications[1].IsCritical = false;
}
}
}
public class Notification : INotifyPropertyChanged
{
private bool _isCritical;
public string Text { get; set; }
public bool IsCritical
{
get { return _isCritical; }
set
{
_isCritical = value;
InvokePropertyChanged("IsCritical");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void InvokePropertyChanged(string name)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}
Hope this helps :).
What I would do is create two DataTemplates and use a DataTemplateSelector. Your XAML would be something like:
<ItemsControl
ItemsSource="{Binding ElementName=Window, Path=Messages}">
<ItemsControl.Resources>
<DataTemplate
x:Key="CriticalTemplate">
<Border
CornerRadius="5"
Background="#DDD">
<Border.Triggers>
<EventTrigger
RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
From="#DDD"
To="#FF0"
Duration="0:0:0.7"
AutoReverse="True"
RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
<TextBlock
Text="{Binding Path=Text}" />
</Border>
</DataTemplate>
<DataTemplate
x:Key="NonCriticalTemplate">
<Border
CornerRadius="5"
Background="#DDD">
<TextBlock
Text="{Binding Path=Text}" />
</Border>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplateSelector>
<this:CriticalItemSelector
Critical="{StaticResource CriticalTemplate}"
NonCritical="{StaticResource NonCriticalTemplate}" />
</ItemsControl.ItemTemplateSelector>
And the DataTemplateSelector would be something similar to:
class CriticalItemSelector : DataTemplateSelector
{
public DataTemplate Critical
{
get;
set;
}
public DataTemplate NonCritical
{
get;
set;
}
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
Message message = item as Message;
if(item != null)
{
if(message.IsCritical)
{
return Critical;
}
else
{
return NonCritical;
}
}
else
{
return null;
}
}
}
This way, WPF will automatically set anything that is critical to the template with the animation, and everything else will be the other template. This is also generic in that later on, you could use a different property to switch the templates and/or add more templates (A Low/Normal/High importance scheme).
It seems to be an odity with ColorAnimation, as it works fine with DoubleAnimation. You need to explicity specify the storyboards "TargetName" property to work with ColorAnimation
<Window.Resources>
<DataTemplate x:Key="NotificationTemplate">
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsCritical}" Value="true">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
Storyboard.TargetName="border"
From="#DDD" To="#FF0" Duration="0:0:0.7"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</DataTemplate.Triggers>
<Border x:Name="border" CornerRadius="5" Background="#DDD" >
<TextBlock Text="{Binding Text}" />
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl x:Name="NotificationItems" ItemsSource="{Binding}" ItemTemplate="{StaticResource NotificationTemplate}" />
</Grid>
Here's a solution that only starts the animation when the incoming property update is a certain value. Useful if you want to draw the user's attention to something with the animation, but afterwards the UI should return to it's default state.
Assuming IsCritical is bound to a control (or even an invisible control) you add NotifyOnTargetUpdated to the binding and tie an EventTrigger to the Binding.TargetUpdated event. Then you extend the control to only fire the TargetUpdated event when the incoming value is the one you are interested in. So...
public class CustomTextBlock : TextBlock
{
public CustomTextBlock()
{
base.TargetUpdated += new EventHandler<DataTransferEventArgs>(CustomTextBlock_TargetUpdated);
}
private void CustomTextBlock_TargetUpdated(object sender, DataTransferEventArgs e)
{
// don't fire the TargetUpdated event if the incoming value is false
if (this.Text == "False") e.Handled = true;
}
}
and in the XAML file ..
<DataTemplate>
..
<Controls:CustomTextBlock x:Name="txtCustom" Text="{Binding Path=IsCritical, NotifyOnTargetUpdated=True}"/>
..
<DataTemplate.Triggers>
<EventTrigger SourceName="txtCustom" RoutedEvent="Binding.TargetUpdated">
<BeginStoryboard>
<Storyboard>..</Storyboard>
</BeginStoryboard>
</EventTrigger>
</DataTemplate.Triggers>
</DataTemplate>
You use style triggers in this case. (I'm doing this from memory so there might be some bugs)
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding IsCritical}" Value="true">
<Setter Property="Triggers">
<Setter.Value>
<EventTrigger RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard>
<ColorAnimation
Storyboard.TargetProperty="Background.Color"
From="#DDD" To="#FF0" Duration="0:0:0.7"
AutoReverse="True" RepeatBehavior="Forever" />
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>

WPF Frame in separate thread?

I have an application where when a person types or selects a listbox there's a portion of the screen that dynamically updates to a new view.
The problem is since WPF runs everything in a single thread the displaying of the view can interfer with typing or navigating making the app less responsive. What i'd like to do is run the view portion in a different thread.
My first thought was to use a window running on a different thread, but more than being something of a hack there's the problem of the window losing focus and being placed behind the mainwindow when the mainwindow is clicked. I could make it topmost but I also need to place other windows in front of it.
So what's the best way to achieve this, can I place the view in a frame and run it in a different thread?
You can load / generate the data in a backround thread and then update the UI using Dispatcher.BeginInvoke.
I would propose you use the Visibility property of this piece of the screen that you want to make appear and use a trigger to set it from Invisible or Collapsed to Visible whenever the user types or selecs. Or you can animate the Opacity property to produce a cool fading effect ;-) I will add some code to illustrate the point.
EDIT: a time consuming backgroundtask, like File operations, can be accomplished using a BackgroundWorker
<Window x:Class="VisibleOnTypingSpike.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label Name="TypingSnooper"
Visibility="{Binding TypingSnooperVisibility}">
You are typing!</Label>
<Label>
<Label.Style>
<Style>
<Setter Property="Label.Opacity" Value="0"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasListBoxNewSelection}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard >
<Storyboard>
<DoubleAnimation From="0" To="1"
Duration="0:0:1"
Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard >
<Storyboard>
<DoubleAnimation From="1" To="0"
Duration="0:0:1"
Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
You selected!
</Label>
</StackPanel>
<TextBox TextChanged="TextBox_TextChanged"></TextBox>
<ListBox Name="SimpleListBox"
SelectionChanged="SimpleListBox_SelectionChanged">
<ListBoxItem>1</ListBoxItem>
<ListBoxItem>2</ListBoxItem>
</ListBox>
</StackPanel>
using System.Windows;
using System.Windows.Controls;
namespace VisibleOnTypingSpike
{
public partial class Window1 : Window
{
public Visibility TypingSnooperVisibility
{
get { return (Visibility)GetValue(TypingSnooperVisibilityProperty); }
set { SetValue(TypingSnooperVisibilityProperty, value); }
}
public static readonly DependencyProperty TypingSnooperVisibilityProperty =
DependencyProperty.Register("TypingSnooperVisibility",
typeof(Visibility),
typeof(Window1),
new UIPropertyMetadata(System.Windows.Visibility.Collapsed));
public bool HasListBoxNewSelection
{
get { return (bool)GetValue(HasListBoxNewSelectionProperty); }
set { SetValue(HasListBoxNewSelectionProperty, value); }
}
public static readonly DependencyProperty HasListBoxNewSelectionProperty =
DependencyProperty.Register("HasListBoxNewSelection",
typeof(bool),
typeof(Window1),
new UIPropertyMetadata(false));
public Window1()
{
InitializeComponent();
DataContext = this;
}
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var textbox = (TextBox) sender;
if (textbox.Text.Length > 0) TypingSnooperVisibility = Visibility.Visible;
else TypingSnooperVisibility = Visibility.Hidden;
}
private void SimpleListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
HasListBoxNewSelection = true;
HasListBoxNewSelection = false;
}
}
}

Resources