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!
Related
I'm new to WPF and facing an issue regarding binding.
I have a MainWindow with a Rectangle that uses binding to change visual attributes. The XAML code is as follows:
<Rectangle Height="72" Canvas.Left="1011" RadiusY="6" RadiusX="6" StrokeThickness="2" Width="82" Canvas.Top="8">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Style.Triggers>
<DataTrigger Binding="{Binding BddState}" Value="True">
<Setter Property="Fill">
<Setter.Value>
<ImageBrush ImageSource="Pictures/BDD_on.png" Stretch="Uniform" />
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding BddState}" Value="False">
<Setter Property="Fill">
<Setter.Value>
<ImageBrush ImageSource="Pictures/Bdd_off.png" Stretch="Uniform" />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
I created a view model class to manage the binding:
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
private Boolean _bddstate;
public Boolean BddState
{
get { return _bddstate; }
set
{
_bddstate = value;
OnPropertyChanged("BddState");
}
}
}
The binding works well from MainWindow using:
private ViewModel _viewModel = new ViewModel();
public MainWindow()
{
InitializeComponent();
this.DataContext = _viewModel;
_viewModel.BddState = true;
}
Inside the MainWindow, I use a StackPanel to load different UserControl (the MainWindow acts as an header).
My concern is the binding from the UserControl; the BddState value changes accordingly but nothing is reflected on MainWindow UI.
At the same time, I can see that the handler of the view model is always null.
What am I doing wrong?
Thanks in advance for your support.
Update: Here is the code of the user control:
<Grid Background="#FFECECEC" Height="706" VerticalAlignment="Top">
<Label x:Name="label" Content="HOME" HorizontalAlignment="Left" Margin="525,8,0,0" VerticalAlignment="Top" FontWeight="Bold" Foreground="{DynamicResource gray_2}" FontSize="20"/>
<Label x:Name="label_Copy" Content="General Info" HorizontalAlignment="Left" Margin="441,34,0,0" VerticalAlignment="Top" Foreground="{DynamicResource gray_2}" FontSize="18"/>
<Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="441,214,0,0" VerticalAlignment="Top" Width="107" Height="36" Click="button_Click"/>
</Grid>
for the code behind, it's just the following:
public partial class Home : UserControl
{
public Home()
{
InitializeComponent();
}
private void button_Click(object sender, RoutedEventArgs e)
{
ViewModel _vm = new ViewModel();
_vm.BddState = true;
}
}
When i click the button of Home UC, the handler of ViewModel is null and the binding is not effective on MainWindow rectangle
here some update following Andy's sample (it could help)
by casting mainwindow datacontext it works:
MainWindowViewModel viewModel = App.Current.MainWindow.DataContext as MainWindowViewModel;
if (viewModel != null)
{
viewModel.BddState = true;
}
I modified your markup to:
<Canvas Name="MyCanvas" >
<Rectangle Height="72" RadiusY="6" RadiusX="6" StrokeThickness="2" Width="82" Canvas.Top="8">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Style.Triggers>
<DataTrigger Binding="{Binding BddState}" Value="True">
<Setter Property="Fill" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding BddState}" Value="False">
<Setter Property="Fill" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
</Canvas>
Which has no reliance on any image and no Canvas.Left.
I get a green rectangle.
Possible reasons you see nothing include:
Your window is narrower than 1011px and Canvas.Left="1011" means your rectangle is not within your window.
Your image paths are not working.
Uniform stretch is creating a problem and the part of the rectangle which has any picture in it is off screen.
I then modified this further to set bddstate true initially via the private backing field in the viewmodel. Added a togglebutton with ischecked bound to bddstate.
That toggles between yellow and green successfully.
Here's a working version involves a usercontrol. I've inherited MainWindowViewModel from a BaseViewModel I had in my test sample.
MainWindow:
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Canvas Name="MyCanvas" >
<Rectangle Height="72" RadiusY="6" RadiusX="6" StrokeThickness="2" Width="82" Canvas.Top="8">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Fill" Value="Green"/>
<Style.Triggers>
<DataTrigger Binding="{Binding BddState}" Value="False">
<Setter Property="Fill" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
</Canvas>
<local:UserControl1 Height="30" Width="100"/>
</Grid>
The viewmodel:
public class MainWindowViewModel : BaseViewModel
{
private Boolean _bddstate = true;
public Boolean BddState
{
get { return _bddstate; }
set
{
_bddstate = value;
RaisePropertyChanged("BddState");
}
}
UserControl1
<ToggleButton Width="100" Height="30" Content="Toggle" IsChecked="{Binding BddState, Mode=TwoWay}"/>
BaseViewModel
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
There is no code behind.
And this still works.
Okay! I did a big edition :)
In a UserControl, I need to use interactions DataTrigger like below. The reason for is that I need a storyboard (MyStory) with a bound key-frame value. (Doing so was discussed here before.)
<UserControl x:Class="WpfApplication1.UserControl2"
...
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:local="clr-namespace:WpfApplication1">
<UserControl.Resources>
<Style x:Key="MyControlStyle" TargetType="UserControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Grid.Resources>
<Storyboard x:Key="MyStory">
<ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="brdBase">
<SplineColorKeyFrame KeyTime="0:0:1" Value="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:UserControl2}}, Path=SpecialColor}"/>
</ColorAnimationUsingKeyFrames>
</Storyboard>
</Grid.Resources>
<Border x:Name="brdBase" BorderThickness="1" BorderBrush="Gray">
<TextBox Text="{Binding SpecialText}"/>
</Border>
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding SpecialText}" Value="Fire!">
<ei:ControlStoryboardAction Storyboard="{StaticResource MyStory}" ControlStoryboardOption="Play"/>
</ei:DataTrigger>
</i:Interaction.Triggers>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid x:Name="grdRoot" DataContext="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:UserControl2}}}">
<UserControl Style="{DynamicResource MyControlStyle}"/>
</Grid>
</UserControl>
C# code behind:
public partial class UserControl2 : UserControl
{
#region ________________________________________ SpecialColor
public Color SpecialColor
{
get { return (Color)GetValue(SpecialColorProperty); }
set { SetValue(SpecialColorProperty, value); }
}
public static readonly DependencyProperty SpecialColorProperty =
DependencyProperty.Register("SpecialColor",
typeof(Color),
typeof(UserControl2),
new FrameworkPropertyMetadata(Colors.Red));
#endregion
#region ________________________________________ SpecialText
public string SpecialText
{
get { return (string)GetValue(SpecialTextProperty); }
set { SetValue(SpecialTextProperty, value); }
}
public static readonly DependencyProperty SpecialTextProperty =
DependencyProperty.Register("SpecialText",
typeof(string),
typeof(UserControl2),
new FrameworkPropertyMetadata(string.Empty));
#endregion
public UserControl2()
{
InitializeComponent();
}
}
I expect the above code to play MyStory right after SpecialText set to "Fire!". To do so, we can use one of the following ways:
1.
<Grid>
<local:UserControl2 SpecialText="Fire!"/>
</Grid>
2. Typing "Fire!" in the UserControl2 text-box at run-time.
The first way doesn't affect on GUI at design-time. I have some complicated UserControls in my app and really need to solve this issue. There are some DPs that call a storyboard to change layout of the UserControl animatedly. Obviously these storyboards involved with one or more bound key-frames. So I must run my app more and more to check it :(
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 would like to invoke a command using EventTrigger when a particular key is touched (for example, the spacebar key)
Currently I have:
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyDown">
<i:InvokeCommandAction Command="{Binding DoCommand}" CommandParameter="{BindingText}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
Now how can I specify that this should occur only when the KeyDown occurs with the spacebar?
You would have to build a custom Trigger to handle that:
public class SpaceKeyDownEventTrigger : EventTrigger {
public SpaceKeyDownEventTrigger() : base("KeyDown") {
}
protected override void OnEvent(EventArgs eventArgs) {
var e = eventArgs as KeyEventArgs;
if (e != null && e.Key == Key.Space)
this.InvokeActions(eventArgs);
}
}
Another approach would be to use KeyBindings and bind them to your Window, UserControl, FrameworkElement, etc. That will not Trigger a button, but say you have a command "MyCommand" that is called from the button, you could invoke the command from InputBindings.
<UserControl.InputBindings>
<KeyBinding Command="{Binding Path=ApplyCommand}" Key="Enter"/>
<KeyBinding Command="{Binding Path=NextPage}" Modifiers="Ctrl" Key="Left"/>
</UserControl.InputBindings>
<StackPanel>
<Button IsDefault="True" Content="Apply">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding Path=ApplyCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
You could also bind these KeyBindings to a TextBox.
I like the idea with a custom trigger but I didn't managed to make it work (some methods were changed or deprecated therefore the showed above definition of the SpaceKeyDownEventTrigger is not compiled now). So, I put here the working version with custom RoutedEvent instead. The SpaceKeyDownEvent is defined in MyControl custom control and is raised from the OnKeyDown method when an unhandled KeyDown attached event reaches MyControl and the key pressed is the spacebar.
public class MyControl : ContentControl
{
// This constructor is provided automatically if you
// add a Custom Control (WPF) to your project
static MyControl()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(MyControl),
new FrameworkPropertyMetadata(typeof(MyControl)));
}
// Create a custom routed event by first registering a RoutedEventID
// This event uses the bubbling routing strategy
public static readonly RoutedEvent SpaceKeyDownEvent = EventManager.RegisterRoutedEvent(
"SpaceKeyDown",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(MyControl));
// Provide CLR accessors for the event
public event RoutedEventHandler SpaceKeyDown
{
add { AddHandler(SpaceKeyDownEvent, value); }
remove { RemoveHandler(SpaceKeyDownEvent, value); }
}
// This method raises the SpaceKeyDown event
protected virtual void RaiseSpaceKeyDownEvent()
{
RoutedEventArgs args = new RoutedEventArgs(SpaceKeyDownEvent);
RaiseEvent(args);
}
// Here KeyDown attached event is customized for the desired key
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Key == Key.Space)
RaiseSpaceKeyDownEvent();
}
}
The MyControl could be added to the template of another control, allowing the latter to use EventTrigger with the SpaceKeyDown routed event:
<Style TargetType="{x:Type local:MyControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyControl}">
<Grid>
<ContentPresenter/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Adding MyControl to the TextBox template -->
<Style TargetType="{x:Type TextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<local:MyControl>
<ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
</local:MyControl>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<EventTrigger RoutedEvent="local:MyControl.SpaceKeyDown">
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="Foreground.Color"
From="White" To="Transparent" Duration="0:0:0.066" AutoReverse="True" RepeatBehavior="3x"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
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>