2

In my WPF application I have an About Box with the application info and version. When this window is loaded it takes a little bit of time, especially if its the first time it's being opened. I'm trying to implement a loading animation while the window is opening so that the application continues to seem responsive.

I've tried using the C# BackgroundWorker to implement this, but it won't work because the process I'm trying to add a loading animation for (the about box opening) is a can only be run on the UI thread. I've tried creating a new thread and placing it in a STA apartment but it did not work.

This is the method in which I launch the about box and control the starting/stopping of the loading animation:

        private void AboutMenuItem_OnClick(object sender, RoutedEventArgs e)
    {
        LoadingCircle.Start();
        LoadingCircle.Visibility = Visibility.Visible;                    

        var aboutBox = new AboutBox { Owner = this };
        aboutBox.Show();

        LoadingCircle.Stop();
        LoadingCircle.Visibility = Visibility.Hidden;
    }

The loading circle does not appear and start moving until aboutBox.Show() is called, I can't understand why this is. If I run my application with the code above the loading circle will appear briefly before the window is loaded but it does not spin.

EDIT:

It seems that what creates the short delay is just the creation of the window, the code for creating the AboutBox is simple:

public partial class AboutBox : Window
{
    public AboutBox()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Close();
    }
}

public class Version
{
    public string UiVersion { get; set; }
    public string ServiceVersion { get; set; }

    public static Version GetVersion()
    {
        var ver = new Version();

        Assembly assembly = Assembly.GetExecutingAssembly();
        FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(assembly.Location);
        ver.UiVersion = fvi.FileVersion;

        ver.ServiceVersion = "<Service Version>";

        return ver;
    }
}

Here's the XAML:

<Window
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         x:Class="NDO.PC.DataViewer.AboutBox" 
         mc:Ignorable="d" 
         Height="384" Width="600" Title="About NanoDrop One"  WindowStartupLocation="CenterOwner" AllowsTransparency="true" WindowStyle="None" Background="White">

<Window.Resources>
    <Style x:Key="ButtonFocusVisual">
        <Setter Property="Control.Template">
            <Setter.Value>
                <ControlTemplate>
                    <Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <LinearGradientBrush x:Key="ButtonNormalBackground" EndPoint="0,1" StartPoint="0,0">
        <GradientStop Color="#F3F3F3" Offset="0"/>
        <GradientStop Color="#EBEBEB" Offset="0.5"/>
        <GradientStop Color="#DDDDDD" Offset="0.5"/>
        <GradientStop Color="#CDCDCD" Offset="1"/>
    </LinearGradientBrush>
    <SolidColorBrush x:Key="ButtonNormalBorder" Color="#FF707070"/>
    <Style x:Key="OKButton" TargetType="{x:Type Button}">
        <Setter Property="FocusVisualStyle" Value="{StaticResource ButtonFocusVisual}"/>
        <Setter Property="Background" Value="{StaticResource ButtonNormalBackground}"/>
        <Setter Property="BorderBrush" Value="{StaticResource ButtonNormalBorder}"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
        <Setter Property="HorizontalContentAlignment" Value="Center"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="Padding" Value="1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate x:Name="OKButton" TargetType="{x:Type Button}">
                    <Border x:Name="Chrome" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}"   SnapsToDevicePixels="true" CornerRadius="5" BorderThickness="1">
                        <ContentPresenter Name="TextName" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsKeyboardFocused" Value="true">
                            <Setter Property="BorderBrush" TargetName="Chrome" Value="White"/>
                        </Trigger>
                        <Trigger Property="IsEnabled" Value="false">
                            <Setter Property="Foreground" Value="#ADADAD"/>
                        </Trigger>
                        <Trigger Property="IsMouseOver" Value="true">
                            <Setter Property="Background" Value="White" TargetName="Chrome"/>
                            <Setter Property="TextBlock.Foreground" Value="#FF0086FF" TargetName="TextName"/>
                            <Setter Property="BorderBrush" Value="#FF0086FF" TargetName="Chrome"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Window.Resources>

<Grid x:Uid="ImageGrid" x:Name="ImageGrid" Grid.Row="0"  VerticalAlignment="Top" HorizontalAlignment="Left">
    <Grid.RowDefinitions>
        <RowDefinition Height="79"/>
        <RowDefinition Height="13"/>
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Image  x:Uid="AboutImage" x:Name="AboutImage" Source="Resources/AboutPageImage.jpg" Width="600" Height="79" Stretch="UniformToFill" />
    <Border Grid.Row="1" x:Uid="Border_1" Margin="0,0,0,0" Height="13" MinWidth="600" VerticalAlignment="Bottom">
        <Border.Background>
            <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                <LinearGradientBrush.RelativeTransform>
                    <TransformGroup>
                        <ScaleTransform CenterY="0.5" CenterX="0.5"/>
                        <SkewTransform CenterY="0.5" CenterX="0.5"/>
                        <RotateTransform Angle="90" CenterY="0.5" CenterX="0.5"/>
                        <TranslateTransform/>
                    </TransformGroup>
                </LinearGradientBrush.RelativeTransform>
                <GradientStop Color="#FFE5EAEE" Offset="1"/>
                <GradientStop Color="#FF0086FF" Offset="0.36"/>
            </LinearGradientBrush>
        </Border.Background>
    </Border>

    <Grid Margin="36,18,36,36" Grid.Row="2" Height="238">
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition Width="128px"/>
        </Grid.ColumnDefinitions>
        <StackPanel>
            <TextBlock FontFamily="Segoe UI Semibold" FontSize="20" FontWeight="Bold" Foreground="#FF0086FF" Margin="0,0,0,12" VerticalAlignment="Top" HorizontalAlignment="Left"><Run Text="About Application"/></TextBlock>
            <StackPanel Orientation="Horizontal">
                <TextBlock Foreground="#FF0086FF" 
                           Text="Software UI version: " />
                <TextBlock Foreground="#FF0086FF" 
                       Margin="5,0,0,0" 
                       Text="{Binding UiVersion}" />
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Foreground="#FF0086FF" 
                           Text="Software Service version: " />
                <TextBlock Foreground="#FF0086FF" 
                       Margin="5,0,0,0" 
                       Text="{Binding ServiceVersion}" />
            </StackPanel>
         </StackPanel>
        <Image Grid.Column="1" Source="Resources/TS_logo_rgb 200x61with spacing.png" Width="128" VerticalAlignment="Bottom" Margin="0,0,-38,-18"/>
        <Button Height="25" Width="75" Background="#FF0086FF" BorderBrush="{x:Null}" Foreground="White" VerticalAlignment="Bottom" HorizontalAlignment="Left" Click="Button_Click" Style="{DynamicResource OKButton}" Content="OK"/>
    </Grid>
