Как правильно реализовать BackgroundWorker в WPF с шаблоном MVVM / ICommand

У меня есть небольшое приложение WPF, написанное в основном по шаблону MVVM. Задача программы - прочитать строки текстового файла, проанализировать данные из них, записать эти данные в список объектов, а затем записать данные из этих объектов в файл .CSV в специальном формате.

Несмотря на то, что я реализовал сам класс BackgroundWorker так же, как и в других приложениях, на этот раз я вызываю метод RunWorkAsync () из метода Execute () моего ICommand. Хотя окончательный результат верен и приложение фактически обеспечивает желаемый результат, пользовательский интерфейс ВСЕ ЕЩЕ блокируется, пока работает BackgroundWorker.

Я заключил свои члены BackgroundWorker и всю логику в класс с именем «ReaderWriter» с конструктором, который принимает мою ViewModel в качестве параметра.

Вызывая метод StartProcess моего экземпляра ReaderWriter, вызывается RunWorkerAsync () BackgroundWorker - именно здесь я надеялся, что он возьмет на себя другой поток и выполнит мой длительный процесс чтения исходного файла, анализа данных , и запись нового файла; все время периодически выполняя ReportProgress () для обновления ProgressBar.

Вот код моего класса ReaderWriter:

class ReaderWriter
{
    private LogDataViewModel vm { get; set; }
    private BackgroundWorker bw { get; set; }

    public ReaderWriter(LogDataViewModel viewModel)
    {
        vm = viewModel;
    }

    public void StartProcess()
    {
        bw = new BackgroundWorker();

        bw.WorkerReportsProgress = true;
        bw.WorkerSupportsCancellation = true;
        bw.DoWork += new DoWorkEventHandler(ReadFromSource);
        bw.ProgressChanged += new ProgressChangedEventHandler(UpdateProgress_Read);
        bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(Completed_Read);

        bw.RunWorkerAsync();
    }

    private void ReadFromSource(object sender, DoWorkEventArgs e)
    {
        double records = 0;
        string[] lines = File.ReadAllLines(vm.SourcePath);
        int lineCount = lines.Length;
        double currentLine = 0;

        bw.ReportProgress(0, lineCount);

        foreach (var line in lines)
        {
            if (line.Length > 0)
            {
                string syntax = line.Substring(17, 6);

                switch (syntax)
                {
                    case "$WIMDA":
                        string[] segments = line.Replace(": <- ", ",").Split(',');
                        vm.LineItems.Add(new LineItem()
                        {
                            Time = segments[0],
                            HgPressure = segments[2],
                            BarPressure = segments[4],
                            AirTemp = segments[6],
                            RelHumidity = segments[10],
                            TrueWindDir = segments[14],
                            KnotsWindSpeed = segments[18],
                            MpsWindSpeed = segments[20]
                        });
                        break;

                    case "$GPGGA":
                        break;

                    default:
                        break;
                }
            }
            currentLine++;
            bw.ReportProgress(1, currentLine);
        }
        using (StreamWriter writer = new StreamWriter(vm.OutputPath))
        {
            writer.WriteLine($"Time,Pressure(Bar),Pressure(Hg),AirTemp({((vm.ConvertTempSetting) ? "F" : "C")}),RelativeHumidity,TrueWindDirection,WindSpeed(Knots),WindSpeed(M/s)");
            foreach (var lineItem in vm.LineItems)
            {
                writer.WriteLine($"{lineItem.Time},{lineItem.BarPressure},{lineItem.HgPressure},{((vm.ConvertTempSetting) ? Converters.ConvertFromCelcius(Convert.ToDouble(lineItem.AirTemp)).ToString() : lineItem.AirTemp)},{lineItem.RelHumidity},{lineItem.TrueWindDir},{lineItem.KnotsWindSpeed},{lineItem.MpsWindSpeed}");
                records++;
            }
        }
        e.Result = records;
    }

