1

I used to be weary of using BackgroundWorker because it required so many functions to work correctly. However when I swapped to C# from VB.NET (about a month ago) I stumbled across a really easy way to instance them;

Example;

private void cmdMaxCompressPNG_Click(object sender, EventArgs e) {
    pbStatus.Maximum = lstFiles.Items.Count;

    List<string> FileList = Load_Listbox_Data();

    var bw = new BackgroundWorker();
    bw.WorkerReportsProgress = true;
    bw.DoWork += delegate {
        foreach (string FileName in FileList) {
            ShellandWait("optipng.exe", String.Format("\"{0}\"", FileName));
            bw.ReportProgress(1);
        }
    };
    bw.ProgressChanged += (object s, ProgressChangedEventArgs ex) => {
        pbStatus.Value += 1;
    };
    bw.RunWorkerCompleted += delegate {
        lstFiles.Items.Clear();
        pbStatus.Value = 0;
        MessageBox.Show(text: "Task Complete", caption: "Status Update");
    };
    bw.RunWorkerAsync();            
}

There it is, all in one function! Simple to write, easy to understand, and no real leg work. I even made a Snippet out of it. I've since converted all my multiple part BackgroundWorker functions, into this little piece of elegant code. I've also started using them more liberally than in the past. Yesterday I was reading an article regarding Async and Await and how that's apparently how I should be doing things. I'm having trouble wrapping my head around it.

I've tried to use local functions, but I can't get the wording correct. It keeps trying to put it as synchronous.

How would I convert the above into an equally tight implementation of Await/Async logic?

[Edit]

ShellandWait;

private void ShellandWait(string ProcessPath, string Arguments, bool boolWait = true) {
    System.Diagnostics.Process ShellProcess = new System.Diagnostics.Process();
    ShellProcess.StartInfo.FileName = ProcessPath;
    ShellProcess.StartInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
    ShellProcess.StartInfo.Arguments = Arguments;
    ShellProcess.StartInfo.CreateNoWindow = true;
    ShellProcess.StartInfo.UseShellExecute = false;
    ShellProcess.StartInfo.RedirectStandardOutput = true;
    ShellProcess.Start();
    if (boolWait) { ShellProcess.WaitForExit(); }
    if (boolWait) { ShellProcess.Close(); }
}
Kayot
  • 582
  • 2
  • 20
  • Check this answer: https://stackoverflow.com/questions/12414601/async-await-vs-backgroundworker – Ovi Jun 27 '18 at 17:55
  • I have used Tasks , Functions and Actions to achieve it. Take a look at this. https://learn.microsoft.com/en-us/previous-versions/msp-n-p/ff963551(v=pandp.10) – Viju Jun 27 '18 at 17:58
  • What does `ShellandWait` do? Since you run external processes why *block* waiting for every single one of them? – Panagiotis Kanavos Jun 28 '18 at 11:34
  • You can do same thing in `vb.net` as well... – Fabio Jun 28 '18 at 11:40
  • I'm running ShellandWait synchronously outside of the UI thread. The processes I typically run are long running and CPU intensive (think 7zip). Running them all at once would quickly bring the system down since I run hundreds to thousands of processes at a time. VB.NET can do everything that C# can do. Most documentation just happens to be for C# so I'm making the jump for ease of documentation. – Kayot Jun 28 '18 at 16:32

3 Answers3

2

The original code processes only one file at a time so you could use a simple loop and only execute ShellandAwait asynchronously:

