What I'm trying to do is to have a progress bar as a separate window that will update based on the progress of other long processing work on another form. When I debug through the progress bar code, DoWork and ProgressChanged are both called as expected but the UI fails to update. The code even reaches the Worker_RunWorkerCompleted and closes the progressbar. I've read about 20 articles and can't nail down the issue. I know that I should be using a view model for WPF but the code below is simplified to see if I can just get the progress bar to update. I know I'm missing something simple but I can't put my finger on what it is.
XAML:
<Window x:Class="CGXAcquisition.Forms.Dialog.FileProgressBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="File Upload Progress" Height="100" Width="300">
<Grid Margin="20">
<ProgressBar Minimum="0" Maximum="100" Name="pbStatus" />
<TextBlock Text="{Binding ElementName=pbStatus, Path=Value, StringFormat={}{0:0}%}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</Window>
The code behind:
public partial class FileProgressBar : Window, INotifyPropertyChanged
{
BackgroundWorker worker = new BackgroundWorker();
private int fileLines = 0;
private int totalFileLines = 0;
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
public int FileLines
{
get { return fileLines; }
set
{
fileLines = value;
Percentage = (int)Math.Round((double)fileLines / TotalFileLines);
OnPropertyChanged("FileLines");
}
}
public int TotalFileLines
{
get { return totalFileLines; }
set { totalFileLines = value; }
}
public int Percentage { get; set; }
public FileProgressBar(int totalLines)
{
InitializeComponent();
TotalFileLines = totalLines;
Loaded = Window_ContentRendered;
}
private void Window_ContentRendered(object sender, EventArgs e)
{
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.DoWork = worker_DoWork;
worker.RunWorkerCompleted = Worker_RunWorkerCompleted;
worker.ProgressChanged = worker_ProgressChanged;
worker.RunWorkerAsync();
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
this.Close();
}
void worker_DoWork(object sender, DoWorkEventArgs e)
{
for(int i = 0; i < 100; i )
{
Thread.Sleep(100);
int percent = i / 100;
worker.ReportProgress(percent, i);
}
}
void worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pbStatus.Value = e.ProgressPercentage;
}
}
The code that creates the progress bar which exists on another form:
private void ShowProgressBar()
{
progress = new FileProgressBar(FileInfo.LineCount);
progress.Show();
}
I call ShowProgressBar() in my onl oaded event
private void onl oaded(object sender, RoutedEventArgs e)
{
ShowProgressBar();
}
and in my constructor I add the event handler to the Loaded event
Loaded = onl oaded;
I expected to see the UI update with the code I have. I've tried using a Dispatcher to update the UI but as far as I understand it, the worker_ProgressChanged should alleviate the need for a dispatcher. I've looked through StackOverflow, MSDN and several other blogs and nothing has helped me identify what I'm missing.
Thank you in advance for your help.
UPDATE I finally noticed that my math was wrong. This line int percent = i / 100; Should be int percent ((int)(double)i / 100 * 100);
I know that I could have just used i but I wanted to do it right.
CodePudding user response:
The Backgroundworker
is a deprecated API. Instead you should use Task.Run
or asynchronous APIs where possible. To report progress you should use the Progress<T>
class.
The main issue with your code: your calculations are wrong. Currently, the max value that your formula produces is 1
(100/100
).
Furthermore, the BackgroundWorker.ReportProgress
accepts an int
as percentage argument. The implicit type cast from double
to int
will truncate the decimal places of the double
value. Hence, 0.9d
becomes 0
.
This means the ProgressBar.Value
will be 0
the whole time until i == 100
. Then the value will be 1
and you close the Window
.
The correct formula to calculate percentage is: value / max_value * 100
.
It is also important to understand that because of the division your participating numeric variables or the divisor must be of type double
. Otherwise the intermediate result value will be truncated due to the implicit conversion from double
to int
. In other words you completely lose the decimal places (no rounding).
You don't tell any details about your background job. But from the window's name I assume you read from a file. In this case don't execute the job on a background thread. Instead use the asynchronous API of the StreamReader
.
Additionally, controls never implement INotifyPropertyChanged
. It has several downsides: performance, property can't serve as binding target, can't be animated etc.
Instead controls, or types that extend DependencyObject
in general, should implement their properties as dependency properties.
Your improved implementation of FileProgressBar
and fixed calculations should look as follows:
FileProgressBar.xaml.cs
public partial class FileProgressBar : Window
{
public int FileLines
{
get => (int)GetValue(FileLinesProperty);
set => SetValue(FileLinesProperty, value);
}
public static readonly DependencyProperty FileLinesProperty = DependencyProperty.Register(
"FileLines",
typeof(int),
typeof(FileProgressBar),
new PropertyMetadata(default(int), OnFileLinesChanegd));
// If this property is used for data binding it must be a dependency property too
public int TotalFileLines { get; set; }
// If this property is used for data binding it must be a dependency property too
public int Percentage { get; set; }
public FileProgressBar(int totalLines)
{
InitializeComponent();
this.TotalFileLines = totalLines;
this.Loaded = Window_ContentRendered;
}
private static void OnFileLinesChanegd(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var this_ = d as FileProgressBar;
this_.Percentage = (int)Math.Round((double)e.NewValue / this_.TotalFileLines * 100d);
}
private async void Window_ContentRendered(object sender, EventArgs e)
{
var progressReporter = new Progress<double>(progress => pbStatus.Value = progress);
await DoWorkAsync(progressReporter);
OnWorkCompleted();
}
private void OnWorkCompleted() => Close();
async Task DoWorkAsync(IProgress<double> progressReporter)
=> await Task.Run(async () => await LongRunningTaskAsync(progressReporter));
private static async Task LongRunningTaskAsync(IProgress<double> progressReporter)
{
for (int i = 0; i < 100; i )
{
await Task.Delay(TimeSpan.FromMilliseconds(100));
double percent = i / 100d * 100d;
progressReporter.Report(percent);
}
}
}