Время от времени в приложении появляются «долгие операции» во время которых интерфейс тормозит и пользователь не понимает что происходит с приложением. Обычно такие операции выносятся в фоновый поток, а в основном потоке приложения показываем прогресс выполнения работы или некую анимацию дающую понять, что приложение не повисло. Но что делать, если работа выполняется в основном потоке и вынести ее в фоновый нельзя (например, идет чтение из визуальных компонентов)? Вот об этом и поговорим под катом.


Итак еще раз задача. В приложении есть «длительная операция», которая выполняется в потоке интерфейса и нужно пользователю показать анимацию, чтобы он не волновался что все пропало.
Для показа анимации я добавил вот такое окно в приложение:

<Window x:Class=»WpfApplication1.BusyWindow»

        xmlns=»http://schemas.microsoft.com/winfx/2006/xaml/presentation»
        xmlns:x=»http://schemas.microsoft.com/winfx/2006/xaml»
        xmlns:d=»http://schemas.microsoft.com/expression/blend/2008″
        xmlns:mc=»http://schemas.openxmlformats.org/markup-compatibility/2006″
        xmlns:local=»clr-namespace:WpfApplication1″
        mc:Ignorable=»d»
        Title=»BusyWindow» Height=»300″Width=»300″ WindowStyle=»None» Background=»Transparent» AllowsTransparency=»True»>
    <Grid>
        <Grid Background=»Transparent» HorizontalAlignment=»Center» VerticalAlignment=»Center»>
            <CanvasRenderTransformOrigin=»0.5,0.5″ HorizontalAlignment=»Center» VerticalAlignment=»Center» Width=»50″ Height=»50″ >
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»20″ Canvas.Top=»0″ Stretch=»Fill» Fill=»Green» Opacity=»1.0″/>
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»40″ Canvas.Top=»20″ Stretch=»Fill» Fill=»Green» Opacity=»0.1″/>
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»20″ Canvas.Top=»40″ Stretch=»Fill» Fill=»Green» Opacity=»0.4″/>
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»0″ Canvas.Top=»20″ Stretch=»Fill» Fill=»Green» Opacity=»0.7″/>
                <Canvas.RenderTransform>
                    <RotateTransform Angle=»0″ />
                </Canvas.RenderTransform>
            </Canvas>
            <CanvasRenderTransformOrigin=»0.5,0.5″ HorizontalAlignment=»Center» VerticalAlignment=»Center» Width=»50″ Height=»50″ >
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»20″ Canvas.Top=»0″ Stretch=»Fill» Fill=»Green» Opacity=»0.01″/>
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»40″ Canvas.Top=»20″ Stretch=»Fill» Fill=»Green» Opacity=»0.2″/>
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»20″ Canvas.Top=»40″ Stretch=»Fill» Fill=»Green» Opacity=»0.5″/>
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»0″ Canvas.Top=»20″ Stretch=»Fill» Fill=»Green» Opacity=»0.8″/>
                <Canvas.RenderTransform>
                    <RotateTransform Angle=»30″ />
                </Canvas.RenderTransform>
            </Canvas>
            <CanvasRenderTransformOrigin=»0.5,0.5″ HorizontalAlignment=»Center» VerticalAlignment=»Center» Width=»50″ Height=»50″ >
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»20″ Canvas.Top=»0″ Stretch=»Fill» Fill=»Green» Opacity=»0.05″/>
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»40″ Canvas.Top=»20″ Stretch=»Fill» Fill=»Green» Opacity=»0.3″/>
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»20″ Canvas.Top=»40″ Stretch=»Fill» Fill=»Green» Opacity=»0.6″/>
                <Ellipse Width=»10″ Height=»10″ Canvas.Left=»0″ Canvas.Top=»20″ Stretch=»Fill» Fill=»Green» Opacity=»0.9″/>
                <Canvas.RenderTransform>
                    <RotateTransform Angle=»60″ />
                </Canvas.RenderTransform>
            </Canvas>
            <Grid.RenderTransform>
                <RotateTransform x:Name=»SpinnerRotate» CenterX=»25″ CenterY=»25″ />
            </Grid.RenderTransform>
            <Grid.Triggers>
                <EventTrigger RoutedEvent=»FrameworkElement.Loaded»>
                    <BeginStoryboard>
                        <Storyboard x:Name=»Animation»>
                            <DoubleAnimationUsingKeyFrames Duration=»0:0:12″ RepeatBehavior=»Forever» SpeedRatio=»12″ Storyboard.TargetName=»SpinnerRotate» Storyboard.TargetProperty=»(RotateTransform.Angle)»>
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:00″ Value=»0″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:01″ Value=»30″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:02″ Value=»60″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:03″ Value=»90″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:04″ Value=»120″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:05″ Value=»150″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:06″ Value=»180″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:07″ Value=»210″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:08″ Value=»240″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:09″ Value=»270″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:10″ Value=»300″ />
                                <DiscreteDoubleKeyFrame KeyTime=»00:00:11″ Value=»330″ />
                            </DoubleAnimationUsingKeyFrames>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Grid.Triggers>
        </Grid>
    </Grid>
