Асинхронное копирование в купе с progressBar'om

На самом деле задача на половину выполнена, я имею готовое решение не без помощи backgoundWorker:

Функция копирования:

private void copy_file(string source_, string des)
        {
                FileStream fsOut = new FileStream(des, FileMode.Create);
                FileStream fsIn = new FileStream(source_, FileMode.Open);
                byte[] bt = new byte[1048756];
                int readByte;

                while ((readByte = fsIn.Read(bt, 0, bt.Length)) > 0)
                {
                    fsOut.Write(bt, 0, readByte);
                    backgroundWorker1.ReportProgress((int)(fsIn.Position * 100 / fsIn.Length));
                }
                fsIn.Close();
                fsOut.Close();
        }

DoWork:

private void Worker_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
        {
            copy_file(server_path + filename, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\" + filename);
        }

RunWorkerCompleted:

private void Worker_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
        {
            if (e.Error == null)
            {               
                err_msg.Text = "Файл находится на рабочем столе!";
            }
            else
            {
                err_msg.Text = "Произошла ошибка.";
            }
            download_sound.Enabled = true;           
        }

ProgressChanged:

private void Worker_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
        {
            progressBar1.Value = e.ProgressPercentage;
            err_msg.Text = progressBar1.Value.ToString() + "%";
        }

Click Button:

Worker.RunWorkerAsync();

Выше указанный метод отлично справляется, но при использовании этого метода в Асинхронных функциях(например private async void button_click()), я получаю эффект очередности потоков, эм... файл копируется - помещается в нужную папку - прогресс бар мгновенно заполняется. Пробовал копировать через сеть, та же беда. Но к счастью работает когда async у btn отсутствует.

Теперь к сути:

Т.к. изначально мой план был использовать async\await методы, я перешел к такому виду копирования файлов:

public async Task CopyFilesAsync(StreamReader Source, StreamWriter Destination)
        {
            char[] buffer = new char[0x1000];
            int numRead;
            while ((numRead = await Source.ReadAsync(buffer, 0, buffer.Length)) != 0)
            {
                await Destination.WriteAsync(buffer, 0, numRead);
            }
        }

_

public async Task CopyFile(string startDirectoy, string endDirectory)
        {
            using (StreamReader SourceReader = File.OpenText(startDirectoy))
            {
                using (StreamWriter DestinationWriter = File.CreateText(endDirectory))
                {
                    await CopyFilesAsync(SourceReader, DestinationWriter);
                    SourceReader.Close();
                    DestinationWriter.Close();
                }
            }
        }

Никак не могу сообразить каким образом мне заставить progressBar выполнять мою задачу. Я так же не могу достучаться до StreamReader.Position использовать:

progressBar1.Value=(int)((StreamReader.BaseStream.Position/StreamReader.BaseStream.Length)*100);

у меня не вышло. ProgressBar работал у меня отдельно от всего >< Если есть пару советов, прошу их дать, буду очень благодарен!

введите сюда описание изображения


Ответы (1 шт):

Автор решения: aepot

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

  • Копирует файл асинхронно
  • По нажатию кнопки Cancel можно остановить копирование
  • button1 - кнопка Copy, button2 - кнопка Cancel, progressBar1 - прогресс бар
  • реализован отчет о прогрессе с помощью интерфейса IProgress и класса Progress
public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        button2.Enabled = false;
    }

    // Токен отмены служит для прерывания работы копирования в любой момент
    private CancellationTokenSource _cts;

    private async Task CopyFileAsync(string srcPath, string dstPath, IProgress<int> status, CancellationToken token)
    {
        const int bufferLength = 16384;

        using FileStream src = File.OpenRead(srcPath);
        using FileStream dst = File.Create(dstPath);
        long currentPosition = 0;
        long contentLength = src.Length;
        int progress = -1;
        int oldProgress;
        byte[] buffer = new byte[bufferLength];
        int bytesReceived;
        while ((bytesReceived = await src.ReadAsync(buffer, 0, bufferLength, token).ConfigureAwait(false)) > 0)
        {
            await dst.WriteAsync(buffer, 0, bytesReceived, token).ConfigureAwait(false);

            currentPosition += bytesReceived;
            oldProgress = progress;
            progress = (int)(currentPosition * 100 / contentLength);
            // так как значение от 0 до 100, нет особого смысла повтороно обновлять интерфейс, если значение не изменилось.
            if (oldProgress != progress)
            {
                status?.Report(progress);
            }
        }
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        if (_cts != null)
            return;
        button1.Enabled = false;
        button2.Enabled = true;

        using (_cts = new CancellationTokenSource())
        {
            try
            {
                await CopyFileAsync("file1.txt", "file2.txt", new Progress<int>(v => { progressBar1.Value = v; }), _cts.Token);
            }
            catch (OperationCanceledException) { } // была отмена, ничего не делать
            catch (Exception ex)
            {
                MessageBox.Show(ex.ToString(), ex.GetType().Name);
            }
            progressBar1.Value = 0;
        }
        _cts = null;

        button1.Enabled = true;
        button2.Enabled = false;
    }

    private void button2_Click(object sender, EventArgs e)
    {
        _cts?.Cancel();
    }
}

Чтобы прогресс обновлялся с учетом медленной анимации прогресс-бара винформ

private async Task CopyFileAsync(string srcPath, string dstPath, IProgress<int> status, CancellationToken token)
{
    const int bufferLength = 16384;

    using FileStream src = File.OpenRead(srcPath);
    using FileStream dst = File.Create(dstPath);
    long currentPosition = 0;
    long contentLength = src.Length;
    int progress = -1;
    int oldProgress;
    byte[] buffer = new byte[bufferLength];
    int bytesReceived;

    long elapsed = 0;
    var sw = Stopwatch.StartNew();

    while ((bytesReceived = await src.ReadAsync(buffer, 0, bufferLength, token).ConfigureAwait(false)) > 0)
    {
        await dst.WriteAsync(buffer, 0, bytesReceived, token).ConfigureAwait(false);

        currentPosition += bytesReceived;
        oldProgress = progress;
        progress = (int)(currentPosition * 100 / contentLength);
        
        // 1 / 30 = 0.033 - 30 кадров в секунду
        if (oldProgress != progress && (progress == 100 || sw.ElapsedMilliseconds - 33 > elapsed))
        {
            status?.Report(progress);
            elapsed = sw.ElapsedMilliseconds;
        }
    }
}

Совет от @PavelMayorov стартовать копирование сразу в пуле потоков можно реализовать вот так

IProgress<int> status = new Progress<int>(v => { progressBar1.Value = v; });
await Task.Run(() => CopyFileAsync("file1.txt", "file2.txt", status, _cts.Token));
→ Ссылка