Click here to Skip to main content
15,886,806 members
Please Sign up or sign in to vote.
5.00/5 (1 vote)
See more:
My WPF application is a monitor for production systems. The main window displays one of 3 Controls showing connection status (DOWN, LOADING, or UP). The UP Control has several high-level Button shapes (an Ellipse in below example), representing various elements of the production system (Router, Config, Internal Connections, External Connections, ...). The color of the object reflects the "worst" status of its children (or grandchildren, or great-grandchildren ...). It will also flash/blink (via Storyboard animation) if one of the children changes status.

Each Button shape maps to a ColorStatus object:
C#
public class ColorStatus: INotifyPropertyChanged
{
    private eStatusColor StatusColor {get; set;}
    public bool          IsFlashing  {get; set;}

      // ...
}

eStatusColor is an enum (INACTIVE, NORMAL, WARNING, CRITICAL), in which I use the Converter Level2Color_Converter() to change it to a SolidColorBrush (grey/green/yellow/red), and apply it to the shape Fill. The bool IsFlashing is used to trigger an animation on the Fill color (will go from current color to StatusBlinked, which is black, then back to current color, over a span of about 1 second). Here is an example Button shape (Router Oval):
C#
<!-- Router Oval -->
<Button Grid.Column="1"             Margin="0,3"
        VerticalAlignment="Center"  HorizontalAlignment="Center"
        Focusable="False"           ToolTip="Open Router Window"
        Command="{Binding OpenRouterWindow_Command}" >
    <Button.Template>
          <!-- This part turns off button borders/on mouseover animations -->
        <ControlTemplate TargetType="Button">
            <ContentPresenter Content="{TemplateBinding Content}"/>
        </ControlTemplate>
    </Button.Template>
    <Grid>
        <Ellipse HorizontalAlignment="Center"  VerticalAlignment="Center"
                 Height="60"                   Width="155"
                 Stroke="Black"                StrokeThickness="2"
                 Fill="{Binding RouterStatus.StatusColor,
                       Converter={local:Level2Color_Converter}}">
            <Ellipse.Style>
                <Style TargetType="{x:Type Ellipse}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding RouterStatus.IsFlashing}" Value="True">
                            <DataTrigger.EnterActions>
                                <BeginStoryboard Name="Router_storyboard">
                                    <BeginStoryboard.Storyboard>
                                        <Storyboard>
                                            <ColorAnimation To="{StaticResource StatusBlinked}"
                                                            Storyboard.TargetProperty="(Path.Fill).(SolidColorBrush.Color)"
                                                            AutoReverse="True"
                                                            RepeatBehavior="Forever"
                                                            Duration="0:0:0.5"/>
                                        </Storyboard>
                                    </BeginStoryboard.Storyboard>
                                </BeginStoryboard>
                            </DataTrigger.EnterActions>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding RouterStatus.IsFlashing}" Value="False">
                            <DataTrigger.EnterActions>
                                <StopStoryboard BeginStoryboardName="Router_storyboard"/>
                            </DataTrigger.EnterActions>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Ellipse.Style>
        </Ellipse>
        <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center"
                   FontFamily="Arial"         FontSize="13"
                   Foreground="White"         Text="ROUTER">
            <TextBlock.Effect>
                <DropShadowEffect/>
            </TextBlock.Effect>
        </TextBlock>
    </Grid>
</Button>

The problem is, if the object is already blinking and you change the color (say, from yellow to red), it turns off the storyboard animation (even though the bool IsFlashing is still true).

Note, there is not fixed order in the receipt of children statuses (we get the in the order they are sent from the production app). Also note, this is a multi-threaded app (reader thread enqueues message to message handler thread, which updates the ViewModel, and GUI updates via INotifyPropertyChanged).

Is there a way to fix this such that the Storyboard animation continues if IsFlashing is true, regardless Is there a way to fix this such that the Storyboard animation continues if IsFlashing is true, regardless of Fill color change? (or not get out of synch on color change?)

What I have tried:

In manual testing (I had a test window with buttons to force color/flashing status changes), I solved this in the following way:
<pre lang="c#">            if (mockup_IsBlinking)
            {
                  // Toggle is blinking to keep it blinking
                mockup_IsBlinking = false;
                mockup_IsBlinking = true;
            }

That is, if it was flashing, turn flashing off then on again to restart the animation.

For real testing (getting status updates from the application I am monitoring), I modified the ColorStatus to simulate the same method by modifying the setter:
C#
public class ColorStatus: INotifyPropertyChanged
{
    private eStatusColor _StatusColor;
    public  eStatusColor  StatusColor {   get
                                          {
                                              return _StatusColor;
                                          }
                                          set
                                          {
                                              _StatusColor = value;
                                              if (IsFlashing)
                                              {
                                                  IsFlashing = false;
                                                  IsFlashing = true;
                                              }
                                          }
                                      }
    public bool         IsFlashing    {get; set;}

      // ...
}

