Вопрос по c#, asynchronous, wpf – Как запустить и взаимодействовать с асинхронной задачей из графического интерфейса WPF

27

У меня есть WPF GUI, где я хочу нажать кнопку, чтобы запустить длинную задачу, не останавливая окно на время выполнения задачи. Во время выполнения задачи я хотел бы получать отчеты о ходе работы и добавить еще одну кнопку, которая остановит задачу в любой момент, когда я выберу.

Я не могу понять, как правильно использовать async / await / task. Я не могу включить все, что я пробовал, но это то, что у меня есть на данный момент.

Класс окна WPF:

public partial class MainWindow : Window
{
    readonly otherClass _burnBabyBurn = new OtherClass();
    internal bool StopWorking = false;

    //A button method to start the long running method
    private async void Button_Click_3(object sender, RoutedEventArgs e)
    {   
        Task burnTheBaby = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);

        await burnTheBaby;
    }

    //A button Method to interrupt and stop the long running method
    private void StopButton_Click(object sender, RoutedEventArgs e)
    {
        StopWorking = true;
    }

    //A method to allow the worker method to call back and update the gui
    internal void UpdateWindow(string message)
    {
        TextBox1.Text = message;
    }
}

И класс для рабочего метода:

class OtherClass
{
    internal Task ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
    {       
        var tcs = new TaskCompletionSource<int>();       

        //Start doing work
        gui.UpdateWindow("Work Started");        

        While(stillWorking)
        {
        //Mid procedure progress report
        gui.UpdateWindow("Bath water n% thrown out");        
        if (gui.StopTraining) return tcs.Task;
        }

        //Exit message
        gui.UpdateWindow("Done and Done");       
        return tcs.Task;        
    }
}

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

Мне нужно знать, как организовать объявления async / await / task, чтобы

А) рабочий метод, чтобы не блокировать окно графического интерфейса
Б) пусть рабочий метод обновит окно графического интерфейса
В) разрешить окну графического интерфейса остановить прерывание и остановить рабочий метод

Любая помощь или указатели очень ценятся.

Ваш Ответ

5   ответов
-2

как был задан вопрос, но я думаю, что стоит отметить, что класс BackgroundWorker предназначен именно для удовлетворения требований A, B и C.

Полный пример на странице справки msdn:https://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker(v=vs.110).aspx

В C # существует так много моделей параллелизма, и все они достигают A, B, C. Bizhan
Кроме того, я не уверен, что ваш комментарий в любом случае является полностью правильным. Увидеть:stackoverflow.com/questions/12414601/... maplemale
Это должен был быть комментарий, если не какая-то реализация Souvik Ghosh
4
,

вет Биджана, чтобы помочь мне разобраться в проблеме, используя удобное форматирование, предоставляемое Stack Overflow.

Внимательно прочитав и отредактировав пост Биджана, я наконец понял:Как ждать завершения асинхронного метода?

В моем случае выбранный ответ для этого другого поста в конечном итоге и привел меня к решению моей проблемы:

«Избегайтеasync void, Ваши методы возвращаютсяTask вместоvoid, Тогда ты можешьawait их."

Вот моя упрощенная версия ответа Биджана (отлично):

1) Это запускает задачу, используя async и ожидает:

private async void Button_Click_3(object sender, RoutedEventArgs e)
{
    // if ExecuteLongProcedureAsync has a return value
    var returnValue = await Task.Run(()=>
        ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3));
}

2) Это метод для выполнения асинхронно:

bool stillWorking = true;
internal void ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
{
    //Start doing work
    gui.UpdateWindow("Work Started");

    while (stillWorking)
    {
        //put a dot in the window showing the progress
        gui.UpdateWindow(".");

        //the following line blocks main thread unless
        //ExecuteLongProcedureAsync is called with await keyword
        System.Threading.Thread.Sleep(50);
    }

    gui.UpdateWindow("Done and Done");
} 

3) вызвать операцию, которая включает в себя свойство из GUI:

void UpdateWindow(string text)
{
    //safe call
    Dispatcher.Invoke(() =>
    {
        txt.Text += text;
    });
}

Или же,

void UpdateWindow(string text)
{
    //simply
    txt.Text += text;
}

Заключительные комментарии) В большинстве случаев у вас есть два метода.

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

Thread.Sleep вasync Метод блокирует основной поток. но ждать задачи нет.Выполнение останавливается на текущем потоке (втором потоке) наawait заявления до завершения задачи.Вы не можете использоватьawait внеasync метод