</Window>

Кода у этого окна нет, все на триггерах. Итак к пример. На главном окне я добавил кнопку, вот с таким магическим кодом:

private void button_Click(object sender, RoutedEventArgs e)

{
    Thread.Sleep(4000);
}

Это и есть эмуляция «длительной операции». Понятно, что после нажатия этой кнопки приложение «висит» четыре секунды и непонятно, что с ним происходит. Давайте покажем мотылятор. Решение в лоб:

private void button_Click(object sender, RoutedEventArgs e)

{
    ShowBusy();
    Thread.Sleep(4000);
    HideBusy();
}

BusyWindow _busyWindow = null;

private void ShowBusy()

{
    _busyWindow = new BusyWindow();
    _busyWindow.Left = this.Left + this.Width / 2;
    _busyWindow.Top = this.Top + this.Height / 2;
    _busyWindow.Show();
}

private void HideBusy()
{
    _busyWindow.Close();
}

Позволяет показать мотылятор, но т.к. основной поток остановлен, то и анимация не происходит. Иллюзия что приложению плохо сохраняется.
Попытка вынести создание и показ окна в отдельный поток ни к чему хорошему не приводит. Изменив код вот так:

private void ShowBusy()

{
    Task.Factory.StartNew(AnimationThreadStartingPoint);
}

private void AnimationThreadStartingPoint()
{
    _busyWindow = new BusyWindow();
    _busyWindow.Left = this.Left + this.Width / 2;
    _busyWindow.Top = this.Top + this.Height / 2;
    _busyWindow.Show();
}

При нажатии на кнопку мы получим вот такое печальное сообщение:

Ок, отказываемся от новомодных Task-ов и возвращаемся к привычным Thread-ам, избавляемся от межпотокового взаимодействия и добавляем блокировки в целях избегания гонок:

BusyWindow _busyWindow = null;

object _busyWindowSync = new object();

private void ShowBusy()

{
    lock(_busyWindowSync)
    {
        if (_busyWindow == null)
        {
            double left = Dispatcher.Invoke((Func<double>)(() => this.Left + this.Width / 2));
            double top = Dispatcher.Invoke((Func<double>)(() => this.Top + this.Height / 2));
            Thread newWindowThread = new Thread(new ParameterizedThreadStart(AnimationThreadStartingPoint));
            newWindowThread.SetApartmentState(ApartmentState.STA);
            newWindowThread.IsBackground = true;
            newWindowThread.Start(new Point() { X = left, Y = top });
        }
    }
}

private void AnimationThreadStartingPoint(object position)
{
    lock(_busyWindowSync)
    {
        if (_busyWindow == null)
        {
            _busyWindow = new BusyWindow();
            _busyWindow.Left = ((Point)position).X;
            _busyWindow.Top = ((Point)position).Y;
            _busyWindow.Show();
        }
    }
    System.Windows.Threading.Dispatcher.Run();
}

private void HideBusy()
{
    lock (_busyWindowSync)
    {
        if (_busyWindow != null)
        {
            _busyWindow.Dispatcher.BeginInvoke((Action)_busyWindow.Close);
        }
    }
}

Все, теперь несмотря на то, что главный поток приложения «висит», анимация показывается и пользователь спокойно ждет окончания длительной операции. Лень делать гифку, поэтому придется поверить мне на слово, что она вертится: