The high performance ProgressBar for Windows Phone (“PerformanceProgressBar”)

2 August 2010

2/17/2011 Update

2/17/2011 update - this post is no longer needed: a performance progress bar is now included in the Silverlight for Windows Phone Toolkit.

Original post:

The ProgressBar template for Silverlight that is built into the Windows Phone today has a negative performance cost in ‘indeterminate’ mode (the animating dots that often indicate loading during an operation of unknown time). The control is also known as ‘progress indicator’ according to the UX guidelines for the phone.

Please use this template and workaround source code (open and free source) in all Windows Phone applications.

One of the reasons that this is so important is that performance is always an issue when there is a progress bar visible (whether you’re parsing data, processing layout changes, or performing network requests), so any issues with bogging down the user interface thread will be more obvious.

Here I offer a workaround which is identical in looks to the standard one, but with excellent performance that offloads from the UI thread. It uses the compositor thread exclusively for animation, instead of the UI (user interface) thread. I’ve touched on the differences in a post on performance and frame rate counters for the Windows Phone.

Updated 9/15/10: Improvements per feedback. Thanks folks!
Updated 8/17/10: Please also toggle IsIndeterminate, do not hard-code it to True.
More info in my 8/17 post.

IndeterminateWindowsPhoneProgressBar

Moving the indeterminate animation to the render thread frees up the UI thread to handle application events, messages, interact with the networking stack, and otherwise keep the app running smoothly.

Using the PerformanceProgressBar in your app

This workaround requires you to add a simple control (a .cs file) to your Windows Phone project, as well as set the ProgressBar style to the alternative, and should take about 5 minutes. This is what you want if you want to give your users that “progress indicator” visual style while data or information is loading, and using this version will let the app continue to look responsive to your customers.

Get the code

Download and add the source file RelativeAnimatingContentControl.cs to your project (it is open source)

Add the PerformanceProgressBar style

Open the App.xaml file in your Windows Phone project (or generic.xaml if a composite control library). Next, add the following XMLNS declaration at the top element – this is important as it tells the parser to look in your project for the code you added above.

Note that you may see warning or error messages in Visual Studio until after you’ve built the project, but it should work fine.

xmlns:unsupported="clr-namespace:Microsoft.Phone.Controls.Unsupported"

Now, go into the Resources section of the file and add this style (you can also download as PerformanceProgressBarStyle.txt) (the <Application.Resources> element).

