thomasb
thomasb

Reputation: 6037

Make a BackgroundWorker do several operations sequentially without freezing the form

I have already asked a somwhat similar question here but I now have a follow-up question.

I need to launch the external program several times in a row, but I have several problems with it :

Here is the code (sorry, it's kinda long) :

private struct svnCommand
{
    public svnCommand(string args, string path, int pourcent)
    {
        this.args = args;
        this.path = path;
        this.pourcent = pourcent;
    }
    public string args;
    public string path;
    public int pourcent;
}

private BackgroundWorker bgwkSVN;

public Merger()
{
    InitializeComponent();
    InitializeBackgroundWorker();
    this.textBoxCheminRacine.Text = cheminRacine;
}

private void MergerRevisions(object sender, EventArgs e)
{

    activerControles(false);

    textBoxOutput.Text = "";
    cheminRacine = textBoxCheminRacine.Text;
    if (!cheminRacine.EndsWith("\\")) { cheminRacine = cheminRacine + "\\"; }

    string branchToMerge = this.textBoxBranche.Text;
    if (branchToMerge.StartsWith("/")) { branchToMerge = branchToMerge.Substring(1); }

    // révision(s)
    string revisions = "";
    foreach (string r in textBoxRevision.Text.Split(','))
    {
        int rev;
        if (int.TryParse(r, out rev))
        {
            revisions += string.Format(" -r {0}:{1}", rev - 1, rev);
        }
        else
        {
            revisions += " -r " + r.Replace("-", ":");
        }
    }

    // pourcentage de complétion pour chaque étape
    int stepPourcent = (int)Math.Floor((double)(100 / (3 + Directory.GetDirectories(cheminRacine + "branches").Length)));

    // merge sur le trunk
    while (bgwkSVN.IsBusy) { }
    bgwkSVN.RunWorkerAsync(new svnCommand(string.Format("merge --accept postpone {0} {1}{2} .", revisions, svnbasepath, branchToMerge), cheminRacine + "trunk", stepPourcent));


    // merge sur chaque branche
    string[] branches = Directory.GetDirectories(cheminRacine + "branches");
    foreach (string b in branches)
    {
        while (bgwkSVN.IsBusy) { }
        bgwkSVN.RunWorkerAsync(new svnCommand(string.Format("merge --accept postpone {0} {1}{2} .", revisions, svnbasepath, branchToMerge), b, stepPourcent));
    }

    // virer les mergeinfo
    while (bgwkSVN.IsBusy) { }
    bgwkSVN.RunWorkerAsync(new svnCommand("pd svn:mergeinfo . -R", cheminRacine, stepPourcent));

    // svn update
    while (bgwkSVN.IsBusy) { }
    bgwkSVN.RunWorkerAsync(new svnCommand("update", cheminRacine, stepPourcent));

    textBoxOutput.Text += Environment.NewLine + "Terminé.";
    MessageBox.Show("Merge terminé.", "Merge terminé", MessageBoxButtons.OK);

    // réactiver les champs et boutons
    activerControles(true);
}

/// <summary>
/// Set up the BackgroundWorker object by attaching event handlers
/// </summary>
private void InitializeBackgroundWorker()
{
    bgwkSVN = new BackgroundWorker();
    bgwkSVN.WorkerReportsProgress = true;
    bgwkSVN.WorkerSupportsCancellation = true;
    bgwkSVN.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
    bgwkSVN.RunWorkerCompleted += new RunWorkerCompletedEventHandler(backgroundWorker1_RunWorkerCompleted);
    bgwkSVN.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
}

/// <summary>
/// Exécuter une commande SVN
/// </summary>
private string SVNcmd(svnCommand s, BackgroundWorker worker, DoWorkEventArgs e)
{
    string o = "";
    o += s.path + Environment.NewLine + s.args + Environment.NewLine;

    if (worker.CancellationPending)
    {
        e.Cancel = true;
    }
    else
    {
        Process p = new Process();
        p.StartInfo.WorkingDirectory = s.path;
        p.StartInfo.FileName = "svn";
        p.StartInfo.Arguments = s.args;
        p.StartInfo.CreateNoWindow = true;
        p.StartInfo.RedirectStandardOutput = true;
        p.StartInfo.UseShellExecute = false;
        p.Start();
        o += p.StandardOutput.ReadToEnd() + Environment.NewLine;
        p.WaitForExit();

        if (s.pourcent > 0)
        {
            worker.ReportProgress(s.pourcent);
        }
    }
    return o;
}


/// <summary>
/// Where the actual, potentially time-consuming work is done.
/// </summary>
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 = SVNcmd((svnCommand)e.Argument, worker, e);
}

/// <summary>
/// Deals with the results of the background operation
/// </summary>
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    // First, handle the case where an exception was thrown.
    if (e.Error != null)
    {
        MessageBox.Show(e.Error.Message);
    }
    else if (e.Cancelled)
    {
        textBoxOutput.Text += Environment.NewLine + "Annulé.";
    }
    else
    {
        textBoxOutput.Text += e.Result.ToString();
    }
}

/// <summary>
/// Updates the progress bar
/// </summary>
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    this.progressBarTraitement.Value += e.ProgressPercentage;
}

Thanks !

Upvotes: 3

Views: 6801

Answers (4)

Oliver
Oliver

Reputation: 45071

So nobugz give you already the right direction, but for completeness here is some sample code:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;

namespace Threading
{
    public partial class FormMain : Form
    {
        private BackgroundWorker _BackgroundWorker;
        private Queue<Func<string>> _Commands;
        private Random _Random;

        public FormMain()
        {
            InitializeComponent();

            _Random = new Random();
            _Commands = new Queue<Func<string>>();
            _BackgroundWorker = new BackgroundWorker();

            _BackgroundWorker.WorkerReportsProgress = true;
            _BackgroundWorker.WorkerSupportsCancellation = true;
            _BackgroundWorker.DoWork += backgroundWorker_DoWork;
            _BackgroundWorker.ProgressChanged += backgroundWorker_ProgressChanged;
            _BackgroundWorker.RunWorkerCompleted += backgroundWorker_RunWorkerCompleted;

            _BackgroundWorker.RunWorkerAsync();
        }

        private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            while (!_BackgroundWorker.CancellationPending)
            {
                if (_Commands.Count > 0)
                {
                    AddMessage("Starting waiting job...");
                    AddMessage(_Commands.Dequeue().Invoke());
                }
                Thread.Sleep(1);
            }
        }

        void backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            progressBar.Value = e.ProgressPercentage;
        }

        private void backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            AddMessage("BackgroundWorker doesn't make any further jobs.");
        }

        private void buttonStart_Click(object sender, EventArgs e)
        {
            _Commands.Enqueue(DoSomething);
            //or maybe with a lambda
            //_Commands.Enqueue(new Func<string>(() =>
            //{
            //    string message;
            //    message = DoSomething();
            //    return message;
            //}));
        }

        private string DoSomething()
        {
            int max = 10;
            for (int i = 1; i <= max; i++)
            {
                Thread.Sleep(_Random.Next(10, 1000));

                if (_BackgroundWorker.CancellationPending)
                {
                    return "Job aborted!";
                }

                AddMessage(String.Format("Currently working on item {0} of {1}", i, max));
                _BackgroundWorker.ReportProgress((i*100)/max);
            }

            return "Job is done.";
        }

        private void AddMessage(string message)
        {
            if (textBoxOutput.InvokeRequired)
            {
                textBoxOutput.BeginInvoke(new Action<string>(AddMessageInternal), message);
            }
            else
            {
                AddMessageInternal(message);
            }
        }

        private void AddMessageInternal(string message)
        {
            textBoxOutput.AppendText(String.Format("{0:G}   {1}{2}", DateTime.Now, message, Environment.NewLine));

            textBoxOutput.SelectionStart = textBoxOutput.Text.Length;
            textBoxOutput.ScrollToCaret();
        }

        private void FormMain_FormClosing(object sender, FormClosingEventArgs e)
        {
            if (_BackgroundWorker.IsBusy)
            {
                _BackgroundWorker.CancelAsync();
                e.Cancel = true;

                AddMessage("Please close only if all jobs are done...");
            }
        }
    }
}

Upvotes: 5

Hans Passant
Hans Passant

Reputation: 941227

The solution is simple: have one BGW execute all of the commands, not just one BGW for each command. You'll need a List<svnCommand> to store the commands so you can easily pass them to RunWorkerAsync(). DoWork() can simply iterate the list with foreach.

Upvotes: 5

tzerb
tzerb

Reputation: 1433

the while (bgwkSVN.IsBusy) { } is waiting in a tight loop and looks like it's causing your delays. I'd split the process into several background threads and start the 'next' one in backgroundWorkerX_RunWorkerCompleted.

Upvotes: 4

Cory Charlton
Cory Charlton

Reputation: 8938

The fact that you have a while (bgwkSVN.IsBusy) { } in your main form thread is why your form stops responding. The background worker is executing it's work on a separate thread but your UI thread is blocked. You should consider starting one RunWorkerAsync() method in the MergerRevisions call and then start the next in the bgwkSVN.RunWorkerCompleted event.

If you're looking for a nasty quick fix that is the wrong way to do it here it is:

Change:

while (bgwkSVN.IsBusy) { }

To:

while (bgwkSVN.IsBusy) 
{
    System.Threading.Thread.Sleep(1000); // Make the current (UI/Form) thread sleep for 1 second
    Application.DoEvents();
}

Upvotes: 2

Related Questions