Отображение экрана ожидания в WPF

Я пытаюсь отобразить диалоговое окно "Пожалуйста, подождите" для длительной работы. Проблема в том, что это однопоточный, хотя я и заставляю WaitScreen отображать его никогда. Есть ли способ, которым я могу изменить видимость этого экрана и заставить его отображаться немедленно? Я включил вызов курсора в качестве примера. Сразу после вызова этого. Курсор, курсор обновляется немедленно. Это именно то поведение, которое я хочу.

private void Button_Click(object sender, RoutedEventArgs e)
{
  this.Cursor = System.Windows.Input.Cursors.Pen;
  WaitScreen.Visibility = Visibility.Visible;

  // Do something long here
  for (Int32 i = 0; i < 100000000; i++)
  {
    String s = i.ToString();
  }

  WaitScreen.Visibility = Visibility.Collapsed;
  this.Cursor = System.Windows.Input.Cursors.Arrow; 
}

WaitScreen - это просто таблица с Z-индексом 99, которую я скрываю и показываю.

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

5 ответов

Решение

Я нашел путь! Благодаря этой теме.

public static void ForceUIToUpdate()
{
  DispatcherFrame frame = new DispatcherFrame();

  Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Render, new DispatcherOperationCallback(delegate(object parameter)
  {
    frame.Continue = false;
    return null;
  }), null);

  Dispatcher.PushFrame(frame);
}

Эта функция должна вызываться непосредственно перед длительной операцией. Это заставит поток пользовательского интерфейса обновляться.

Делать это однопоточно действительно будет больно, и это никогда не будет работать так, как вы хотели бы. В конечном итоге окно в WPF станет черным, а программа изменится на "Не отвечает".

Я бы порекомендовал использовать BackgroundWorker для выполнения вашей длительной задачи.

Это не так сложно. Нечто подобное будет работать.

private void DoWork(object sender, DoWorkEventArgs e)
{
    //Do the long running process
}

private void WorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    //Hide your wait dialog
}

private void StartWork()
{
   //Show your wait dialog
   BackgroundWorker worker = new BackgroundWorker();
   worker.DoWork += DoWork;
   worker.RunWorkerCompleted += WorkerCompleted;
   worker.RunWorkerAsync();
}

Затем вы можете посмотреть на событие ProgressChanged, чтобы отобразить прогресс, если хотите, (не забудьте установить для WorkerReportsProgress значение true). Вы также можете передать параметр в RunWorkerAsync, если вашим методам DoWork необходим объект (доступен в e.Argument).

Это действительно самый простой способ, вместо того, чтобы пытаться сделать его однопоточным.

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

Вариант № 1 Выполните код для синхронного отображения ожидающего сообщения тем же методом, который выполняет реальную задачу. Просто поместите эту строку перед длительным процессом:

Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Normal, (Action)(() => { /* Your code to display a waiting message */ }));

Он будет обрабатывать ожидающие сообщения в главном потоке диспетчера в конце Invoke ().

Примечание. Причина выбора Application.Current.Dispatcher, но Dispatcher.CurrentDispatcher объясняется здесь.

Вариант № 2 Отображение экрана "Ожидание" и обновление пользовательского интерфейса (обработка ожидающих сообщений).

Для этого разработчики WinForms выполнили метод Application.DoEvents. WPF предлагает две альтернативы для достижения похожих результатов:

Вариант #2.1 С использованием класса DispatcherFrame.

Проверьте немного громоздкий пример из MSDN:

[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public void DoEvents()
{
    DispatcherFrame frame = new DispatcherFrame();
    Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(ExitFrame), frame);
    Dispatcher.PushFrame(frame);
}

public object ExitFrame(object f)
{
    ((DispatcherFrame)f).Continue = false;
    return null;
}

Вариант № 2.2 Вызов пустого действия

Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Background, (Action)(() => { }));

Смотрите обсуждения, какой из них (2.1 или 2.2) лучше здесь. ИМХО вариант № 1 все же лучше, чем № 2.

Вариант № 3 Отображение ожидающего сообщения в отдельном окне.

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

Загрузите WpfLoadingOverlay.zip с этого github (это был пример из статьи "Отзывчивость WPF: асинхронная загрузка анимаций во время рендеринга", но я больше не могу найти ее в Интернете) или взгляните на основную идею ниже:

