views:

889

answers:

1

I'm using a WPF Expander to display a number of analog process variables.

I want to make the expander's border 'glow' (or flash) when one of these variables enters a 'warning' or 'alarm' state.

To achieve this, I'm using some data triggers bound to a couple of boolean properties in my view model ('AnalogWarningActive' and 'AnalogAlarmActive'). The data triggers kick off a storyboard that animates the expander border opacity.

The data triggers work as I'd expect: the proper border colour appears and the opacity animation begins. However, there are 2 problems:

  1. The opacity of the entire expander (and all contained controls) is changing and not just the opacity of its border.

  2. When the 'AnalogWarningActive' and 'AnalogAlarmActive' tags return to False, the border disappears but the opactiy animation continues indefinitely (ie. the entire expander continues to fade in and out).

Here is the xaml I'm using:

<SolidColorBrush x:Key="AnalogAlarmBrush" Color="#FFFF8080" />
<SolidColorBrush x:Key="AnalogWarningBrush" Color="#FFFFFF80" />

<Storyboard x:Key="AlarmBorderFlasher" AutoReverse="True" RepeatBehavior="Forever">
    <DoubleAnimation  
        Storyboard.TargetProperty="(Border.Opacity)" 
        From="1.0" To="0.4" 
        Duration="0:0:0.8" />
</Storyboard>

<Expander Header="Test Data" IsExpanded="True">
    <Expander.Style>
        <Style TargetType="{x:Type Expander}">
            <Style.Triggers>

                <DataTrigger Binding="{Binding Path=AnalogWarningActive}" Value="True" >

                    <DataTrigger.EnterActions>
                        <BeginStoryboard Name="WarningBorderStoryboard" Storyboard="{StaticResource AlarmBorderFlasher}" />
                    </DataTrigger.EnterActions>
                    <DataTrigger.ExitActions>
                        <StopStoryboard BeginStoryboardName="WarningBorderStoryboard" />
                    </DataTrigger.ExitActions>

                    <DataTrigger.Setters>
                        <Setter Property="BorderBrush" Value="{StaticResource AnalogWarningBrush}" />
                        <Setter Property="BorderThickness" Value="4" />
                    </DataTrigger.Setters>

                </DataTrigger>

                <DataTrigger Binding="{Binding Path=AnalogAlarmActive}" Value="True" >

                    <DataTrigger.EnterActions>
                        <BeginStoryboard Name="AlarmBorderStoryboard" Storyboard="{StaticResource AlarmBorderFlasher}" />
                    </DataTrigger.EnterActions>
                    <DataTrigger.ExitActions>
                        <StopStoryboard BeginStoryboardName="AlarmBorderStoryboard" />
                    </DataTrigger.ExitActions>

                    <DataTrigger.Setters>
                        <Setter Property="BorderBrush" Value="{StaticResource AnalogAlarmBrush}" />
                        <Setter Property="BorderThickness" Value="4" />
                    </DataTrigger.Setters>

                </DataTrigger>

            </Style.Triggers>
        </Style>
    </Expander.Style>

    <!-- snipped the contents of the expander (a tabcontrol and a few text boxes, labels, etc)-->

</Expander>
+1  A: 

Question #1

The Opacity setting on a Visual such as Border affects that Visual and all its descendants. That is why setting Border.Opacity makes everything disappear.

You have two choices: 1. Animate the border Stroke property, or 2. Change the content to not be a descendant of Border.

Animating the Stroke property is trivial to code, but has the disadvantage that not all brushes are easily animatable to transparent and back. For example, this is difficult with gradient brushes. Also, if your borders were a variety of colors and you didn't want to go to 100% transparent there is no good way to animate the Stroke without changing the color.

Changing the content to not be a descendant of Border is very simple, and is what I would be inclined to do in most cases. Simply replace:

<Border x:Name="MyBorder" Stroke="Red" StrokeThickness="3" CornerRadius="6">
  <my:ContentHere />
</Border>

with this:

<Grid>
  <Border x:Name="MyBorder" Stroke="Red" StrokeThickness="3" Stroke="Red" CornerRadius="6">
  <Border Stroke="Transparent" StrokeThickness="3" CornerRadius="Red">
    <my:ContentHere />
  </Border>
</Grid>

Now the visible border's opacity can be animated, while the transparent one controls the layout and clipping of the child.

If you don't have any CornerRadius or other funny business, you can just set a margin on the content and forego the transparent border:

<Grid>
  <Border x:Name="MyBorder" Stroke="Red" StrokeThickness="3" />
  <my:ContentHere Margin="3" />
</Grid>

Question #2

At first glance I don't see any problem with your XAML that would cause the animation to keep running after the triggering value goes back to false, but I didn't look very closely. From what I noticed I would think it would stop the animation at the current value, not leave it running.

You might try replacing StopStoryboard with RemoveStoryboard. RemoveStoryboard should reset the animated properties back to their original values.

Ray Burns
This works perfectly. Implementing your suggestion for question #1 seemed to fix #2 as well. Thanks!