<Style x:Key="PerformanceProgressBar" TargetType="ProgressBar">

    <Setter Property="Foreground" Value="{StaticResource PhoneAccentBrush}"/>

    <Setter Property="Background" Value="{StaticResource PhoneAccentBrush}"/>

    <Setter Property="Maximum" Value="100"/>

    <Setter Property="IsHitTestVisible" Value="False"/>

    <Setter Property="Padding" Value="{StaticResource PhoneHorizontalMargin}"/>

    <Setter Property="Template">

        <Setter.Value>

            <ControlTemplate TargetType="ProgressBar">

                <unsupported:RelativeAnimatingContentControl HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">

                    <unsupported:RelativeAnimatingContentControl.Resources>

                        <ExponentialEase EasingMode="EaseOut" Exponent="1" x:Key="ProgressBarEaseOut"/>

                        <ExponentialEase EasingMode="EaseIn" Exponent="1" x:Key="ProgressBarEaseIn"/>

                    </unsupported:RelativeAnimatingContentControl.Resources>

                    <VisualStateManager.VisualStateGroups>

                        <VisualStateGroup x:Name="CommonStates">

                            <VisualState x:Name="Determinate"/>

                            <VisualState x:Name="Indeterminate">

                                <Storyboard RepeatBehavior="Forever" Duration="00:00:04.4">

                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="IndeterminateRoot">

                                        <DiscreteObjectKeyFrame KeyTime="0">

                                            <DiscreteObjectKeyFrame.Value>

                                                <Visibility>Visible</Visibility>

                                            </DiscreteObjectKeyFrame.Value>

                                        </DiscreteObjectKeyFrame>

                                    </ObjectAnimationUsingKeyFrames>

                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DeterminateRoot">

                                        <DiscreteObjectKeyFrame KeyTime="0">

                                            <DiscreteObjectKeyFrame.Value>

                                                <Visibility>Collapsed</Visibility>

                                            </DiscreteObjectKeyFrame.Value>

                                        </DiscreteObjectKeyFrame>

                                    </ObjectAnimationUsingKeyFrames>

                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.0" Storyboard.TargetProperty="X" Storyboard.TargetName="R1TT">

                                        <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/>

                                        <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/>

                                        <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/>

                                        <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/>

                                    </DoubleAnimationUsingKeyFrames>

                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.2" Storyboard.TargetProperty="X" Storyboard.TargetName="R2TT">

                                        <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/>

                                        <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/>

                                        <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/>

                                        <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/>

                                    </DoubleAnimationUsingKeyFrames>

                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.4" Storyboard.TargetProperty="X" Storyboard.TargetName="R3TT">

                                        <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/>

                                        <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/>

                                        <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/>

                                        <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/>

                                    </DoubleAnimationUsingKeyFrames>

                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.6" Storyboard.TargetProperty="X" Storyboard.TargetName="R4TT">

                                        <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/>

                                        <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/>

                                        <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/>

                                        <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/>

                                    </DoubleAnimationUsingKeyFrames>

                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.8" Storyboard.TargetProperty="X" Storyboard.TargetName="R5TT">

                                        <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/>

                                        <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/>

                                        <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/>

                                        <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/>

                                    </DoubleAnimationUsingKeyFrames>

                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="R1">

                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/>

                                        <DiscreteDoubleKeyFrame KeyTime="00:00:02.5" Value="0"/>

                                    </DoubleAnimationUsingKeyFrames>

                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.2" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="R2">

                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/>

                                        <DiscreteDoubleKeyFrame KeyTime="00:00:02.5" Value="0"/>

                                    </DoubleAnimationUsingKeyFrames>

                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.4" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="R3">

                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/>

                                        <DiscreteDoubleKeyFrame KeyTime="00:00:02.5" Value="0"/>

                                    </DoubleAnimationUsingKeyFrames>

                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.6" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="R4">

                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/>

                                        <DiscreteDoubleKeyFrame KeyTime="00:00:02.5" Value="0"/>

                                    </DoubleAnimationUsingKeyFrames>

                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00.8" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="R5">

                                        <DiscreteDoubleKeyFrame KeyTime="0" Value="1"/>

                                        <DiscreteDoubleKeyFrame KeyTime="00:00:02.5" Value="0"/>

                                    </DoubleAnimationUsingKeyFrames>

                                </Storyboard>

                            </VisualState>

                        </VisualStateGroup>

                    </VisualStateManager.VisualStateGroups>

                    <Grid>

                        <Grid x:Name="DeterminateRoot" Margin="{TemplateBinding Padding}" Visibility="Visible">

                            <Rectangle x:Name="ProgressBarTrack" Fill="{TemplateBinding Background}" Height="4" Opacity="0.1"/>

                            <Rectangle x:Name="ProgressBarIndicator" Fill="{TemplateBinding Foreground}" HorizontalAlignment="Left" Height="4"/>

                        </Grid>

                        <Border x:Name="IndeterminateRoot" Margin="{TemplateBinding Padding}" Visibility="Collapsed">

                            <Grid HorizontalAlignment="Left">

                                <Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R1" Opacity="0" CacheMode="BitmapCache">

                                    <Rectangle.RenderTransform>

                                        <TranslateTransform x:Name="R1TT"/>

                                    </Rectangle.RenderTransform>

                                </Rectangle>

                                <Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R2" Opacity="0" CacheMode="BitmapCache">

                                    <Rectangle.RenderTransform>

                                        <TranslateTransform x:Name="R2TT"/>

                                    </Rectangle.RenderTransform>

                                </Rectangle>

                                <Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R3" Opacity="0" CacheMode="BitmapCache">

                                    <Rectangle.RenderTransform>

                                        <TranslateTransform x:Name="R3TT"/>

                                    </Rectangle.RenderTransform>

                                </Rectangle>

                                <Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R4" Opacity="0" CacheMode="BitmapCache">

                                    <Rectangle.RenderTransform>

                                        <TranslateTransform x:Name="R4TT"/>

                                    </Rectangle.RenderTransform>

                                </Rectangle>

                                <Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R5" Opacity="0" CacheMode="BitmapCache">

                                    <Rectangle.RenderTransform>

                                        <TranslateTransform x:Name="R5TT"/>

                                    </Rectangle.RenderTransform>

                                </Rectangle>

                            </Grid>

                        </Border>

                    </Grid>

                </unsupported:RelativeAnimatingContentControl>

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

