First of all, I've seen many similar issues here on SO and around the net. None of these appear to address my particular issue.
I have a simple BackgroundWorker
whose job is to read a file line-by-line and report progress to indicate how far through it is. There are up to a total of 65,553 lines in the file, so it is important to me that the BackgroundWorker
finishes as fast as possible.
Since MVVM is built on separation of concerns (SoC) and the decoupling of the View and View-Model, the BackgroundWorker
updates properties on the View-Model that the View binds to. My setup is very similar to Kent Boorgaart's answer on another question.
In high-stress scenarios where the BackgroundWorker
demands a lot of CPU without sleeping, the UI thread is starved and not able to update any of the bound properties that have been notified via INotifyPropertyChanged
. However, if the BackgroundWorker
sleeps, then the job will not finish as fast as possible.
How can I ensure the View receives progress updates while respecting MVVM and while not throttling the job?
In the View-Model the BackgroundWorker
is setup like this. The Start()
function is called by a RelayCommand
(part of MVVM-Light).
public void Start(string memoryFile)
{
this.memoryFile = memoryFile;
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.WorkerReportsProgress = true;
worker.RunWorkerAsync();
}
Here is the code for the actual work performed:
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
IsAnalyzing = true;
bw.ReportProgress(0, new ProgressState("Processing..."));
int count = File.ReadLines(memoryFile).Count();
StreamReader reader = new StreamReader(memoryFile);
string line = "";
int lineIndex = 0;
while ((line = reader.ReadLine()) != null)
{
bw.ReportProgress((int)(((double)lineIndex / count) * 100.0d));
//Process record... (assume time consuming operation)
HexRecord record = HexFileUtil.ParseLine(line);
lineIndex++;
if (lineIndex % 150 == 0)
{
//Uncomment to give UI thread some time.
//However, this will throttle the job.
//Thread.Sleep(5);
}
}
bw.ReportProgress(100, new ProgressState("Done."));
Thread.Sleep(1000);
IsAnalyzing = false;
}
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Progress = e.ProgressPercentage;
if (e.UserState != null)
{
Task = ((ProgressState)e.UserState).Task;
}
}
In the above code, the following properties are used for bindings between the View and View-Model and each will trigger INotifyPropertyChanged.PropertyChanged
events:
Progress
Task
IsAnalyzing
EDIT:
In follow up Stephen Cleary and Filip Cordas, I've attempted using Task.Run()
with and without ObservableProgress
.
I've simplified the background task to iterate through numbers instead of lines of a file.
private void DoWork(IProgress<ProgressState> progress)
{
IsAnalyzing = true;
progress.Report(new ProgressState(0, "Processing..."));
for (int i = 0; i < 2000000000; i += 1000000)
{
int percent = (int)(((double)i / 2000000000) * 100.0d);
progress.Report(new ProgressState(percent, String.Format("Processing ({0}%)", percent)));
Thread.Sleep(5);
}
progress.Report(new ProgressState(100, "Done."));
Thread.Sleep(1000);
IsAnalyzing = false;
}
Now, I start the task in either one or two ways (with or without the ObservableProgress
):
public void Start(string memoryFile)
{
this.memoryFile = memoryFile;
/* TODO: Uncomment this section to use ObservableProgress instead.
ObservableProgress.CreateAsync<ProgressState>(progress => System.Threading.Tasks.Task.Run(() => DoWork(progress)))
.Sample(TimeSpan.FromMilliseconds(50))
.ObserveOn(Application.Current.Dispatcher)
.Subscribe(p =>
{
Progress = p.ProgressPercentage;
Task = p.Task;
});*/
// TODO: Comment this section to use ObservableProgress instead.
var progress = new Progress<ProgressState>();
progress.ProgressChanged += (s, p) =>
{
Progress = p.ProgressPercentage;
Task = p.Task;
};
System.Threading.Tasks.Task.Run(() => DoWork(progress));
}
ObservableProgress.cs
public static class ObservableProgress
{
public static IObservable<T> CreateAsync<T>(Func<IProgress<T>, Task> action)
{
return Observable.Create<T>(async obs =>
{
await action(new Progress<T>(obs.OnNext));
obs.OnCompleted();
return Disposable.Empty;
});
}
}
In both scenarios (with or without ObservableProgress
) I find that I still need to throttle the background job by using Thread.Sleep(5)
. Otherwise the UI freezes.
EDIT 2:
I made a small modification to progress reports inside the worker thread:
for (int i = 0; i < 2000000000; i += 10) //Notice this loop iterates a lot more.
{
int percent = (int)(((double)i / 2000000000) * 100.0d);
//Thread.Sleep(5); //NOT Throttling anymore.
if (i % 1000000 == 0)
{
progress.Report(new ProgressState(percent, String.Format("Processing ({0}%)", percent)));
}
}
With this modification, the UI does not lock anymore and changes are propagating properly. Why is this so?