1

I have a C# program that is supposed to run a C++ exe file with several different inputs in form of .xlxs files. It looks like this:

enter image description here

When the Form, frmRun, is opened it creates a background worker and then calls myWorker_DoWork.

private List<RunSettings> inputfiles = new List<RunSettings>(); //this is just a struct with getter and setters for all of the values that the constructor for frmRun takes

public frmRun(bool SaveCsvFiles, bool DeleteCsvFiles, bool OpenOutputFile, string[] files) //this is called from a main form in the real program
{
    InitializeComponent();
    Show();
    Refresh();
    foreach (string file in files) //for each one of the files in the list
    {
        inputfiles.Add(new RunSettings(file, SaveCsvFiles, DeleteCsvFiles, OpenOutputFile));
    }

    //https://www.codeproject.com/Articles/841751/MultiThreading-Using-a-Background-Worker-Csharp
    //here is the background handler stuff
    myWorker.DoWork += new DoWorkEventHandler(myWorker_DoWork); //Event handler, which will be called when the background worker is instructed to begin its asynchronous work. It is here inside this event where we do our lengthy operations, for example, call a remote server, query a database, process a file... This event is calle
    myWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(myWorker_RunWorkerCompleted); //Event handler, which occurs when the background worker has finished execution has been canceled or has raised an exception.This event is called on the main thread, which means that we can access the user controls from inside this method.
    myWorker.ProgressChanged += new ProgressChangedEventHandler(myWorker_ProgressChanged); //Event handler which occurs when the ReportProgress method of the background worker has been called. We use this method to write the progress to the user interface. This event is called on the main thread, which means that we can access the user controls from inside this method.
    myWorker.WorkerReportsProgress = true; //Needed to instruct the worker that it can report progress to the main thread.
    myWorker.WorkerSupportsCancellation = true; //Needed to instruct the worker that it can be canceled upon the user request.
    myWorker.RunWorkerAsync(files);//Call the background worker
}

In myWorker_DoWork I then want to process all of the operations needed for each one of the files that is the in the inputfiles List:

protected void myWorker_DoWork(object sender, DoWorkEventArgs e) //starting the program for a list of files
{
    BackgroundWorker sendingWorker = (BackgroundWorker)sender;//Capture the BackgroundWorker that fired the event
    var tasks = new List<Task>();

    for (int i = 0; i < inputfiles.Count; i++) //for each one of the files in the list
    {
        if (!sendingWorker.CancellationPending) //At each iteration of the loop, check if there is a cancellation request pending 
        {
            sendingWorker.ReportProgress(0, "Starting the Program for " + Path.GetFileNameWithoutExtension(inputfiles[i].filename));
            RunMyProgram RunAndSummarize = new RunMyProgram ();
            RunAndSummarize.Progress += ProgressUpdate; //https://stackoverflow.com/questions/14871238/report-progress-backgroundworker-from-different-class-c-sharp
            var task = Task.Run(() => RunAndSummarize.Run(inputfiles[i]));
            tasks.Add(task);

            //RunAndSummarize.Run(inputfiles[i]);
        }
        else
        {
            e.Cancel = true;//If a cancellation request is pending, assign this flag a value of true
            break;// If a cancellation request is pending, break to exit the loop

            //this needs to be reworked a bit if we are gonna use it... We have to call it somewhere and stuff
        }
    }

    Task.WaitAll(tasks.ToArray());

    Close();
}

In my RunProgram Class I do not do much for now except for converting the .xlsx files to csv files:

public void Run(RunSettings settings)
{ 
    ExcelFile InputFile = new ExcelFile(); //a class that handels the Excel related stuff in the program
    string fileNameWithoutExt = Path.GetFileNameWithoutExtension(settings.filename);
    string DirectoryofInputFile = Path.GetDirectoryName(settings.filename);
    string DirectoryofOutputFile = DirectoryofInputFile + @"\" + fileNameWithoutExt;

    //the output file is be created in a subfolder with the same name as the input file
    OutputFolder MakeOutputFolderAndCopyInputFile = new OutputFolder();
    MakeOutputFolderAndCopyInputFile.MakeOuputDirectory(DirectoryofOutputFile); //created in the same folder as the input
    MakeOutputFolderAndCopyInputFile.CopyInputFile(settings.filename, DirectoryofOutputFile, " output");

    //saves the excel file to csv
    Progress(0, "Converting the inputfile for " + fileNameWithoutExt + "..."); //https://stackoverflow.com/questions/10775367/cross-thread-operation-not-valid-control-textbox1-accessed-from-a-thread-othe
    InputFile.OpenExcel(DirectoryofInputFile, fileNameWithoutExt + ".xlsx");
    InputFile.SetActiveSheet(0);
    InputFile.SaveAsCsv(DirectoryofOutputFile, fileNameWithoutExt);
    InputFile.Close();
    InputFile = null;
}

