I have a complicated relationship with WPF. When you know what you're doing, it's great for building desktop apps. It's much easier to handle scenarios like apps getting used across varying DPIs, or doing complex UI layouts. But there are a few bits of it which, when you don't know how to do them, aren't intuitive. I bumped into another one the other day: Needing to trigger an animation on-demand. And so I can remember this next time I need to do this, here's a quick write-up of one way to achieve it...
url copied!
What I needed was (in theory) fairly simple. I had a
TextBox
which was going to accept some user input for doing a search. But in the scenario where the program had an issue with that input (which is most likely "no matches found") I wanted it to "flash" red to indicate the code had tried and failed.
It turns out this needs two things, which each have a bunch of detail:
url copied!
In WPF, the way you construct an animation is to apply a
Storyboard
to your component. You can define these in the
Style
element of your control, or have them as shared resources. This didn't seem like a "shared" scenario, so inside the
TextBox
element you can add something like:
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Resources>
<Storyboard x:Key="flashSearchAnimation" RepeatBehavior="1x">
<ColorAnimation Storyboard.TargetProperty="(TextBox.Background).(SolidColorBrush.Color)"
From="White" To="Red" AutoReverse="False" RepeatBehavior="1x" Duration="0:0:0.125"/>
<ColorAnimation Storyboard.TargetProperty="(TextBox.Background).(SolidColorBrush.Color)"
From="Red" To="White" AutoReverse="False" RepeatBehavior="1x" Duration="0:0:0.125"/>
</Storyboard>
</Style.Resources>
...
</Style>
</TextBox.Style>
That applies a style to the
TextBox
which contains a
Storyboard. For this example, that's made up of two colour changes - from white to red, and then back from red to white. But you can have whatever changes you need there. These are set to happen once when the animation is triggered, and complete over a fraction of a second.
The
TargetProperty
here is a bit complicated, and - I'll be honest - came straight out of a search query as I had no idea how to correctly express the idea that the animation needed to change the "colour of the brush used ro the TextBox background". But StackOverflow still has some uses...
Next you need to tell the
TextBox
under what scenarios you want this animation to run. The "when the mouse enters" kind of automatic start is fairly well documented. But if you want to control it manually, there's less useful info about. To achieve it, you need to apply
Triggers
to the component:
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Resources>
... as above ...
</Style.Resources>
<Style.Triggers>
<EventTrigger RoutedEvent="local:TheWindowClass.FlashSearch">
<BeginStoryboard Storyboard="{StaticResource flashSearchAnimation}"/>
</EventTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
An
EventTrigger
can be used to start the animation when a specific event is raised. Commonly that event might be "mouse moved into component" or similar, but here it needs to be something custom. So this trigger needs two things. The
RoutedEvent
property needs to specify the event on some class that this trigger can subscribe to. And there needs to be a
BeginStoryboard
element which says "trigger the storyboard we defined above".
We'll get to defining the event in a second, but note how the
RoutedEvent
property needs three parts. First it needs the right namespace. That gets defined in the root control of your XAML file (most likely a window) alongside binding all the namespaces for bits of WPF:
<Window x:Class="ExampleProject.TheWindowClass"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ExampleProject"
...
ResizeMode="CanResizeWithGrip">
Secondly, it needs the name of the control the event is defined on - the
Window
in this example (which is the
TheWindowClass
name here). And then thirdly it needs the name of the event property, which is
FlashSearch
here.
We need to define that property next.
url copied!
Declaring a custom event needs two things. First you have to register the existence of your custom event with the runtime:
public static readonly RoutedEvent FlashSearchEvent = EventManager.RegisterRoutedEvent(
name: "FlashSearch",
routingStrategy: RoutingStrategy.Direct,
handlerType: typeof(RoutedEventHandler),
ownerType: typeof(TheWindowClass));
There are three ways you can make events travel through the system. I've picked
RoutingStrategy.Direct
here because I want to trigger the event directly on the component in question. But if you need one event to affect multiple components you may want one of the other options available here, so the event can
Bubble
up the component tree, or
Tunnel
down through it.
The critical thing with this declaration though is that the
ownerType
property needs to match the class you mentioned in the WPF above for the
EventTrigger
and its
RoutedEvent property. I didn't realise that first time around, and it took me some time to realise this is what was causing my code to fail.
And secondly you need to expose a C# event property to allow subscribing and unsubscribing from this custom event. And the name of this custom property also needs to match what's in your
RoutedEvent
property:
public event RoutedEventHandler FlashSearch
{
add { this.AddHandler(FlashSearchEvent, value); }
remove { this.RemoveHandler(FlashSearchEvent, value); }
}
This is fairly boilerplate - it just ensures that when someone does the equivalent of
FlashSearch += someHandler
then that handler gets passed over to the
RoutedEvent
definition above.
Overall the
EventTrigger
RoutedEvent
property in the XAML is a little unintuitive to me - it's it's referring to a class (TheWindowClass) but an instance property (FlashSearch) which something you'd not normally write - but I guess this is being used for reflection internally, so it does end up bound to the instance. But however they achieve it, it does work.
url copied!
So finally you need to fire off your event. At the relevant point in your code you can put:
var e = new RoutedEventArgs(FlashSearchEvent); searchEdit.RaiseEvent(e);
To declare an instance of
RoutedEventArgs
(which specifies what event you're raising) you need to pass it the static
RoutedEvent
defined above.
Then you raise that event object against whatever object you need. As mentioned earlier, I wanted it raised directly against my
TextBox, but you might raise it against the window or elsewhere if that suits your requirement. But in that circumstance, consider how the event propagates through the controls, as mentioned above.
And with that in place, the flashing will now work:
Poor screen-cap, but success none the less.
↑ Back to top