Второй метод (ExecuteLongProcedureAsync) оборачивается в задачу и возвращает общийTask<original return type> объект, который может быть проинструктирован для асинхронной обработки путем добавленияawait перед этим.

Все в этом методе выполняется асинхронноВажный:

Лиеро поднял важный вопрос. Когда вы связываете элемент со свойством ViewModel,свойство изменило обратный вызов выполняется в потоке пользовательского интерфейса. Таким образом, нет необходимости использоватьDispatcher.Invoke. Изменения значений, инициированные INotifyPropertyChanged, автоматически отправляются обратно диспетчеру.

Эрик, ремикс ответов других людей очень воодушевляет, если ты им доверяешь. Либеральная лицензия (см. «Cc by-sa 3.0» в нижнем колонтитуле сайта) явно разрешает это. halfer
Просто удалил этот пост, потому что я видел, что кто-то проголосовал за него, поэтому кажется, что кто-то увидел в этом ценность. Я не знаю, что с этим делать, и я прошу прощения, если это противоречит правилам. Я скопировал и вставил существующий ответ и удалил текст, чтобы я мог обдумать проблему. Eric D
46

private async void Button_Click_3(object sender, RoutedEventArgs e)
{
    txt.Text = "started";
    await Task.Run(()=> HeavyMethod(this));
    txt.Text = "done";
}
internal void HeavyMethod(MainWindow gui)
{
    while (stillWorking)
    {
        window.Dispatcher.Invoke(() =>
        {
            // UI operations go inside of Invoke
            txt.Text += ".";
        });

        // CPU-bound or I/O-bound operations go outside of Invoke
        System.Threading.Thread.Sleep(51);
    }
}
Объяснение:

Ты можешь толькоawait вasync метод.

Task.Run очередиTask впул потоков (то есть он использует существующий поток из пула потоков или создает новый поток в пуле потоков для запуска задачи)

Казнь ожидаетawait для завершения задачи и возвращает ее результаты, не блокируя основной поток из-заasync ключевое словомагическая способность:

магия изasync Ключевое слово в том, что этоне создать другую тему. Это только позволяет компиляторусдаваться а такжезабрать контроль над этим методом.

Стоит также отметить, что еслиtask имеет типTask<T> затемawait task возвращает значениеT, еслиtask имеет типTask затемawait task ничего не возвращает (или возвращаетvoid)

Так

Вашосновная нить вызываетasync метод (Button_Click_3) как обычный метод и без потоков до сих пор ... Теперь вы можете запустить задачу внутриButton_Click_3 как это:

private async void Button_Click_3(object sender, RoutedEventArgs e)
{
    //queue a task to run on threadpool
    Task task = Task.Run(()=>
        ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3));
    //wait for it to end without blocking the main thread
    await task;
}

или просто

private async void Button_Click_3(object sender, RoutedEventArgs e)
{
    await Task.Run(()=>
        ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3));
}

или еслиExecuteLongProcedureAsync имеет возвращаемое значение типаstring

private async void Button_Click_3(object sender, RoutedEventArgs e)
{
    Task<string> task = Task.Run(()=>
        ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3));
    string returnValue = await task;
}

или просто

private async void Button_Click_3(object sender, RoutedEventArgs e)
{
    string returnValue = await Task.Run(()=>
        ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3));

    //or in cases where you already have a "Task returning" method:
    //  var httpResponseInfo = await httpRequestInfo.GetResponseAsync();
}

Метод внутри задачи (илиExecuteLongProcedureAsync) работаетасинхронно и выглядит так:

//change the value for the following flag to terminate the loop
bool stillWorking = true;

//calling this method blocks the calling thread
//you must run a task for it
internal void ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
{
    //Start doing work
    gui.UpdateWindow("Work Started");

    while (stillWorking)
    {
        //put a dot in the window showing the progress
        gui.UpdateWindow(".");
        //the following line will block main thread unless
        //ExecuteLongProcedureAsync is called with await keyword
        System.Threading.Thread.Sleep(51);
    }

    gui.UpdateWindow("Done and Done");
} 
Примечание 1:

Task.Run более новая (.NetFX4.5) и более простая версияTask.Factory.StartNew

await являетсяне Task.Wait()

Заметка 2:

Sleep блоки основной поток даже в том, что он вызывается в методе сasync ключевое слово.