</Grid>

Community
  • 1
  • 1
Vito
  • 1,580
  • 1
  • 17
  • 32
  • Because whatever is loading in the background is blocking the UI thread. Its difficult to help without seeing what parts of your code are taking so much time to load... – Ron Beyer Jul 17 '15 at 17:10
  • @RonBeyer Isn't that mostly irrelevant? We just need to offload the expensive work off the UI thread. – BradleyDotNET Jul 17 '15 at 17:11
  • @BradleyDotNET Right, but if it is loading something into the UI, its difficult to offload while you work with it, for example maybe he's creating thousands of user controls, or some complex awesome animation, etc... – Ron Beyer Jul 17 '15 at 17:12
  • @RonBeyer, I added the code for AboutBox. – Vito Jul 17 '15 at 17:58
  • Don't forget your about box XAML, that is very important. Also, quick question, how long does it take for your window to load? – Thraka Jul 17 '15 at 18:02
  • @Thraka it usually takes under a second for the window to load. – Vito Jul 17 '15 at 18:22
  • @Vito nothing seems out of the ordinary. As I mentioned in my answer I would just load your about window into a class variable and not show it. Then just show it when you click the button. Otherwise, don't worry about it. When someone clicks about, they expect a window to popup, they aren't concerned with the UI of the previous window at that point. – Thraka Jul 17 '15 at 18:27

2 Answers2

4

The issue that you are trying to do everything on the UI thread. You need to background or otherwise not block on the long running logic.

I would recommend using await/async. Something like this would work:

    LoadingCircle.Start();
    LoadingCircle.Visibility = Visibility.Visible;                    

    var aboutBox = new AboutBox { Owner = this };
    await Task.Run(() => 
    {
        aboutBox.Init();
    });
    aboutBox.Show();

    LoadingCircle.Stop();
    LoadingCircle.Visibility = Visibility.Hidden;

That puts the long logic into an asynchronous Task and stops execution of just the current method until it returns. It does not block the calling thread. Note that you need to mark your event handler async for this.

I created a fake "Init" method on the About box so that your constructor can do effectively nothing (but do it on the UI thread).

Also, these problems kind of disappear when doing WPF the "right" way with MVVM, so consider using that pattern in the future. It forces you to separate your view and business logic so threading the latter becomes trivial.

BradleyDotNET
  • 60,462
  • 10
  • 96
  • 117
  • This doesn't work. You're creating a new thread to load UI, this is what the UI thread is for. .NET complains that this isn't the STA thread. – Thraka Jul 17 '15 at 17:39
  • @Thraka maybe right, I get the following exception when the AboutBox is created when using this approach: The calling thread must be STA, because many UI components require this. – Vito Jul 17 '15 at 17:53
  • @Vito If thats the case, you need to seperate the construction of your box from the long-running logic. – BradleyDotNET Jul 17 '15 at 18:05
  • @Thraka, updated to involve an initialization method that wouldn't require a UI thread. – BradleyDotNET Jul 17 '15 at 18:07
  • @BradleyDotNET Good idea to circumvent that lack of MVVM patterns. Though it looks like his about box isn't doing anything. I asked him to post his XAML. Maybe it's just a complex visual tree, though I doubt that if he doesn't even have any code behind. – Thraka Jul 17 '15 at 18:09
  • He has a version class that doesn't show what loads it. I suspect he isn't showing *something*. – BradleyDotNET Jul 17 '15 at 18:10
3

As was mentioned previously, you should analyze why the window takes so long to load. There are some other options you can do:

  1. Move code around so that the window displays fast and the other code then takes a hit. Use the Busy Indicator on the actual window to flag that it is loading

  2. Don't load the about window on click, load it prior, like at startup, and just show it when the button is clicked.

  3. Use the Window_Loaded event on the about window to do work, put that work in other threads. Don't use the constructor of the window to do stuff.

Busy Indicator

You should checkout the WPF Toolkit (also available through nuget) it has a control called a Busy Indicator.

The busy indicator is a control that just shows a progress bar or other content and darkens out the background. You just set a property IsBusy to true and it turns on. So if your about window is doing a lot of work, you can show this on the about window until that work is complete.

Here is a tutorial.

Ray
  • 7,940
  • 7
  • 58
  • 90
Thraka
  • 2,065
  • 19
  • 24