-
-
Save DanielSWolf/0ab6a96899cc5377bf54 to your computer and use it in GitHub Desktop.
using System; | |
using System.Threading; | |
static class Program { | |
static void Main() { | |
Console.Write("Performing some task... "); | |
using (var progress = new ProgressBar()) { | |
for (int i = 0; i <= 100; i++) { | |
progress.Report((double) i / 100); | |
Thread.Sleep(20); | |
} | |
} | |
Console.WriteLine("Done."); | |
} | |
} |
using System; | |
using System.Text; | |
using System.Threading; | |
/// <summary> | |
/// An ASCII progress bar | |
/// </summary> | |
public class ProgressBar : IDisposable, IProgress<double> { | |
private const int blockCount = 10; | |
private readonly TimeSpan animationInterval = TimeSpan.FromSeconds(1.0 / 8); | |
private const string animation = @"|/-\"; | |
private readonly Timer timer; | |
private double currentProgress = 0; | |
private string currentText = string.Empty; | |
private bool disposed = false; | |
private int animationIndex = 0; | |
public ProgressBar() { | |
timer = new Timer(TimerHandler); | |
// A progress bar is only for temporary display in a console window. | |
// If the console output is redirected to a file, draw nothing. | |
// Otherwise, we'll end up with a lot of garbage in the target file. | |
if (!Console.IsOutputRedirected) { | |
ResetTimer(); | |
} | |
} | |
public void Report(double value) { | |
// Make sure value is in [0..1] range | |
value = Math.Max(0, Math.Min(1, value)); | |
Interlocked.Exchange(ref currentProgress, value); | |
} | |
private void TimerHandler(object state) { | |
lock (timer) { | |
if (disposed) return; | |
int progressBlockCount = (int) (currentProgress * blockCount); | |
int percent = (int) (currentProgress * 100); | |
string text = string.Format("[{0}{1}] {2,3}% {3}", | |
new string('#', progressBlockCount), new string('-', blockCount - progressBlockCount), | |
percent, | |
animation[animationIndex++ % animation.Length]); | |
UpdateText(text); | |
ResetTimer(); | |
} | |
} | |
private void UpdateText(string text) { | |
// Get length of common portion | |
int commonPrefixLength = 0; | |
int commonLength = Math.Min(currentText.Length, text.Length); | |
while (commonPrefixLength < commonLength && text[commonPrefixLength] == currentText[commonPrefixLength]) { | |
commonPrefixLength++; | |
} | |
// Backtrack to the first differing character | |
StringBuilder outputBuilder = new StringBuilder(); | |
outputBuilder.Append('\b', currentText.Length - commonPrefixLength); | |
// Output new suffix | |
outputBuilder.Append(text.Substring(commonPrefixLength)); | |
// If the new text is shorter than the old one: delete overlapping characters | |
int overlapCount = currentText.Length - text.Length; | |
if (overlapCount > 0) { | |
outputBuilder.Append(' ', overlapCount); | |
outputBuilder.Append('\b', overlapCount); | |
} | |
Console.Write(outputBuilder); | |
currentText = text; | |
} | |
private void ResetTimer() { | |
timer.Change(animationInterval, TimeSpan.FromMilliseconds(-1)); | |
} | |
public void Dispose() { | |
lock (timer) { | |
disposed = true; | |
UpdateText(string.Empty); | |
} | |
} | |
} |
Great! Thanks.
Hey,
while the examples being rather nice (great job!:thumbsup:), some async/await
variation may raise up in your mind, sooner or later. Maybe. 😉
Since i did this recently by my own, maybe some questions about IProgress<>
show up for you too. Maybe something like:
What if my progressbar
Console.Write()
, inside theIProgress<>
handler, is executed after themain()
ones,
or even aftermain()
has already finished (in someasync/await
scenario) ?
I struggled a bit with such questions. But Stephen Toub was to the rescue:
- https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps/
- https://devblogs.microsoft.com/pfxteam/await-synchronizationcontext-and-console-apps-part-2/
- https://devblogs.microsoft.com/dotnet/configureawait-faq/
- https://stackoverflow.com/questions/25817703/configureawaitfalse-not-needed-in-console-win-service-apps-right#25817760
- Use your GoogleFu and search for apropriate posts from „Stephen Cleary“ (another great guy with great resources)
So, "if" it ever happens, that you wanna switch to some async/await
version of your code (never say never… 😉), these links may safe you some sanity. At least to me, they were rather helpful. So i thought „maybe post them here“, if someone else lands here (like me), cause of some same related questions or Google queries.
Thanks for you code!!
@mbodm what do you need async for? My guess is that you subscribe the Report method to some event, like DownloadProgressChanged
in WebClient, so it is not blocking any other actions, right?
@Leendert-JanFloor In a typical modern approach, you are using the TAP pattern. You are also using the HttpClient class more often today, than the WebClient
class (but this is not the main topic here).
One of the advantages in a TAP pattern approach is: The user of your library can decide on his own, how and where the progress-handling happens. Example given:
- Is it spooled onto the UI thread ?
- Is it spooled onto a threadpool thread ?
- Etc.
This solves some downsides, your mentioned older callback approach has. Your mentioned approach is called the EAP pattern. Just use your Google Fu techniques, to understand the differences and why EAP is used in the earlier years and TAP is more widely used today.
Besides the differences, the TAP pattern approach is developed together with the uprising of async/await
and fits very well into it. Or better said: The TAP pattern approach exists specifically cause of async/await
driven scenarios. Which are not that rare today.
Good sources, for all of above stuff, are posts containing 1 of these 2 names:
- Stephen Toub
- Stephen Cleary
These 2 guys knew exactly what they are talking about and offer great resources on that topic. The former one is an oustanding developer at MS and has developed most of the .NET threading and async/await
stuff. The latter one has a lot of experience in asynchronous .NET programming and has some great resources.
All that said:
The use of IProgress<>
(and it´s Report()
method) allows your TAP library users, to decide on their own, if the report "callback" is happening on the UI thread or not. Often, in WinForms or WPF scenarios, you want that. Because you can access Form controls (like Button
, CheckBox
, ProgressBar
and so on) solely from the UI thread.
So, if you wanna access your Button
directly in the "callback" report, that report has to run on the UI thread. But often you also not want that. Or you use some approach that risks to block your UI thread and you have to handle with a lot of stuff, to not block the UI thread. There is this good old problem of "i can´t move my application window while (in example) my download progress happens!". The TAP pattern and IProgress<>
offers solutions for exactly that behaviours.
More on that topic just no longer fits this GitHub issue post. 😉 But at least above statements hopefully give you some starting point. I hope it helped at least a bit, to shed some light on that topic.
Have a nice day!
This helped me a lot, thanks for sharing. Cheers.
Wow, this is so nice!
This was much appreciated! Thank you for sharing. :)
Error CS0246: The type or namespace name 'IProgress' could not be found (are you missing a using directive or an assembly reference?)