await предотвращает задача блокирования основного потока из-заasync ключевое слово.

private async void Button_Click(object sender, RoutedEventArgs e)
{
        ExecuteLongProcedureAsync();//blocks
        await Task.Run(() => ExecuteLongProcedureAsync());//does not block
}
Примечание 3 (GUI):

Если вам нужно получить доступ к GUI асинхронно (внутриExecuteLongProcedureAsync метод),взывать любая операция, которая включает доступ к полям GUI:

void UpdateWindow(string text)
{
    //safe call
    Dispatcher.Invoke(() =>
    {
        txt.Text += text;
    });
}

Тем не менее, если задача запускается в результатесвойство изменило обратный вызов из ViewModel, нет необходимости использоватьDispatcher.Invoke потому что обратный вызов на самом деле выполняется из потока пользовательского интерфейса.

Доступ к коллекциям в потоках не из пользовательского интерфейса

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

Изменения значений, инициированные INotifyPropertyChanged, автоматически отправляются обратно диспетчеру.

Как включить межпоточный доступ

Помните,async Сам метод работает в основном потоке. Так что это действительно:

private async void Button_Click_3(object sender, RoutedEventArgs e)
{
    txt.Text = "starting"; // UI Thread
    await Task.Run(()=> ExecuteLongProcedureAsync1());
    txt.Text = "waiting"; // UI Thread
    await Task.Run(()=> ExecuteLongProcedureAsync2());
    txt.Text = "finished"; // UI Thread
}
Прочитайте больше

MSDN объясняетTask

MSDN объясняетasync

async await - За кулисами

async await - ЧАСТО ЗАДАВАЕМЫЕ ВОПРОСЫ

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

расследоватьпараллельное пространство имен

Наконец, прочитайте эту электронную книгу:Patterns_of_Parallel_Programming_CSharp

Замечательно, спасибо тебе большое, это сводило меня с толку. Теперь я тоже пойму, что остановить / остановить достаточно легко :) Kickaha
Отличный ответ. Я хотел бы добавить еще один вариант, хотя. Если вы связываете свой интерфейс с объектом, который реализуетINotifyPropertyChanged (обычно называется ViewModel), затем вы можете изменить свойства объекта из асинхронного потока. Механизм привязки данных WPF переключится на поток пользовательского интерфейса. Liero
Пожалуйста. для записи иногда лучше спать асинхронный поток, добавивSystem.Threading.Thread.Sleep(10); до конца метода UpdateWindow. Bizhan
Спасибо @Liero за полезную информацию. Я положил это в ответ Bizhan
2

Вот пример использованияasync/await, IProgress<T> а такжеCancellationTokenSource, Это современные возможности языка C # и .Net Framework, которые выдолжен использовать. Другие решения заставляют мои глаза немного кровоточить.

Особенности кодаПосчитайте до 100 в течение 10 секундОтображение прогресса на индикаторе выполненияДлительная работа (период ожидания) выполняется без блокировки пользовательского интерфейсаПользователь отменил отменуИнкрементные обновления прогрессаОтчет о состоянии после операцииВид
<Window x:Class="ProgressExample.MainWindow"
        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"
        mc:Ignorable="d"
        Title="MainWindow" SizeToContent="WidthAndHeight" Height="93.258" Width="316.945">
    <StackPanel>
        <Button x:Name="Button_Start" Click="Button_Click">Start</Button>
        <ProgressBar x:Name="ProgressBar_Progress" Height="20"  Maximum="100"/>
        <Button x:Name="Button_Cancel" IsEnabled="False" Click="Button_Cancel_Click">Cancel</Button>
    </StackPanel>