private void cmdMaxCompressPNG_Click(object sender, EventArgs e) 
{
    pbStatus.Maximum = lstFiles.Items.Count;

    var FileList = Load_Listbox_Data();

    foreach (var FileName in FileList) 
    {
        //Only thing that needs to run in the background
        await Task.Run(()=>ShellandWait("optipng.exe", String.Format("\"{0}\"", FileName));
        //Back in the UI
        pbStatus.Value += 1;
    }
};
lstFiles.Items.Clear();
pbStatus.Value = 0;
MessageBox.Show(text: "Task Complete", caption: "Status Update");

It would be even better if ShellandWait was modified so it *doesn't block. I assume it uses Process.WaitForExit() to block. The method should await asynchronously instead by listening to the Exited event. Such events can be converted to tasks as shown in Tasks and the Event-based Asynchronous Pattern.

The method would look something like this :

Task<string> ShellAsync(string commandPath,string argument)
{
    var tcs = new TaskCompletionSource<string>();
    var process = new Process();
    //Configure the process
    //...
    process.EnableRaisingEvents = true;
    process.Exited += (s,e) => tcs.TrySetResult(argument);
    process.Start();

    return tcs.Task;
}

This would allow the loop to be simplified to :

foreach (var FileName in FileList) 
{
    await ShellAsync("optipng.exe", String.Format("\"{0}\"", FileName));
    //Back in the UI
    pbStatus.Value += 1;
}
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • This was actually pretty straight forward. I've already converted my long running functions to use this. I can still use "await Task.Run(()=>" in edge cases as well. – Kayot Jun 28 '18 at 17:15
0

The way I went about this (after reading some more) is to use Task.Run()

private async void cmdMaxCompressPNG_Click(object sender, EventArgs e) {
    pbStatus.Maximum = lstFiles.Items.Count;
    List<string> FileList = Load_Listbox_Data();
    await Task.Run(() => {
        foreach (string FileName in FileList) {
            ShellandWait("optipng.exe", String.Format("\"{0}\"", FileName));
            pbStatus.GetCurrentParent().Invoke(new MethodInvoker(delegate { pbStatus.Value += 1; }));
        }         
    });
    lstFiles.Items.Clear();
    pbStatus.Value = 0;
    MessageBox.Show(text: "Task Complete", caption: "Status Update");
}

Note the async next to private.

I had to get fancy with the progress bar since it was a status strip progress bar. If it would have been a standard control I could have used;

pbStatus.Invoke((Action)(() => pbStatus.Value += 1))

Answer to progress bar found at -> Update progress bar from Task.Run async

And here -> How to Invoke the progress bar in Status strip?

Kayot
  • 582
  • 2
  • 20
  • That's not a good solution and far too verbose - `.Invoke()` will run on the UI thread so why put that inside the task at all? If you want to run external processes why use this code at all? Those processes will run in parallel by default. `ShellandWait` should be modified so it *doesn't* block. As for asynchronous progress reporting there's already a class that does this, `Progress`. Check [Reporting Progress Asynchronously](https://blog.stephencleary.com/2012/02/reporting-progress-from-async-tasks.html) for a good description – Panagiotis Kanavos Jun 28 '18 at 11:33
  • 1
    BTW the answer to `Update progress bar from Task.Run async` is simply wrong and can *easily* lead to deadlock, if something else occupies the UI thread. The correct and most voted answer uses Progress. Before tasks, people used BeginInvoke, not Invoke to avoid blocking and waiting if the UI thread was busy – Panagiotis Kanavos Jun 28 '18 at 11:59
0

I'd consider doing this with Microsoft's Reactive Framework. I think it's much more powerful than using Tasks.

private void cmdMaxCompressPNG_Click(object sender, EventArgs e)
{
    pbStatus.Maximum = lstFiles.Items.Count;

    var query =
        from FileName in Load_Listbox_Data().ToObservable()
        from u in Observable.Start(() => 
            System.Diagnostics.Process
                .Start("optipng.exe", String.Format("\"{0}\"", FileName))
                .WaitForExit())
        select u;

    query
        .ObserveOn(this) //marshall back to UI thread
        .Subscribe(
            x => pbStatus.Value += 1,
            () =>
            {
                lstFiles.Items.Clear();
                pbStatus.Value = 0;
                MessageBox.Show(text: "Task Complete", caption: "Status Update");
            });
}

Just NuGet "System.Reactive.Windows.Forms" and add using System.Reactive.Linq; to get it working.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172