вторник, 11 августа 2015 г.

Показ окна в отдельном потоке

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


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

<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">
            <Canvas RenderTransformOrigin="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>
            <Canvas RenderTransformOrigin="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>
            <Canvas RenderTransformOrigin="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);
        }
    }
}

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

Комментариев нет:

Отправить комментарий