I created a small class
public class MyProgress
{
public string Report1 { get; set; }
public string Report2 { get; set; }
}
Then in a method i want to report in real time each filename:
private void DirSearch(string root, string filesExtension,string textToSearch)
{
List<MyProgress> prog = new List<MyProgress>();
string[] filePaths = Directory.GetFiles(root, filesExtension, SearchOption.AllDirectories);
for (int i = 0; i < filePaths.Length; i++)
{
int var = File.ReadAllText(filePaths[i]).Contains(textToSearch) ? 1 : 0;
if (var == 1)
{
string filename = filePaths[i];
prog.Add(new MyProgress { Report1 = filename });
}
}
backgroundWorker1.ReportProgress(0, prog);
}
Not sure if the ReportProgress should be out the FOR loop or inside after the prog.Add line.
And then in the progresschanged event
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
foreach (p in (e.UserState as List<MyProgress>))
{
Console.WriteLine("Progress for {0} is {1}", p.);
}
}
I'm getting errors in the foreach i'm not sure what type should be the variable 'p'
I tried:
foreach (List<MyProgress> p in (e.UserState as List<MyProgress>))
But getting error:
Error 8 Cannot convert type 'Search_Text_In_Files.Form1.MyProgress' to 'System.Collections.Generic.List'
OP totally changed the question
you don't know what p should be?
hint you are enumerating a List of MyProgress
foreach (var p in (e.UserState as List<MyProgress>))
but you are passing a string
backgroundWorker1.ReportProgress(0, filename);
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Console.WriteLine(e.UserState.ToString());
}
and this is the typical signature of a do work
even if DirSearch is the working method you should be passing BackgroundWorker to it IMHO
and you should pass your results out as .Result - prog
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
// Get the BackgroundWorker that raised this event.
BackgroundWorker worker = sender as BackgroundWorker;
// Assign the result of the computation
// to the Result property of the DoWorkEventArgs
// object. This is will be available to the
// RunWorkerCompleted eventhandler.
e.Result = ComputeFibonacci((int)e.Argument, worker, e);
}
Not sure if the ReportProgress should be out the FOR loop or inside after the prog
You want to report progress from inside your for loop that is doing the work. If you pass a list of progress reports in a single call to ReportProgress, backgroundWorker1_ProgressChanged will be invoked just once at the end of your processing. All of your progress reports could come at once, when all the work is done.
Your current code reports progress as it is being made, and attempts to report it again after everything is done. You pass different data types into the same progress report handler, string and List<MyProgress>.
for (int i = 0; i < filePaths.Length; i++)
{
int var = File.ReadAllText(filePaths[i]).Contains(textToSearch) ? 1 : 0;
if (var == 1)
{
string filename = filePaths[i];
backgroundWorker1.ReportProgress(0, filename);
}
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Console.WriteLine("Progress for {0} is {1}", e.UserState);
}
Related
This question already has answers here:
How do I update the GUI from another thread?
(47 answers)
WinForm Application UI Hangs during Long-Running Operation
(3 answers)
Closed 3 years ago.
My program displays the time in a timer event and a button starts a function that keeps reading the content of a file until it reaches 50 lines.
The test file is created by a different process that once in a while appends some lines to it.
How can I modify the program to avoid blocking the form during
execution ?
The difference compared to WinForm Application UI Hangs during Long-Running Operation is that the functions called have to update some elements of the form.
And I don't want to use Application.DoEvents(),
I have hundreds of lines of Application.DoEvents() in my programs and sometimes they create a mess.
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
void Timer1Tick(object sender, EventArgs e)
{
UpdateTime();
}
void UpdateTime()
{
DateTime dt = DateTime.Now;
textBox1.Text = dt.ToString("hh:mm:ss");
}
void BtnRunClick(object sender, EventArgs e)
{
int nlines = 0;
while(nlines < 50) {
nlines = listBox1.Items.Count;
this.Invoke(new Action(() => ReadLinesFromFile()));
Thread.Sleep(1000);
}
}
void ReadLinesFromFile()
{
string sFile = #"D:\Temp1\testfile.txt";
string[] lines = File.ReadAllLines(sFile);
listBox1.Items.Clear();
foreach(string line in lines) {
listBox1.Items.Add(line);
listBox1.SelectedIndex = listBox1.Items.Count - 1;
}
}
}
Asynchronous approach for IO operation will execute all operations on the same UI thread without blocking it.
private async void BtnRunClick(object sender, EventArgs e)
{
int nlines = 0;
while(nlines < 50) {
nlines = listBox1.Items.Count;
await ReadLinesFromFile();
await Task.Delay(1000);
}
}
private async Task ReadLinesFromFile()
{
var file = #"D:\Temp1\testfile.txt";
string[] lines = await ReadFrom(file);
listBox1.Items.Clear();
foreach(string line in lines) {
listBox1.Items.Add(line);
listBox1.SelectedIndex = listBox1.Items.Count - 1;
}
}
private async Task<string[]> ReadFrom(string file)
{
using (var reader = File.OpenText(file))
{
var content = await reader.ReadToEndAsync();
return content.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
}
}
You need only to invoke the ui updates:
void BtnRunClick(object sender, EventArgs e)
{
new Thread(Run).Start();
}
void Run()
{
int nlines = 0;
while (nlines < 50)
{
nlines = listBox1.Items.Count;
ReadLinesFromFile();
Thread.Sleep(1000);
}
}
void ReadLinesFromFile()
{
string sFile = #"D:\Temp1\testfile.txt";
string[] lines = File.ReadAllLines(sFile);
listBox1.InvokeOnUIThread(() => {
listBox1.Items.Clear();
foreach (string line in lines)
{
listBox1.Items.Add(line);
listBox1.SelectedIndex = listBox1.Items.Count - 1;
}
});
}
Add this class to your project:
public static class ControlExtensions
{
public static void InvokeOnUIThread(this Control control, Action action)
{
if (control.InvokeRequired)
{
control.Invoke(action);
}
else
{
action();
}
}
}
All you just need is Async and Await markers to achieve this.
This you can apply to problems which have a long running operation that continuously updates your UI.
The below snippet continuously updates TextBox.Text in a loop, giving it the appearance of a timer. LongRunningProcess() simulates a time taking action
async Task LongRunningProcess()
{
await Task.Delay(1000);
}
private async void Button_Click_1(object sender, RoutedEventArgs e)
{
for (int i = 0; i < 10; i++)
{
DateTime dt = DateTime.Now;
textBox1.Text = dt.ToString("hh:mm:ss");
await Task.Run(() => LongRunningProcess());
}
}
If you want to know more about Asynchrnous programming in C# you can
refer to below article by Stephen Cleary who is THE Authority in this
field
https://blog.stephencleary.com/2012/02/async-and-await.html
Is it bad practice to write code like this. What I want to accomplish is that a user can press a button on a control. The button starts some kind of analyzing process and for each item done it shows a result to the user.
private IEnumerable<int> AnalyzeItems() {
for(int i = 0; i < 1000; i++) {
Thread.Sleep(500);
yield return i;
}
}
private void PerformTask_Click(object sender, EventArgs e) {
Task.Run(() => {
foreach (var item in AnalyzeItems()) {
ResultLog.Invoke((Action)delegate() { ResultLog.Text += item.ToString(); });
}
});
}
why do not use Backgroundworker?
First setup the backgroundworker properties to:
WorkerReportsProgress = true
WorkerSupportsCancellation = true
This is the code:
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
}
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) {
for (int i = 0; i < 1000; i++) {
Thread.Sleep(500);
if (backgroundWorker1.CancellationPending) {
e.Cancel = true;
break;
}
backgroundWorker1.ReportProgress(i / 10, "step " + i);
}
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) {
label1.Text = e.UserState.ToString();
progressBar1.Value = e.ProgressPercentage;
}
private void button1_Click(object sender, EventArgs e) {
cancelButton.Focus();
button1.Enabled = false;
backgroundWorker1.RunWorkerAsync();
}
private void cancelButton_Click(object sender, EventArgs e) {
backgroundWorker1.CancelAsync();
}
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
button1.Enabled = true;
if (e.Error != null) {
MessageBox.Show(e.Error.Message, "Unexpected error");
}
if (e.Cancelled) {
MessageBox.Show("Process stopped by the user", "Cancelled");
}
label1.Text = "Press start";
progressBar1.Value = progressBar1.Minimum;
}
}
Is your approach bad practice? It depends.
If you don't expect your code inside Task.Run to throw any exceptions and you want to continue doing something else, then your code is ok. However, if you want to capture any possible exceptions and wait for the process to finish without freezing UI, then you might want to consider using async/await.
private async void PerformTask_Click(object sender, EventArgs e) {
try
{
await Task.Run(() => {
foreach (var item in AnalyzeItems()) {
ResultLog.Invoke((Action)delegate() { ResultLog.Text += item.ToString(); });
}
});
}
catch(Exception ex)
{
// handle...
}
}
Alternative approach would be to use IProgress<T>. This allows for easy separation of long running work and updating UI. Please note that you shouldn't call this method too often, because
This will put too much work on UI thread resulting in UI freeze.
If you pass any valuetype to IProgress<T>.Report method, then it gets copied. If you call this too often, you risk running garbage collector very often resulting in even bigger freezes.
All of this means that you should utilize IProgress only for truly long running work.
Now that we have it all out of the way, here is a sample of how you could notify users about progress of analyzed items:
private double _currentProgress;
public double CurrentProgress {
get => _currentProgress;
set
{
_currentProgress = value;
NotifyPropertyChanged();
}
}
private async void PerformTask_Click(object sender, EventArgs e)
{
var progress = new Progress<double>();
progress.ProgressChanged += (sender, p) => CurrentProgress = p;
await Task.Run(() => AnalyzeItems(Enumerable.Range(0, 5000).ToList(), progress));
}
private void AnalyzeItems(List<int> items, IProgress<double> progress)
{
for (int itemIndex = 0; itemIndex < items.Count; itemIndex++)
{
// Very long running CPU work.
// ...
progress.Report((double)itemIndex * 100 / items.Count);
}
}
If AnalyzeItems takes less than 100 ms for individual item, then you don't want to report after every finished item (see why above). You can decide how often you want to update status like this:
private void AnalyzeItems(List<int> items, IProgress<double> progress)
{
var lastReport = DateTime.UtcNow;
for (int itemIndex = 0; itemIndex < items.Count; itemIndex++)
{
// Very long running work.
Thread.Sleep(10);
// Tell the user what the current status is every 500 milliseconds.
if (DateTime.UtcNow - lastReport > TimeSpan.FromMilliseconds(500))
{
progress.Report((double)itemIndex * 100 / items.Count);
lastReport = DateTime.UtcNow;
}
}
}
If you have really a lot of very fast iterations, you may want to consider changing DateTime.Now to something else.
Good morning, I'm trying to write an application that use in his interface a progressbar (in C#, WPF). I have read about the need of perform the UI task in a different thread, using Backgroundworker. I trying to make it work using a lot of information, but nothing happens (the program work fine, but the progressbar only shown at the end of the "hard-work tasks").
I'm civil engineer (not a software one), so I ask if anyone can help me with that.
namespace SAP2000___Quake_Definitions
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly BackgroundWorker bgWoker = new BackgroundWorker();
public MainWindow()
{
InitializeComponent();
this.bgWoker.WorkerReportsProgress = true;
this.bgWoker.WorkerSupportsCancellation = true;
this.bgWoker.DoWork += bgWorker_DoWork;
this.bgWoker.ProgressChanged += bgWorker_ProgressChanged;
this.bgWoker.RunWorkerCompleted += bgWorker_RunWorkerCompleted;
}
private void bgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
this.progBar.Value = e.ProgressPercentage;
}
private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bgWorker = (BackgroundWorker)sender;
Dispatcher.Invoke(new Action(() => DoTheHardWork()));
}
private void processButton_Click(object sender, RoutedEventArgs e)
{
this.bgWoker.RunWorkerAsync();
}
private void DoTheHardWork()
{
switch (this.chckBox2.IsChecked.GetValueOrDefault())
{
case true:
this.bgWoker.ReportProgress(0);
//more hardwork with inputs from WPF
case false:
this.bgWoker.ReportProgress(0);
//more hardwork with inputs from WPF
}
}
}
}
That is not how you should be using a BackgroundWorker. I wrote some example code a few years back. It should get you on the right track:
#region Primenumbers
private void btnPrimStart_Click(object sender, EventArgs e)
{
if (!bgwPrim.IsBusy)
{
//Prepare ProgressBar and Textbox
int temp = (int)nudPrim.Value;
pgbPrim.Maximum = temp;
tbPrim.Text = "";
//Start processing
bgwPrim.RunWorkerAsync(temp);
}
}
private void btnPrimCancel_Click(object sender, EventArgs e)
{
if (bgwPrim.IsBusy)
{
bgwPrim.CancelAsync();
}
}
private void bgwPrim_DoWork(object sender, DoWorkEventArgs e)
{
int highestToCheck = (int)e.Argument;
//Get a reference to the BackgroundWorker running this code
//for Progress Updates and Cancelation checking
BackgroundWorker thisWorker = (BackgroundWorker)sender;
//Create the list that stores the results and is returned by DoWork
List<int> Primes = new List<int>();
//Check all uneven numbers between 1 and whatever the user choose as upper limit
for(int PrimeCandidate=1; PrimeCandidate < highestToCheck; PrimeCandidate+=2)
{
//Report progress
thisWorker.ReportProgress(PrimeCandidate);
bool isNoPrime = false;
//Check if the Cancelation was requested during the last loop
if (thisWorker.CancellationPending)
{
//Tell the Backgroundworker you are canceling and exit the for-loop
e.Cancel = true;
break;
}
//Determin if this is a Prime Number
for (int j = 3; j < PrimeCandidate && !isNoPrime; j += 2)
{
if (PrimeCandidate % j == 0)
isNoPrime = true;
}
if (!isNoPrime)
Primes.Add(PrimeCandidate);
}
//Tell the progress bar you are finished
thisWorker.ReportProgress(highestToCheck);
//Save Return Value
e.Result = Primes.ToArray();
}
private void bgwPrim_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
pgbPrim.Value = e.ProgressPercentage;
}
private void bgwPrim_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
pgbPrim.Value = pgbPrim.Maximum;
this.Refresh();
if (!e.Cancelled && e.Error == null)
{
//Show the Result
int[] Primes = (int[])e.Result;
StringBuilder sbOutput = new StringBuilder();
foreach (int Prim in Primes)
{
sbOutput.Append(Prim.ToString() + Environment.NewLine);
}
tbPrim.Text = sbOutput.ToString();
}
else
{
tbPrim.Text = "Operation canceled by user or Exception";
}
}
#endregion
You have to limit all UI writing work to the Progress Report and Run wokrer compelte Events. Those will be raised in the thread that created the BGW (wich should be the UI thread) automagically.
Note that you can only report progress between distinct steps. I had the advantage that I had to write the loop anyway. But if you have existing code (like most download or disk code), you can usually only report between files.
my mistakes were three:
Trying to use "Dispatcher.Invoke(new Action(() => DoTheHardWork()));" to solve an exception related to my thread (exception caused by point #3).
Avoiding the instantiation: BackgroundWorker bgWorker = (BackgroundWorker)sender (thank you #Christopher).
Writing a code that manipulate a UI-Component inside the DoWork event handle of my Backgroundworker. MSDN says: You must be careful not to manipulate any user-interface objects in your DoWork event handler. Instead, communicate to the user interface through the ProgressChanged and RunWorkerCompleted events. Trying this, the exception occur.
Solving the point #2 and #3, the UI is perfectly responsive respect to the "hardwork" function (runned in background).
In form1 top:
ExtractImages ei = new ExtractImages();
In form1 constructor i start the backgroundworker first time
public Form1()
{
InitializeComponent();
backgroundWorker1.RunWorkerAsync();
}
In dowork event
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
if (backgroundWorker1.CancellationPending == true)
{
e.Cancel = true;
return; // this will fall to the finally and close everything
}
else
{
ei.ProgressChanged += (senders, eee) => backgroundWorker1.ReportProgress(eee.Percentage, eee.StateText);
ei.Init();
}
}
In progresschanged
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
ProgressBar1.Value = e.ProgressPercentage;
label7.Text = e.UserState.ToString();
label8.Text = e.ProgressPercentage + "%";
}
In completed
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error == null) { ProgressBar1.Value = 100; }
else
{
}
}
In the class top
public bool WebProblem = false;
public class ProgressEventArgs : EventArgs
{
public int Percentage { get; set; }
public string StateText { get; set; }
}
public event EventHandler<ProgressEventArgs> ProgressChanged;
public void Init()
{
object obj = null;
int index = 0;
ExtractCountires();
foreach (string cc in countriescodes)
{
// raise event here
ProgressChanged?.Invoke(obj, new ProgressEventArgs { Percentage = 100 * index / countriescodes.Count, StateText = cc });
ExtractDateAndTime("http://www.sat24.com/image2.ashx?region=" + cc);
index += 1;
}
ImagesLinks();
}
Method in class
public void ExtractDateAndTime(string baseAddress)
{
try
{
var wc = new WebClient();
wc.BaseAddress = baseAddress;
HtmlDocument doc = new HtmlDocument();
var temp = wc.DownloadData("/en");
doc.Load(new MemoryStream(temp));
var secTokenScript = doc.DocumentNode.Descendants()
.Where(e =>
String.Compare(e.Name, "script", true) == 0 &&
String.Compare(e.ParentNode.Name, "div", true) == 0 &&
e.InnerText.Length > 0 &&
e.InnerText.Trim().StartsWith("var region")
).FirstOrDefault().InnerText;
var securityToken = secTokenScript;
securityToken = securityToken.Substring(0, securityToken.IndexOf("arrayImageTimes.push"));
securityToken = secTokenScript.Substring(securityToken.Length).Replace("arrayImageTimes.push('", "").Replace("')", "");
var dates = securityToken.Trim().Split(new string[] { ";" }, StringSplitOptions.RemoveEmptyEntries);
var scriptDates = dates.Select(x => new ScriptDate { DateString = x });
foreach (var date in scriptDates)
{
DatesAndTimes.Add(date.DateString);
}
}
catch(WebException wex)
{
WebProblem = true;
}
}
What i want to do is once the WebProblem is true stop the loop in the Init() and stop/cancel the backgroundworker in form1.
Then in the backgroundworker completed event throw a message to a label about the problem. And start a timer and after 30 seconds start the backgroundworker again.
If the exception happen and i will use a break point in the catch and click continue it will continue but i want that once there was a problem stop everything and start over again until it will pass without any problem in the middle.
A lot of really peculiar stuff there. I suggest starting with a BackgroundWorker tutorial. Suggestions about what you have posted:
The form constructor is a really bad place to call .RunWorkerAsync; especially if you want to be able to restart the background process again. If you really want to start your BackgroundWorker immediately, you should call something like this in your Form_Load method:
backgroundWorker1.RunWorkerAsync(new Uri("http://www.sat24.com/image2.ashx"));
Your DoWorkEventHandler is where you should do your background work, check for cancellation, and report results and you aren't doing any of those. You want something like:
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
var result = new List<DateString>();
var baseUri = (Uri)e.Argument;
var countryCodes = ExtractCountries();
foreach (var cc in countryCodes)
{
if (backgroundWorker1.CancellationPending)
{
e.Cancel = true;
return;
}
result.Add(ExtractDateAndTime(new Uri(baseUri, "?region=" + cc));
}
e.Result = result;
}
You want ExtractCountries to return an IEnumerable of whatever data you want to enumaerate, and ExtractDateAndTime to accept a complete Uri and return a list of the results. Do not embed that logic in the methods or use globals to return the results; everything you need is already a part of BackgroundWorker, do not create a new class just to hand off the data. Your ExtractDateAndTime declaration should look like this:
private DateString ExtractDateAndTime(Uri source)
Do not catch exceptions in ExtractDateAndTime; doing that will discard the exception and continue as if nothing was wrong, then return the wrong answer without warning. If you want to display the exception, let it propagate to the RunWorkerCompleted event and inspect RunWorkerCompletedEventArgs.Error.
If you want to restart the BackgroundWorker if you get an error you want to declare a Form.Timer in your Design view or Form constructor and a Tick EventHandler something like:
private void timer1_Tick(object sender, EventArgs e)
{
timer1.Stop();
backgroundWorker1.RunWorkerAsync(new Uri("http://www.sat24.com/image2.ashx"));
}
In your RunWorkerCompletedEventHandler do something like:
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
AlertUserSomehow(e.Error);
timer1.Interval = 30000;
timer1.Start();
return;
}
DoSomethingWith((List<DateString>)e.Result);
}
I'm a bit new to using backgroundWorker and been doing some research online on how to use it. From the examples I'm getting there is something that doesn't make sense. In your DoWork function you run a loop from 0-100 or 1-10 and that loop basically tells your progress bar what your progress is, and inside that loop you do all your hard work. Well what if you have some job to do that is a loop, for example looping through a list and printing the values to a file?
Here's code that I've been playing with.
void m_oWorkder_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
taskLabel.Text = "Task Cancelled";
}
else if (e.Error != null)
{
taskLabel.Text = "Error while performing background operation.";
}
else
{
taskLabel.Text = "Task Completed...";
}
startButton.Enabled = true;
cancelButton.Enabled = false;
}
void m_oWorkder_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
taskLabel.Text = "Processing ... " + progressBar1.Value.ToString() + "%";
}
void m_oWorkder_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 100; i++)
{
someRandom();
m_oWorker.ReportProgress(i);
if (m_oWorker.CancellationPending)
{
e.Cancel = true;
m_oWorker.ReportProgress(0);
return;
}
}
m_oWorker.ReportProgress(100);
}
private void startButton_Click(object sender, EventArgs e)
{
startButton.Enabled = false;
cancelButton.Enabled = true;
m_oWorker.RunWorkerAsync();
}
private void cancelButton_Click(object sender, EventArgs e)
{
if (m_oWorker.IsBusy)
{
m_oWorker.CancelAsync();
}
}
private void someRandom()
{
int x = 0;
while (x != 100)
{
x++;
Console.WriteLine("Value of x = " + x);
}
}
There are different ways how to deal with this.
I prefer using the DoWorkEventArgs.Argument. So basically pass your list of fileNames to the BackGroundWorker:
void m_oWorkder_DoWork(object sender, DoWorkEventArgs e)
{
var fileNames = (list<string>) e.Argument;
foreach (var fileName in fileNames)
{
//write some stuff, do whatever you wish
m_oWorker.ReportProgress(fileNames.IndexOf(fileName));
}
}
In your DoWork function you run a loop from 0-100 or 1-10 and that loop basically tells your progress bar what your progress is, and inside that loop you do all your hard work.
This is a common pattern, but it's not compulsory.
You don't have to have a loop inside your DoWork method. You can do whatever you want, reporting progress whenever you want, and your ProgressChanged handler can interpret your progress reports however it wants, not necessarily as a percentage.