Подождите, пока не будет нарисован UIElement (без updatelayout)

У меня есть функция, которая занимает около 5 секунд, если нажать кнопку. Если кнопка нажата, я хочу отобразить какое-то уведомление, чтобы указать, что нажатие кнопки обрабатывается, что-то вроде

<Button Click="OnButtonClick" Content="Process Input" />

<Border x:Name="NotificationBorder" Opacity="0" IsHitTestVisible="False" 
        Width="500" Height="100" Background="White">
    <TextBlock Text="Your input is being processed" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>

И в коде позади кнопки нажмите:

private void OnButtonClick(Object sender, RoutedEventArgs e)
{
    DoubleAnimation da = new DoubleAnimation
        {
            From = 5,
            To = 0,
            Duration = TimeSpan.FromSeconds(2.5),
        };

    // Making border visible in hopes that it's drawn before animation kicks in
    NotificationBorder.Opacity = 1;
    da.Completed += (o, args) => NotificationBorder.Opacity = 0;

    NotificationBorder.UpdateLayout(); //Doesn't do anything
    UpdateLayout(); // Doesn't do anything

    NotificationBorder.BeginAnimation(OpacityProperty, da);

    // Simulate calculationheavy functioncall
    Thread.Sleep(5000);
}

как-то UpdateLayout() недостаточно быстро, уведомление отображается только после 5 секунд Thread.Sleep старше.

Dispatcher.Invoke((Action)(() => NotificationBorder.Opacity = 1), DispatcherPriority.Render); тоже не сработает.

Кроме того, я не могу позволить Thread.Sleep запускать в отдельном рабочем потоке - в реальном приложении ему нужно считывать данные из принадлежащих Dispatcher объектов и (пере) создавать части пользовательского интерфейса.

Есть ли способ сделать это видимым раньше? Thread.Sleep() называется?

3 ответа

Проблема в том, что любая длительная активность в основном потоке (потоке пользовательского интерфейса) приведет к зависанию в пользовательском интерфейсе, поэтому вы не увидите никакой анимации.

Вы должны сделать свои расчеты в отдельной теме. Если вам нужен доступ к принадлежащим Dispatcher объектам, вы можете использовать uiObject.Dispatcher.BeginInvoke или Invoke. BackgroundWorker может помочь вам выполнить некоторые длительные вычисления и подписаться на событие Completed, которое будет автоматически запущено BackgroundWorker в потоке пользовательского интерфейса.

Если вы в порядке с зависанием пользовательского интерфейса, измените некоторые свойства и переназначьте другие вещи с помощью Dispatcher.BeginInvoke(DispatcherPriority.Background,...), например

NotificationBorder.Opacity = 1; Dispatcher.BeginInvoke(DispatcherPriority.Background, (Action)(()=>Thread.Sleep(5000)));

Если вы хотите, чтобы пользовательский интерфейс обновлялся, вам необходимо изменить свою длительную операцию на асинхронную, что означает правильное отделение зависимостей пользовательского интерфейса от любой логики или, возможно, операций, связанных с вводом-выводом, которые занимают все время. Если вы этого не сделаете, ни одно из обновлений пользовательского интерфейса, происходящих в вашей длительной операции, в действительности не будет отражено в пользовательском интерфейсе до тех пор, пока это не будет сделано.

Если вы работаете с.NET 4.5, вот мой предпочтительный шаблон, как это сделать:

    public async Task DoComplexStuff()
    {
        //Grab values from UI objects that must be accessed on UI thread

        //Run some calculations in the background that don't depend on the UI
        var result = await Task.Run((Func<string>)DoComplexStuffInBackground);

        //Update UI based on results

        //Possibly do more stuff in the background, etc.
    }

    private string DoComplexStuffInBackground()
    {
        return "Stuff";
    }

Это несколько "назад" по сравнению с подходом BackgroundWorker старого стиля, но я считаю, что это приводит к более чистой логике. Вместо написания своего фонового кода с явными вызовами для обновлений пользовательского интерфейса, вы пишете свой код пользовательского интерфейса вокруг явных фоновых рабочих вызовов через Task.Run(), Это также ведет к естественному разделению UI-ориентированного кода и базового логического кода, так как вам необходимо реорганизовать свою логику в методы, которые можно вызывать таким образом. Это хорошая идея для долгосрочной ремонтопригодности в любом случае.

Таким образом, вы можете отправить оба действия (пользовательский интерфейс и ваши вычисления) в диспетчер, и они будут выполнены в строке

Dispatcher.BeginInvoke(do ui things);
Dispatcher.BeginInvoke(do your logic things);

Но вам лучше перенести ваш расчет в фоновый поток.

Другие вопросы по тегам