My problem is that when I try to use multi treading like above, I get an error like this:

system argument out of range exception: Index was out of range. Must be non-negative and less than the size of the collection. Parameter name: index.

The code works just fine when I delete the multi tread stuff in myWorker_DoWork and just convert the files one by one instead or when I just skip the background workerand hide the progress for the user. Do you know what I have done wrong? Can't I combine a background worker with multi thread in C#?

Edit: The progress update class.

protected void ProgressUpdate(int progress, string text) //tells the user what is happening
{
    base.Invoke((Action)delegate
    {
        string time = DateTime.Now.ToString("HH:mm:ss");

        lblStatus.Text = text + "...";

        string newstatustext = time + ": " + text;
        if (txtProgress.Lines.Length > 0) //dont add a new line for the first line in the textbox, otherwise do
        {
            newstatustext = Environment.NewLine + newstatustext;
        }
        txtProgress.Text += newstatustext;

        txtProgress.Refresh();
        prgProgress.Value += progress / inputfiles.Count; //we give the status for one file but we want the precentage for all of the files
        prgProgress.Refresh();
    });
}
KGB91
  • 630
  • 2
  • 6
  • 24
  • 1
    Why do you mix `BackgroundWorker` and TPL? Also, `Task.WaitAll` is blocking call – Pavel Anikhouski Jul 30 '20 at 09:47
  • I want to have one form to which all of the class objects of `RunMyProgram` reports to, even if I want to run each of the class objects in parallel. When all of the class objects have done their job I want to report that to the user and then close the form. – KGB91 Jul 30 '20 at 09:51
  • @KGB91 - You can't access any UI element from a background thread. You need to `Invoke` on to the UI thread. – Enigmativity Jul 30 '20 at 10:38
  • How do you mean? It seems to print the progress just fine in the txtbox. But at the end of the program it tells me something went wrong. I have updated my post with `ProgressUpdate`. – KGB91 Jul 30 '20 at 10:56
  • 1
    @KGB91 you don't need BGW, it's obsolete and completely replaced by `Task.Run()` for background operations and the `Progress` class for progress reporting. That class was added in 2002 as an invisible Windows Forms component. – Panagiotis Kanavos Jul 30 '20 at 11:00
  • If I dont have it I seem not to be able to update my main UI, which I want. Or am I wrong here? – KGB91 Jul 30 '20 at 11:01
  • 1
    Yes - the UI can only be modified from the UI thread. The `ProgressUpdate` event will have to run on the UI thread. If the BGW is created on the UI thread, that's what will happen and `Invoke` isn't necessary. Your code though is *very* convoluted and misuses BGW. The `DoWork` method doesn't really do anything, it fires off other tasks with Task.Run. You could get rid of BGW *completely*, use `await` to await those tasks to complete and simply update the UI after `await` – Panagiotis Kanavos Jul 30 '20 at 11:07
  • 1
    You could replace all this code with a simple `await Task.WhenAll()` after which you could update the UI directly. – Panagiotis Kanavos Jul 30 '20 at 11:10
  • Thanks. I tried that. The problem is just that I want to update the UI from the class I am calling, and I cant get that working without the BGW. Do you have any tutorial or something I could check regarding this matter? – KGB91 Jul 30 '20 at 11:27

1 Answers1

2

The issue with your code is that you're using i inside the Task.Run but by the time that the the code in the Task.Run has executed the loop has finished and the value of i has changed.

By capturing i using the variable j you can avoid this issue.

for (int i = 0; i < inputfiles.Count; i++) //for each one of the files in the list
{
    if (!sendingWorker.CancellationPending) //At each iteration of the loop, check if there is a cancellation request pending 
    {
        sendingWorker.ReportProgress(0, "Starting the Program for " + Path.GetFileNameWithoutExtension(inputfiles[i].filename));
        RunMyProgram RunAndSummarize = new RunMyProgram();
        RunAndSummarize.Progress += ProgressUpdate; //https://stackoverflow.com/questions/14871238/report-progress-backgroundworker-from-different-class-c-sharp
        var j = i;
        var task = Task.Run(() => RunAndSummarize.Run(inputfiles[j]));
        tasks.Add(task);

        //RunAndSummarize.Run(inputfiles[i]);
    }
    else
    {
        e.Cancel = true;//If a cancellation request is pending, assign this flag a value of true
        break;// If a cancellation request is pending, break to exit the loop

        //this needs to be reworked a bit if we are gonna use it... We have to call it somewhere and stuff
    }
}
Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • It seems to work! I get an error at the end of the process "Cross-operation not valid: control frmRun accessed from a thread other than the thread it was created on" - but it seems to copy and saving the files. – KGB91 Jul 30 '20 at 10:23
  • 1
    @Sinatr - I just wanted the OP to confirm first. – Enigmativity Jul 30 '20 at 10:37