Add this alternate ProgressBar control style everywhere

Now, whenever you or your designers place a ProgressBar in your app, set the style to be PerformanceProgressBar and it will be picked up from the App.xaml.

In XAML:

<ProgressBar IsIndeterminate="True" Style="{StaticResource PerformanceProgressBar}"/>

In code:

Style style = (Style)App.Current.Resources["PerformanceProgressBar"];

if (style == null) { throw new InvalidOperationException("The style was not found."); }

ProgressBar bar = new ProgressBar

{

    IsIndeterminate = true,

    Style = style,

};

LayoutRoot.Children.Add(bar);

That’s it! Read on if you want to learn about how this all actually works, why it isn’t built into the system (there’s a good reason), and just general info about the performance-improving workflow I went through.

The back story

When people started seeing poor performance on applications that were using indeterminate progress bars, it was a stumper. I’m pretty sure there were hallway conversations including: “what the heck? how’s this so bad? there’s nothing happening here – it’s just animating rectangles! WTF? is there a bug in the animation system? what’s really the issue? this is a super simple animation! fix it!”

I jumped in and started looking at the root of the potential performance problem. It was definitely interesting because the emulator experience was pretty good (though still heavy in the UI thread), but once these same apps were on real developer devices, the issue manifested. The emulator is just that – it emulates, so there will be differences.

Working to unpeel the onion and diagnose the indeterminate ProgressBar as the root of the performance issue, some of what was tried:

  • Noting applications were sluggish on hardware devices when a progress bar would appear in indeterminate mode
  • Removing networking calls, commenting out any data parsing, and cutting out application logic – leaving just simple states and the progress bar
  • Building a repro application with an indeterminate ProgressBar, the UI thread was still stressed according to the frame rate counters
  • Adding several progress bars compounded the problem
  • Trying to understand why the render thread frame rate was doing fine (above 50 fps), but the UI was below 10 fps at times
  • Reviewing the default control style and template’s visual state animations and code to look for complex animations or opportunities for improvement (BitmapCache was not being used for instance, but it wasn’t a big enough difference to explain the cause)
  • Identifying that the method used to move the small rectangles on the screen by animating the Value property of Slider would cause per-thread callbacks to the property change handler of the Slider control, forcing the animations to the UI thread instead of the independent animations render thread

So there, the issue was in the animations and underlying template for the indeterminate ProgressBar and its use of Slider controls.

Why Slider is in the template

The default style and control template for the Windows Phone was created to meet the UX & user interface guidelines that the Windows Phone team has created. A consistent and good-looking set of controls is an important part of the platform and so the progress bar has a very slick appearance.

In “indeterminate” mode, five small rectangles in the user’s accent color, that move into the screen one just briefly after another. It’s mesmerizing and more fun than the standard Silverlight one you may have seen on the web before.

Trouble is, the animation defined for the rectangles is impossible to declare in XAML markup – there’s no way to provide an animation frame description which says “at this keyframe, the rectangle should be positioned 1/3 of the width of the control”: Silverlight happily will let you define a specific fixed position (like translating 75 pixels), but not a variable or ratio-based size for the animation.

But wait! There’s another control in the core platform that can do this visual concept.

Enter Slider. Slider is a nice control because it calculates a position based on the width and effectively a percent (the Value divided by the Maximum provides the ratio), then uses that to update the Thumb/Track template part of the control.

Here’s some short XAML that helps demonstrate how this ratio works for offsetting the thumb:

<Grid Width="100" Background="#33000000" Margin="5">

    <Slider Value="30" Maximum="100"/>

</Grid>

<Grid Grid.Column="1" Width="200" Background="#33000000" Margin="5">

    <Slider Value="30" Maximum="100"/>

</Grid>

And you’ll see that even though the Value is 30 in each case, the Sliders are different sizes based on their parents’ size: effectively allowing for moving the thumb based on a ratio applied to the actual width:

IdenticalSliders