</Window>
Код
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private CancellationTokenSource currentCancellationSource;

        public MainWindow()
        {
            InitializeComponent();
        }

        private async void Button_Click(object sender, RoutedEventArgs e)
        {
            // Enable/disabled buttons so that only one counting task runs at a time.
            this.Button_Start.IsEnabled = false;
            this.Button_Cancel.IsEnabled = true;

            try
            {
                // Set up the progress event handler - this instance automatically invokes to the UI for UI updates
                // this.ProgressBar_Progress is the progress bar control
                IProgress<int> progress = new Progress<int>(count => this.ProgressBar_Progress.Value = count);

                currentCancellationSource = new CancellationTokenSource();
                await CountToOneHundredAsync(progress, this.currentCancellationSource.Token);

                // Operation was successful. Let the user know!
                MessageBox.Show("Done counting!");
            }
            catch (OperationCanceledException)
            {
                // Operation was cancelled. Let the user know!
                MessageBox.Show("Operation cancelled.");
            }
            finally
            {
                // Reset controls in a finally block so that they ALWAYS go 
                // back to the correct state once the counting ends, 
                // regardless of any exceptions
                this.Button_Start.IsEnabled = true;
                this.Button_Cancel.IsEnabled = false;
                this.ProgressBar_Progress.Value = 0;

                // Dispose of the cancellation source as it is no longer needed
                this.currentCancellationSource.Dispose();
                this.currentCancellationSource = null;
            }
        }

        private async Task CountToOneHundredAsync(IProgress<int> progress, CancellationToken cancellationToken)
        {
            for (int i = 1; i <= 100; i++)
            {
                // This is where the 'work' is performed. 
                // Feel free to swap out Task.Delay for your own Task-returning code! 
                // You can even await many tasks here

                // ConfigureAwait(false) tells the task that we dont need to come back to the UI after awaiting
                // This is a good read on the subject - https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
                await Task.Delay(100, cancellationToken).ConfigureAwait(false);

                // If cancelled, an exception will be thrown by the call the task.Delay
                // and will bubble up to the calling method because we used await!

                // Report progress with the current number
                progress.Report(i);
            }
        }

        private void Button_Cancel_Click(object sender, RoutedEventArgs e)
        {
            // Cancel the cancellation token
            this.currentCancellationSource.Cancel();
        }
    }
6

TaskCompletionSource<T> это неверно.TaskCompletionSource<T> это способ создатьTAP-совместимые обертки для асинхронных операций. В вашемExecuteLongProcedureAsync метод, пример кода привязан к ЦП (т. е. по сути синхронный, а не асинхронный).

Итак, гораздо естественнее написатьExecuteLongProcedure как синхронный метод. Это также хорошая идея использовать стандартные типы для стандартного поведения, в частности,с помощьюIProgress<T> для обновления прогресса а такжеCancellationToken для отмены:

internal void ExecuteLongProcedure(int param1, int param2, int param3,
    CancellationToken cancellationToken, IProgress<string> progress)
{       
  //Start doing work
  if (progress != null)
    progress.Report("Work Started");

  while (true)
  {
    //Mid procedure progress report
    if (progress != null)
      progress.Report("Bath water n% thrown out");
    cancellationToken.ThrowIfCancellationRequested();
  }

  //Exit message
  if (progress != null)
    progress.Report("Done and Done");
}

Теперь у вас есть более многократно используемый тип (без зависимостей GUI), который использует соответствующие соглашения. Может использоваться как таковой:

public partial class MainWindow : Window
{
  readonly otherClass _burnBabyBurn = new OtherClass();
  CancellationTokenSource _stopWorkingCts = new CancellationTokenSource();

  //A button method to start the long running method
  private async void Button_Click_3(object sender, RoutedEventArgs e)
  {
    var progress = new Progress<string>(data => UpdateWindow(data));
    try
    {
      await Task.Run(() => _burnBabyBurn.ExecuteLongProcedure(intParam1, intParam2, intParam3,
          _stopWorkingCts.Token, progress));
    }
    catch (OperationCanceledException)
    {
      // TODO: update the GUI to indicate the method was canceled.
    }
  }

  //A button Method to interrupt and stop the long running method
  private void StopButton_Click(object sender, RoutedEventArgs e)
  {
    _stopWorkingCts.Cancel();
  }

  //A method to allow the worker method to call back and update the gui
  void UpdateWindow(string message)
  {
    TextBox1.Text = message;
  }
}
Ааа .. да достаточно справедливо. Чтобы реализовать паузу, я обязательно обязательно выделю и сохраню, а затем возобновлю состояние из сохранения вместо того, чтобы предполагать постоянство, когда явно не должно быть никаких. Спасибо. Kickaha
Ура, как вы говорите, это помогает держать мой тип переносимым на разные уровни интерфейса. Кроме того, токен отмены фактически отменяет поток и (ошибочно полагает) освобождает ресурсы потоков ... и мое прерывание на основе токена bool фактически приостанавливает выполнение потока, так что это тоже хорошо. Спасибо :) Kickaha
@ Kickaha: Thebool подход технически неверен; это делает неверные предположения оМодель памяти .NET. Stephen Cleary

Похожие вопросы