Отмена загрузки WebClient во время ожидания завершения загрузки

Ищем более приемлемый шаблон ожидания WebClient для:

  • Скачать файл (может занять пару сотен миллисекунд или несколько минут)
  • Дождитесь завершения загрузки, прежде чем выполнять любую другую работу.
  • Периодически проверять флаг другого класса (bool) и при необходимости отменять загрузку (невозможно изменить этот класс)

Ограничения:

  • Невозможно использовать async/await, если это не что-то вроде Task.Run(async () => await method())
  • Когда вызывается метод Download, он просто должен вести себя как обычный метод, возвращающий строку
  • Может использовать любую функцию из .Net 4.5 и компилятора Roslyn.
  • Не имеет значения, используется ли WebClient.DownloadFileTaskAsync или DownloadFileAsync; просто нужно иметь возможность отменить загрузку по мере необходимости с помощью WebClient

Текущая реализация работает, но не совсем корректно. Есть ли более приемлемая альтернатива, чем использование цикла while и Thread.Sleep для периодической проверки otherObject.ShouldCancel при использовании WebClient?

private string Download(string url)
{
    // setup work
    string fileName = GenerateFileName();

    // download file
    using (var wc = new WebClient()) 
    {
        wc.DownloadFileCompleted += OnDownloadCompleted

        Task task = wc.DownloadFileTaskAsync(url, fileName);

        // Need to wait until either the download is completed
        // or download is canceled before doing any other work
        while (wc.IsBusy || task.Status == TaskStatus.WaitingForActivation) 
        {
            if (otherObject.ShouldCancel) 
            {
                wc.CancelAsync();
                break;
            }

            Thread.Sleep(100);
        }

        void OnDownloadCompleted(object obj, AsyncCompletedEventArgs args)
        {
            if(args.Cancelled)
            {
                // misc work
                return;
            }

            // misc work (different than other work below)
        }
    }

    // Other work after downloading, regardless of cancellation.
    // Could include in OnDownloadCompleted as long as this
    // method blocked until all work was complete

    return fileName;
}

person Metro Smurf    schedule 28.02.2018    source источник
comment
Вы пытались передать аргумент CancellationToken, а затем использовать CancellationToken.Register(webClient.CancelAsync);?   -  person Khaled El Kholy    schedule 28.02.2018
comment
Я бы сказал, что цикл во время сна соответствует вашим требованиям. Поскольку Download должен быть синхронным - ему все равно нечего делать во время ожидания завершения загрузки, так почему бы не опросить этот флаг тем временем. Конечно, вы можете добавить ко всему этому какие-то причудливые вещи, но это мало что изменит.   -  person Evk    schedule 28.02.2018
comment
@KhaledElKholy - не нужно ли периодически опрашивать otherObject.ShouldCancel, чтобы сигнализировать cancellationToken.Cancel()?   -  person Metro Smurf    schedule 28.02.2018
comment
Нет, отменаTokenSource.Cancel() приведет к вызову webClient.Cancel(), который вы зарегистрировали как обратный вызов, что затем приведет к тому, что асинхронная задача вызовет исключение WebException или TaskCanceledException. Кроме того, почему бы вам не передать экземпляр cancelTokenSource в otherObject, чтобы он мог вызывать cancelTokenSource.Cancel() без периодической проверки свойства otherObject.ShouldCancel? у вас есть доступ к внутренней реализации типа otherObject?   -  person Khaled El Kholy    schedule 28.02.2018
comment
@KhaledElKholy - я не могу ничего изменить в otherObject (см. вопрос, 3-й пункт).   -  person Metro Smurf    schedule 28.02.2018
comment
Почему бы не подписаться на событие DownloadProgressChanged и не проверить там свойство otherObject.ShouldCancel? его следует вызывать часто, чтобы у вас была возможность вызвать wc.CancelAsync(); также вы можете вызвать task.Wait() вместо зацикливания и использования Thread.Sleep(). Если вы используете task.Wait(), вам придется обернуть его в блок try catch, потому что он вызовет AggregateException при вызове wc.CancelAsync.   -  person Khaled El Kholy    schedule 28.02.2018
comment
@KhaledElKholy - хорошая идея; На самом деле я рассматривал возможность отключения события изменения хода выполнения, но в конечном итоге отказался от него, относительно: если удаленный сервер начнет зависать во время загрузки, я не был уверен, будет ли вызвано событие изменения хода выполнения, что приведет к невозможности чтобы отключить флаг ShouldCancel. Теперь я склоняюсь к тому, чтобы оставить все как есть, если только не увижу какой-то другой убедительной альтернативы, которая может быть автономной в этом единственном методе.   -  person Metro Smurf    schedule 28.02.2018


