Wpf – How to stop the WPF ProgressBar pulsing/animating when it reaches 100%

mvvmprogress-barwpf

I have an MVVM-based WPF 4 application which uses a ProgressBar to show the percentage completion of a long-running operation.

<ProgressBar Name="ProgressBar"
    IsIndeterminate="False"
    Minimum="0"
    Maximum="100"
    Value="{Binding Path=ProgressPercentageComplete, Mode=OneWay}"
    Visibility="Visible"/>

I am happy for the "pulsing" animation to occur while the progress bar is moving, but once it reaches 100% I'd like it to stop animating and just remain static at 100%.

I've tried setting IsIndeterminate="False" but this doesn't help and I can see why after reading the MSDN Documentation:

When this property is true, the
ProgressBar animates a few bars moving
across the ProgressBar in a continuous
manner and ignores the Value property.

Is it possible to stop this animation? Either completely, or just at 100%.

Best Answer

I wrote a generalized solution for this using an attached property, allowing me to toggle the behavior on any ProgressBar simply through a direct property or style setter, like so:

<ProgressBar helpers:ProgressBarHelper.StopAnimationOnCompletion="True" />

The code:

public static class ProgressBarHelper {
    public static readonly DependencyProperty StopAnimationOnCompletionProperty =
        DependencyProperty.RegisterAttached("StopAnimationOnCompletion", typeof(bool), typeof(ProgressBarHelper),
                                            new PropertyMetadata(OnStopAnimationOnCompletionChanged));

    public static bool GetStopAnimationOnCompletion(ProgressBar progressBar) {
        return (bool)progressBar.GetValue(StopAnimationOnCompletionProperty);
    }

    public static void SetStopAnimationOnCompletion(ProgressBar progressBar, bool value) {
        progressBar.SetValue(StopAnimationOnCompletionProperty, value);
    }

    private static void OnStopAnimationOnCompletionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) {
        var progressBar = obj as ProgressBar;
        if (progressBar == null) return;

        var stopAnimationOnCompletion = (bool)e.NewValue;

        if (stopAnimationOnCompletion) {
            progressBar.Loaded += StopAnimationOnCompletion_Loaded;
            progressBar.ValueChanged += StopAnimationOnCompletion_ValueChanged;
        } else {
            progressBar.Loaded -= StopAnimationOnCompletion_Loaded;
            progressBar.ValueChanged -= StopAnimationOnCompletion_ValueChanged;
        }

        if (progressBar.IsLoaded) {
            ReevaluateAnimationVisibility(progressBar);
        }
    }

    private static void StopAnimationOnCompletion_Loaded(object sender, RoutedEventArgs e) {
        ReevaluateAnimationVisibility((ProgressBar)sender);
    }

    private static void StopAnimationOnCompletion_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) {
        var progressBar = (ProgressBar)sender;

        if (e.NewValue == progressBar.Maximum || e.OldValue == progressBar.Maximum) {
            ReevaluateAnimationVisibility(progressBar);
        }
    }

    private static void ReevaluateAnimationVisibility(ProgressBar progressBar) {
        if (GetStopAnimationOnCompletion(progressBar)) {
            var animationElement = GetAnimationElement(progressBar);
            if (animationElement != null) {
                if (progressBar.Value == progressBar.Maximum) {
                    animationElement.SetCurrentValue(UIElement.VisibilityProperty, Visibility.Collapsed);
                } else {
                    animationElement.InvalidateProperty(UIElement.VisibilityProperty);
                }
            }
        }
    }

    private static DependencyObject GetAnimationElement(ProgressBar progressBar) {
        var template = progressBar.Template;
        if (template == null) return null;

        return template.FindName("PART_GlowRect", progressBar) as DependencyObject;
    }
}

Basically, it adds a ValueChanged handler which adjusts the visibility of the animated element.

A few notes:

  • I'm using "PART_GlowRect" to find the animated element, although someone called this a hack. I disagree: this element name is officially documented through TemplatePartAttribute, which you can see in ProgressBar's declaration. While it's true that this doesn't necessarily guarantee that the named element exists, the only reason it should be missing is if the animation feature isn't supported at all. If it is supported but uses a different element name than the one documented, I would consider that a bug, not an implementation detail.

  • Since I'm pulling an element out of the template, it's also necessary to handle the Loaded event (which is raised when a template is applied) to wait for the template to become available before attempting to set initial visibility, and if necessary set it again when the template is replaced on the fly by a theme change.

  • Rather than explicitly toggling Visibility between Collapsed and Visible, I'm using SetCurrentValue to set to Collapsed, and InvalidateProperty to reset it. SetCurrentValue applies a value that does not take priority over other value sources, and InvalidateProperty re-evaluates the property without taking the SetCurrentValue setting into consideration. This ensures that if there are existing styles or triggers which would affect the visibility under normal conditions (i.e. when it is not at 100%), it would reset to that behavior if the progress bar is reused (going from 100% back to 0%) rather than being hard-coded to Visible.

Related Topic