public partial class LoadingOverlayWindow : Window
{
    /// <summary>
    ///     Launches a loading window in its own UI thread and positions it over <c>overlayedElement</c>.
    /// </summary>
    /// <param name="overlayedElement"> An element for overlaying by the waiting form/message </param>
    /// <returns> A reference to the created window </returns>
    public static LoadingOverlayWindow CreateAsync(FrameworkElement overlayedElement)
    {
        // Get the coordinates where the loading overlay should be shown
        var locationFromScreen = overlayedElement.PointToScreen(new Point(0, 0));

        // Launch window in its own thread with a specific size and position
        var windowThread = new Thread(() =>
            {
                var window = new LoadingOverlayWindow
                    {
                        Left = locationFromScreen.X,
                        Top = locationFromScreen.Y,
                        Width = overlayedElement.ActualWidth,
                        Height = overlayedElement.ActualHeight
                    };
                window.Show();
                window.Closed += window.OnWindowClosed;
                Dispatcher.Run();
            });
        windowThread.SetApartmentState(ApartmentState.STA);
        windowThread.Start();

        // Wait until the new thread has created the window
        while (windowLauncher.Window == null) {}

        // The window has been created, so return a reference to it
        return windowLauncher.Window;
    }

    public LoadingOverlayWindow()
    {
        InitializeComponent();
    }

    private void OnWindowClosed(object sender, EventArgs args)
    {
        Dispatcher.InvokeShutdown();
    }
}

Другой вариант - написать свою долгосрочную подпрограмму как функцию, которая возвращает IEnumerable<double> чтобы указать прогресс, и просто сказать:

yield return 30;

Это указывало бы, например, на 30% пути. Затем вы можете использовать таймер WPF для его запуска в фоновом режиме в качестве сопрограммы.

Это подробно описано здесь, с примером кода.

Короткий ответ

Упрощая предыдущие ответы, вы можете просто создать такую ​​задачу с ОЧЕНЬ НЕБОЛЬШИМ изменением кода.

      private void Button_Click(object sender, RoutedEventArgs e)
{
  this.Cursor = System.Windows.Input.Cursors.Pen;
  WaitScreen.Visibility = Visibility.Visible;

  Task.Factory.StartnNew(()=>{
      // Do something long here
      for (Int32 i = 0; i < 100000000; i++)
      {
        String s = i.ToString();
      }
  }).ContinueWith(()=>{
      WaitScreen.Visibility = Visibility.Collapsed;
      this.Cursor = System.Windows.Input.Cursors.Arrow; 
  }, TaskScheduler.FromCurrentSynchronizationContext());
}

Масштабируемый ответ

Если вы хотите, чтобы он был более масштабируемым, вы можете создатьRunLongTask(Action action)метод:

      private void RunLongTask(Action action)
{
  IsBusy = true;

  Task.Factory.StartnNew(action).ContinueWith(()=>{
      IsBusy = false;
  }, TaskScheduler.FromCurrentSynchronizationContext());
}

Где можно связатьIsBusyк вашим свойствам управления окном, как некоторыеIsEnabledилиVisibilityсвойство. (Для наглядности нужен конвертер, который я добавлю в конце ответа)

      <Grid Name="OverlayGrid" Visibility={Binding IsBusy, Converter={local:BoolVisibilityCollapseConverter}}">...</Grid>
<!-- where local is defined at the Window element, referring to the namespace where you created the converter code -->

Затем вы используете его как:

      RunLongTask(SomeParameterlessMethodName);

или

      RunLongTask(()=>{
    //long
    //long
    //long
});

Преобразователи, используемые

      using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;

      [ValueConversion(typeof(bool), typeof(Visibility))]
class BoolVisibilityCollapseConverter : MarkupExtension, IValueConverter
{
    //convert from source to target
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        bool v = (bool)value;
        if (v)
            return Visibility.Visible;
        else
            return Visibility.Collapsed;
    }

    //convert from target to source
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new InvalidOperationException("BoolVisibilityHideConverter is intended to be bound one way from source to target");
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

[ValueConversion(typeof(bool), typeof(Visibility))]
class InverseBoolVisibilityCollapseConverter : MarkupExtension, IValueConverter
{
    //convert from source to target
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        bool v = !(bool)value;
        if (v)
            return Visibility.Visible;
        else
            return Visibility.Collapsed;
    }

    //convert from target to source
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new InvalidOperationException("InverseBoolVisibilityCollapseConverter is intended to be bound one way from source to target");
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}
Другие вопросы по тегам