This let the designers create the ratio-based animation for the rectangles (thumbs of a slider) that makes them smoothly move in thirds. The rectangles enter to one third the width of the screen while fading in, then move at a different rate from 1/3 to 2/3 the width of the screen, and then fly and fade out.

So indeterminate mode progress bar was destined to use a set of Sliders to move those rectangles. The progress bar defines 5 Slider controls.

Here’s a snippet from the default style template in the beta release of the Windows Phone developer tools:

  <Border x:Name="IndeterminateRoot" Visibility="Collapsed" Margin="{TemplateBinding Padding}">

    <Grid>

      <Slider x:Name="Slider1" Style="{StaticResource PhoneProgressBarSliderStyle}" Foreground="{TemplateBinding Foreground}"/>

      <Slider x:Name="Slider2" Style="{StaticResource PhoneProgressBarSliderStyle}" Foreground="{TemplateBinding Foreground}"/>

      <Slider x:Name="Slider3" Style="{StaticResource PhoneProgressBarSliderStyle}" Foreground="{TemplateBinding Foreground}"/>

      <Slider x:Name="Slider4" Style="{StaticResource PhoneProgressBarSliderStyle}" Foreground="{TemplateBinding Foreground}"/>

      <Slider x:Name="Slider5" Style="{StaticResource PhoneProgressBarSliderStyle}" Foreground="{TemplateBinding Foreground}"/>

    </Grid>

  </Border>

The static style resource defines a custom look for the Slider to create an accent colored rectangle, instead of looking like the normal phone Slider control you may place on the page.

Nesting controls within templates is how Silverlight was designed to work, so it isn’t the composition of controls that necessarily degrades performance in this case (though simplicity is always key, especially on a device that has very different computing properties than your PC).

Now here’s a small chunk of the Storyboard animation code that animates the Slider instances. Note the Value target property:

 <DoubleAnimationUsingKeyFrames 

    Storyboard.TargetName="Slider3" 

    Storyboard.TargetProperty="Value" 

    BeginTime="00:00:00.4">

<!-- and so on -->

 </DoubleAnimationUsingKeyFrames>

 

And now we are finally to the root of the problem: the trouble is that Value is a DependencyProperty defined by the nested Slider controls, and it has some performance implications.

Animating the Slider::Value property means the animation happens on the UI thread (not good!)

The Value DependencyProperty of Slider comes from its parent class, RangeBase. The declaration for the property shows that there is a property change callback, OnValueChanged, that is called whenever the property changes, so in our case, when the progress bar animations animate the Slider.Value target property – which will be every single frame, it’s an animation.

The platform allows dependency properties to optionally have these callbacks to perform logic and states changes, validation, and other operations. In real world control development, I’d guess that it’s 50/50 whether a property has that callback. So it’s not a design flaw of a property, they are there for a good reason, but can have unintended consequences to performance, especially once you start doing wild things with those properties.

If you open up Reflector to look at Slider, you’ll see a call to UpdateTrackLayout. This is the meat of the control that updates the size of the template’s special Grid columns or rows to simulate the movement of the Slider.

ReflectorSlider

The callback will always happen on the UI thread, as all core UI interactions on the platform need to happen on the UI thread, and the runtime guarantees this.

That means that the five animations for the value property cannot happen on the separate render thread.

What needs to change in ProgressBar? Should we build our own?

So maybe the default style on the phone is a little “heavy”; but the ProgressBar control implementation is fine. There are no code fixes or changes required to the actual control.

I recommend just using a special C# file and re-templating the standard ProgressBar, though it would be easy enough to package this up into an independent control library or ship with some kind of toolkit. We’ll see what people do.

Why can’t MSFT update the progress bar in the platform?

I’m really only talking here about the beta version of the developer tools for the Windows Phone.

In my opinion, changing this in the platform while matching the Windows Phone user interface guidelines is a big technical challenge (again, the lack of ratio-based translation key frames in Silverlight), so in the meantime, please use mine – I’ll make sure it continues to work through the release of the phone and will blog and tweet updates as necessary.

This could change in the future, but since this workaround doesn’t change the ProgressBar control itself, it is easy enough to pull out the workaround – just remove the custom Style setter!

Shouldn’t everything just be fast and have great performance anyway?

We all wish. There is a lot of give and take in application development and especially tweaking for perf.

