Search code examples
c#wpfmvvmbackgroundworkericommand

How to Properly Implement BackgroundWorker in WPF with MVVM / ICommand Pattern


I have a small WPF application written primarily following MVVM pattern. The point of the program is to read the lines of a text file, parse the data from them, write that data to a list of objects, then write the data in those objects into a specifically-formatted .CSV file.

Even though I have implemented the BackgroundWorker class itself the same way I always have with other applications, this time I am invoking the RunWorkAsync() method from within the Execute() method of my ICommand. While the final output is correct and the application actually delivers the desired result, the UI STILL locks up while the BackgroundWorker is running.

I have wrapped my BackgroundWorker members and all the logic inside a class named "ReaderWriter" with a constructor that takes my ViewModel as a parameter.

By calling the "StartProcess" method of my ReaderWriter instance, the BackgroundWorker's RunWorkerAsync() is called -- this is where I was hoping it would take over on another thread and do my long-running process of reading the source file, parsing the data, and writing the new file; all while periodically doing ReportProgress() to update the ProgressBar.

Here is the code for my ReaderWriter class:

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();
    }
}

And for my 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");
    }
}

And lastly, here is the Command:

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();
    }
}

Any help at this point is very much appreciated, as I've struggled with this for a while now and any attempt to research solutions yield no help for my particular situation. This seems like a fairly unique scenario, but I am hoping we can make it work.

Thank you!


Solution

  • You are using ReportProgress incorrectly and far too often (on every line in the file). It will be being hammered and every call is causing some sort of update in your UI hence locking it up.

    ReportProgress is probably easiest to use by passing a percentage to it. I'm not really sure what you are doing in UpdateProgress_Read with the switch. It would be best to only update as you pass a 100th of your total lines.

    set your progressBar maximum to 100

    ProgressMaximum = 100;
    

    calculate 1% of your total lines

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

    and only Report progress as you pass each 1%

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

    and change UpdateProgress_Read so it just increments

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

    you'll need to come up with better variable names then x and y! and also work out what to do if you have less than 100 lines in the file.