    private void UpdateProgress_Read(object sender, ProgressChangedEventArgs e)
    {
        vm.IncrementProgress();
        switch (Type.GetTypeCode(e.UserState.GetType()))
        {
            case TypeCode.Double:
                vm.IncrementProgress();
                break;

            case TypeCode.String:
                break;

            case TypeCode.Int32:
                vm.AppendStatus(DateTime.Now, $"{(int)e.UserState} lines parsed from log file");
                break;

            default:
                break;
        }
        if (vm.IsFirst)
        {
            vm.ProgressIsVisible = true;
            vm.IncrementProgress();
            vm.SetMaximum((int)e.UserState);
            vm.IsFirst = false;
        }
    }
    private void Completed_Read(object sender, RunWorkerCompletedEventArgs e)
    {
        if (e.Cancelled)
        {
            vm.AppendStatus(DateTime.Now, $"Conversion was cancelled by user");
        }
        else
        {
            vm.AppendStatus(DateTime.Now, $"{(double)e.Result} records written to {vm.OutputPath}");
        }
        vm.LineItems.Clear();
    }
}

И для моей ViewModel:

public class LogDataViewModel : LogDataModel
{
    #region Commands
    public BeginProcessCommand BeginCommand { get; set; }
    public SelectOutputPathCommand OutputCommand { get; set; }
    public SelectSourceCommand SourceCommand { get; set; }
    public ResetCommand ResetCommand { get; set; }
    #endregion

    public bool IsFirst { get; set; }

    public LogDataViewModel()
    {
        BeginCommand = new BeginProcessCommand(this);
        OutputCommand = new SelectOutputPathCommand(this);
        SourceCommand = new SelectSourceCommand(this);
        ResetCommand = new ResetCommand(this);

        PrepareViewModel();
    }


    private void PrepareViewModel()
    {
        ProgressValue = 0;
        ProgressMaximum = 0;
        ProgressIsVisible = false;
        IsFirst = true;

        OutputPath = Properties.Settings.Default.RememberedSavePath;
        if (LineItems == null) LineItems = new List<LineItem>();
        if (StatusActions == null) StatusActions = new ObservableCollection<StatusAction>();
        AppendStatus(DateTime.Now, "Initialized Program");
    }
}

И, наконец, вот команда:

public class BeginProcessCommand : ICommand
{
    LogDataViewModel vm;

    public BeginProcessCommand(LogDataViewModel viewModel)
    {
        vm = viewModel;
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public bool CanExecute(object parameter)
    {
        bool result = true;

        if (!File.Exists(vm.SourcePath))
            result = false;
        try
        {
            if (!Directory.Exists(Path.GetDirectoryName(vm.SourcePath)))
                result = false;
        }
        catch
        {
            result = false;
        }
        return result;
    }

    public void Execute(object parameter)
    {
        ReaderWriter rw = new ReaderWriter(vm);
        rw.StartProcess();
    }
}

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

Спасибо!


person Paul Karkoska    schedule 03.08.2017    source источник
comment
Это потому, что вы забиваете пользовательский интерфейс обновлениями. Старайтесь сообщать о прогрессе только каждые 100 строк. Если у вас много линий и цикл вращается довольно быстро, увеличьте это число.   -  person    schedule 03.08.2017
comment
Спасибо, Уилл. Я сделал то, что вы предложили, и это сработало отлично!   -  person Paul Karkoska    schedule 04.08.2017


Ответы (1)


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

ReportProgress, вероятно, проще всего использовать передавая ему процент. Я не совсем уверен, что вы делаете в UpdateProgress_Read с переключателем. Лучше всего обновлять только после того, как вы пройдете сотую часть своего общего числа строк.

установите свой прогрессБар максимум на 100

ProgressMaximum = 100;

рассчитайте 1% от ваших общих строк

var x = lineCount / 100;
var y = 0;

и сообщать только о прогрессе по мере прохождения каждого 1%

currentLine++;
if((currentLine % x) == 0)
{
    y++;
    bw.ReportProgress(y);
}

и измените UpdateProgress_Read, чтобы он просто увеличивался

private void UpdateProgress_Read(object sender, ProgressChangedEventArgs e)
{
    vm.IncrementProgress();
}

вам нужно придумать лучшие имена переменных, чем x и y! а также подумайте, что делать, если в файле меньше 100 строк.

person ajg    schedule 03.08.2017
comment
Большое спасибо за подробный ответ, айг! Ваше подробное объяснение помогло мне лучше понять, как следует использовать ReportProgress (). Я изменил небольшой фрагмент кода в своей итерации foreach, и теперь он работает отлично. Я рад, что это была намного более простая проблема, о которой я изначально подозревал! знак равно - person Paul Karkoska; 04.08.2017