BUT, when I connect to the actual application I am monitoring, I believe the blast of children status updates come in so fast that the animation gets out of sync (we get 135 children adds as NORMAL, which causes top-level Button shape from INACTIVE to NORMAL with IsFlashing=false, then actual child status updates come in, with 3 NORMAL to WARNING with IsFlashing=true and 27 NORMAL to CRITICAL with IsFlashing=true). When I start the monitor, ~ 1/5 of the time the top level flashing is correct (blinking), 4/5 of the time it is incorrect (IsFlashing is true, but storyboard animation is not working).
Posted
Updated 5-Aug-20 12:32pm
v2
Comments
[no name] 29-Jul-20 16:16pm    
I don't see any "OnPropertyChanged" code related to IsFlashing; any binding will only see it at startup.
Member 12606650 31-Jul-20 15:40pm    
I had abbreviated those items in the class ColorStatus with "// ..." (as well as ctor method), but it does have the standard INotifyPropertyChanged items (else it wouldn't compile). Here is the missing INotifyPropertyChangedlines of code:

public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
protected PropertyChangedEventHandler PropertyChangedField;
public void OnPropertyChanged(string name)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
[no name] 31-Jul-20 18:15pm    
You have to call the handler when you want to recognize a change; it doesn't fire in a vacuum. You've only implemented the minimum, which does nothing ... except compile. "Interfaces" say nothing about execution; only "hooks".

You need to read up.
Member 12606650 5-Aug-20 14:35pm    
In the past I've gotten chided about too much code in my posts, so I may have abbreviated too much in this post, but the code IS in place.
I have a test window that has a button to rotate the color, and a button to toggle the IsFlashing bool. This works 100% of the time. But when I connect to the system I am monitoring, it blasts the initial state of the system, and we receive ~165 updates in ~ 1 second, and this only works ~1/5 of the time. By work, I mean the end result of my top-level item should be blinking, but it winds up not blinking (even though IsFlashing is true). Manual test works because my clicking is no-where near fast enough to get the UI out of synch.

1 solution

I solved this problem by changing the Storyboard from a ColorAnimation on Storyboard.TargetProperty (Path.Fill) to a DoubleAnimationUsingKeyFrames on Storyboard.TargetProperty (Path.Opacity), and then placing a solid-black shape (Ellipse in the above example) underneath the animating shape. Now, since 2 different elements are being bound (Fill on StatusColor enum, and Opacity on IsFlashing), the UI doesn't get out of synch.

So new black shape is at Panel.ZIndex="0", the shape with color and storyboard is at Panel.ZIndex="1" (and text at Panel.ZIndex="2").

I also changed the storyboard a bit (from to give more time to the color before turning black. Here is the xaml for the object

C#
<!-- Router Oval -->
<Button Grid.Column="1"             Margin="0,3"
        VerticalAlignment="Center"  HorizontalAlignment="Center"
        Focusable="False"           ToolTip="Open Router Window"
        Command="{Binding OpenRouterWindow_Command}" >
    <Button.Template>
          <!-- This part turns off button borders/on mouseover animations -->
        <ControlTemplate TargetType="Button">
            <ContentPresenter Content="{TemplateBinding Content}"/>
        </ControlTemplate>
    </Button.Template>
    <Grid>
        <Ellipse Panel.ZIndex="0"
                 HorizontalAlignment="Center"  VerticalAlignment="Center"
                 Height="60"                   Width="155"
                 Stroke="Black"                StrokeThickness="2"
                 Fill="{StaticResource StatusBlinkedBrush}"/>
        <Ellipse Panel.ZIndex="1"
                 HorizontalAlignment="Center"  VerticalAlignment="Center"
                 Height="60"                   Width="155"
                 Stroke="Black"                StrokeThickness="2"
                 Fill="{Binding RouterStatus.StatusColor,
                       Converter={local:Level2Color_Converter}}">
            <Ellipse.Style>
                <Style TargetType="{x:Type Ellipse}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding RouterStatus.IsFlashing}" Value="True">
                            <DataTrigger.EnterActions>
                                <BeginStoryboard Name="Router_storyboard">
                                    <BeginStoryboard.Storyboard>
                                        <Storyboard>
                                            <DoubleAnimationUsingKeyFrames 
                                                    Storyboard.TargetProperty="(Path.Opacity)" 
                                                    AutoReverse="True" RepeatBehavior="Forever"
                                                    Duration="0:0:0.7">
                                                <LinearDoubleKeyFrame Value="1"    KeyTime="0:0:0"   />
                                                <LinearDoubleKeyFrame Value="1"    KeyTime="0:0:0.2" />
                                                <LinearDoubleKeyFrame Value="0.75" KeyTime="0:0:0.35"/>
                                                <LinearDoubleKeyFrame Value="0.5"  KeyTime="0:0:0.6" />
                                                <LinearDoubleKeyFrame Value="0"    KeyTime="0:0:0.7" />
                                            </DoubleAnimationUsingKeyFrames>
                                        </Storyboard>
                                    </BeginStoryboard.Storyboard>
                                </BeginStoryboard>
                            </DataTrigger.EnterActions>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding RouterStatus.IsFlashing}" Value="False">
                            <DataTrigger.EnterActions>
                                <StopStoryboard BeginStoryboardName="Router_storyboard"/>
                            </DataTrigger.EnterActions>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Ellipse.Style>
        </Ellipse>
        <TextBlock Panel.ZIndex="2" 
                   VerticalAlignment="Center" HorizontalAlignment="Center"
                   FontFamily="Arial"         FontSize="13"
                   Foreground="White"         Text="ROUTER">
            <TextBlock.Effect>
                <DropShadowEffect/>
            </TextBlock.Effect>
        </TextBlock>
    </Grid>
</Button>
 
Share this answer
 

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900