Ответы (1)


Я надеюсь, что это полезно. В основном ваша оболочка регистрирует обратный вызов, используя cancelToken.Register(webClient.Cancel); после вызова cancelToken.Cancel() асинхронная задача должна вызвать исключение, которое вы можете обработать следующим образом:

public class Client
{
    public async Task<string> DownloadFileAsync(string url, string outputFileName, CancellationToken cancellationToken)
    {
        using (var webClient = new WebClient())
        {
            cancellationToken.Register(webClient.CancelAsync);

            try
            {
                var task = webClient.DownloadFileTaskAsync(url, outputFileName);

                await task; // This line throws an exception when cancellationTokenSource.Cancel() is called.
            }
            catch (WebException ex) when (ex.Message == "The request was aborted: The request was canceled.")
            {
                throw new OperationCanceledException();
            }
            catch (TaskCanceledException)
            {
                throw new OperationCanceledException();
            }

            return outputFileName;
        }
    }
}

Простой способ попробовать этот пример

    private async static void DownloadFile()
    {
        var cancellationTokenSource = new CancellationTokenSource();
        var client = new Client();

        var task = client.DownloadFileAsync("url",
            "output.exe", cancellationTokenSource.Token);

        cancellationTokenSource.Token.WaitHandle.WaitOne(TimeSpan.FromSeconds(5));

        cancellationTokenSource.Cancel();

        try
        {
            var result = await task;
        }
        catch (OperationCanceledException)
        {
            // Operation Canceled
        }
    }

В более реалистичном сценарии cancelTokenSource.Cancel() будет вызываться событием, вызванным взаимодействием с пользователем или обратным вызовом.

Обновить

Другой подход — подписаться на событие DownloadProgressChanged и проверить наличие otherObject.ShouldCancel при вызове обратного вызова.

Вот пример:

public class Client
{
    public string Download(string url)
    {
        // setup work
        string fileName = GenerateFileName();

        // download file
        using (var wc = new WebClient())
        {
            wc.DownloadProgressChanged += OnDownloadProgressChanged;
            wc.DownloadFileCompleted += OnDownloadFileCompleted;

            DownloadResult downloadResult = DownloadResult.CompletedSuccessfuly;

            void OnDownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
            {
                if (otherObject.ShouldCancel)
                {
                    ((WebClient)sender).CancelAsync();
                }
            }

            void OnDownloadFileCompleted(object sender, AsyncCompletedEventArgs e)
            {
                if (e.Cancelled)
                {
                    downloadResult = DownloadResult.Cancelled;
                    return;
                }

                if (e.Error != null)
                {
                    downloadResult = DownloadResult.ErrorOccurred;
                    return;
                }
            }

            try
            {
                Task task = wc.DownloadFileTaskAsync(url, fileName);
                task.Wait();
            }
            catch (AggregateException ex)
            {
            }

            switch (downloadResult)
            {
                case DownloadResult.CompletedSuccessfuly:

                    break;
                case DownloadResult.Cancelled:

                    break;
                case DownloadResult.ErrorOccurred:

                    break;
            }
        }

        // Other work after downloading, regardless of cancellation.
        // Could include in OnDownloadCompleted as long as this
        // method blocked until all work was complete

        return fileName;
    }
}

public enum DownloadResult
{
    CompletedSuccessfuly,
    Cancelled,
    ErrorOccurred
}
person Khaled El Kholy    schedule 28.02.2018
comment
Оцените ответ. Однако я не совсем уверен, как это обеспечивает альтернативный подход? 1) otherObject.ShouldCancel не используется для подтверждения отмены. 2) Теперь есть произвольный 5-секундный тайм-аут. 3) Использование async/await было специально исключено как вариант (см. вопрос). - person Metro Smurf; 28.02.2018