On the Silverlight mobile team we’re very serious about performance, but don’t expect performance to be free: we’re doing our part to drive performance improvements through the product, but we need application developer’s help as well to optimize their applications for the phone experience.

What’s this RelativeAnimatingContentControl I added to my project?

I designed this custom content control that makes adjustments to the storyboards of the visual states attached to it. When the layout is updated (i.e. there is a known size), the control will go through the storyboards and update the properties that are designed to be “relative”, using its initial ratio.

The control has some magic number trickery to accomplish this, and only works with double animation elements and double animations with key frames. Instead of hard-coding numbers and using that, the control interprets the initial values of the animation properties as a percentage. If you’re interested in the inner workings, let me know and I am happy to post more about it. Quickly, if a From, To, or keyframe Value has an initial value ending in “.1” (such as 33.1, or 75.1, or 0.1), the property is interpreted as a percentage ratio based on the width of the control – 0 to 100. The .1 is stipped off. If the magic number decimal is instead .2, like 50.2, then the height of the control is used for the calculation instead. The idea is that a designer and coder work to define the special template with these magic numbers, but it doesn’t keep the designer from defining other animations that are not ratio-based.

Since the control must be the root of the ProgressBar template, it is an opt-in model and won’t affect any other uses of ProgressBar either.

This is all to work around the lack of ratio animation properties in Silverlight, and it’s a pretty good solution, magic numbers aside. I feel a little sick about the magic numbers part. I’m sharing this feedback with the team of course and we’ll see what the future holds.

Why’s the control in the Microsoft.Phone.Controls.Unsupported namespace?

I wanted to make it clear to everyone that this is not an official Microsoft control. It’s a way to work around a limitation in Silverlight, and probably not a good general-purpose solution.

That said, the namespace probably should be JeffWilcox.Controls.Supported: I’m always happy to look at the feedback I get on these posts and update the content to be relative and address issues.

What changed in the PerformanceProgressBar style

You’ll see that the ProgressBar style looks very similar. Here are some of the differences:

  • The root of the control template is this special RelativeAnimatingContentControl. The control updates the visual state storyboards when the size of the control changes, giving the animations a ratio to the size of the control based on the initial value stored in the storyboard.
  • The 5 Slider controls have been replaced by 5 simple Rectangle instances
  • The rectangles have BitmapCache turned on
  • Each rectangle has a defined TranslateTransform which is used to move the rectangle instead of the UI animation from the standard (which animated the Value property, on the UI thread, which then made the item move after a layout pass with the underlying grid in the template)

So here’s the part with the rectangles:

<Rectangle Fill="{TemplateBinding Foreground}" Height="4" IsHitTestVisible="False" Width="4" x:Name="R1" Opacity="0" CacheMode="BitmapCache">

    <Rectangle.RenderTransform>

        <TranslateTransform x:Name="R1TT"/>

    </Rectangle.RenderTransform>

</Rectangle>

And here’s some of the animation code. Note the “magic” numbers ending in 0.1 – those tell the system it’s a ratio based on the width of the control. A hacky overload of a property, but something that can work and give us a real perf win today!

<DoubleAnimationUsingKeyFrames BeginTime="00:00:00.0" Storyboard.TargetProperty="X" Storyboard.TargetName="R1TT">

    <LinearDoubleKeyFrame KeyTime="00:00:00.0" Value="0.1"/>

    <EasingDoubleKeyFrame KeyTime="00:00:00.5" Value="33.1" EasingFunction="{StaticResource ProgressBarEaseOut}"/>

    <LinearDoubleKeyFrame KeyTime="00:00:02.0" Value="66.1"/>

    <EasingDoubleKeyFrame KeyTime="00:00:02.5" Value="100.1" EasingFunction="{StaticResource ProgressBarEaseIn}"/>

</DoubleAnimationUsingKeyFrames>

Feedback

I hope everyone uses this ProgressBar to make their apps rock. Remember that one key to great perf is to let the UI thread “breath”: don’t bog it down, let it do its job, and especially in loading, downloading and parsing scenarios, really important to use a progress bar that won’t inhibit the progress.

Let me know how it goes!

Jeff Wilcox is a Software Engineer at Microsoft in the Open Source Programs Office (OSPO), helping Microsoft engineers use, contribute to and release open source at scale.

comments powered by Disqus