0

This is my first post, here it goes... I have a class called Job that connects to multiple tables in a database in the constructor and gets an item from each table. The database is being accessed through SOAP web services. The object is created from a button click. When the class is created in main, a search string and a custom progress bar GUI element with text are passed in.

The progress bar is a custom user control and has an Update() method to update the message text and the progress.

I have tried multiple ways to get the GUI (Progress Bar) to update from the constructor, and found this post (How to use WPF Background Worker) to be very helpful, however the GUI is only updating when the whole method is finished.

I have tried removing the methods from the constructor of the Job class and calling them from the main thread and updating the GUI in between method calls, I have tried using dispatcher to update the GUI and even awaiting them as a task. I also tried updating the values of the Custom Control directly. Still, the GUI only seems to update after the search method is complete.

Is there a better way to go about this? Am I doing too much in the constructor of the Job class? Even before the object is created the GUI doesn't seem to be updating.

private async void AddMasterJob_Click(object sender, RoutedEventArgs e)
    {
        SearchMethod();
    }
//Search method is used in multiple places.
private async void SearchMethod()
    {
        //This does not change the GUI until the whole method is complete. 
        await Task.Run(() => CurrentProgressControl.Visibility = Visibility.Visible);
        await Task.Run(() => CurrentProgressControl.BringIntoView());

        //Checks if record exists during constructor
        var tempJob = new Job(SearchBox.Text, CurrentProgressControl);
        //Only assign _masterJob if search was successful.
        if (tempJob.MasterJob != null)
        {
            //Try updating the GUI fields directly
            await Task.Run(() => CurrentProgressControl.progressBar.Value = 25);
            await Task.Run(() => CurrentProgressControl.label.Content = "Getting SubJobs");
            //Gets the subjobs and creates an array of subjob objects
            tempJob.GetSubJobs();
            //Try updating using the update method built into the custom control
            CurrentProgressControl.Update("Getting Planning Lines and Ledger Entries.", 45);
            //Get the planning lines and ledger entries
            tempJob.GetPlanningLinesandLedgerEntries();
            CurrentProgressControl.Update("Creating the Estimate Line List.", 60);
            //Combines Subjobs, Planning Lines, and Ledger Entries
            //Calls CalculateTable(_tableLines)
            tempJob.CreateEstimateLineList();
            CurrentProgressControl.Update("Separating into Labor and Material.", 75);
            //Populate _laborLines and _materialLines
            tempJob.SeparateTableLines(tempJob.TableLines);
            CurrentProgressControl.Update("Creating SubTotals.", 85);
            //Insert the subtotal Lines
            tempJob.CreateSubtotalLines(tempJob.LaborLines);
            tempJob.CreateSubtotalLines(tempJob.MaterialLines);
            CurrentProgressControl.Update("Calculating Totals.", 95);
            //Calculate the total estimate column and subtotal lines
            tempJob.CalculateTable(tempJob.MaterialLines);
            tempJob.CalculateTable(tempJob.LaborLines);

            //Calculate the totals for the whole Job
            tempJob.CalculateTotals();
            CurrentProgressControl.Update("Completed Successfully.", 100);

            _masterJob = tempJob;
            RaisePropertyChanged("MasterJob");
        }
    }

The GUI updates after the searchMethod is complete, but not before. Some of the constructor methods inside of the search method take a several seconds so I would expect the GUI to update multiple times, but instead just one update that says complete.

Ben Lusk
  • 34
  • 6

1 Answers1

0

Do not update the UI by Task.Run, but instead use it to perform and await the long running operations:

private async void AddMasterJob_Click(object sender, RoutedEventArgs e)
{
    await SearchMethod();
}

private async Task SearchMethod()
{
    CurrentProgressControl.Visibility = Visibility.Visible;
    CurrentProgressControl.BringIntoView();

    var tempJob = new Job(SearchBox.Text, CurrentProgressControl);

    if (tempJob.MasterJob != null)
    {
        CurrentProgressControl.progressBar.Value = 25;
        CurrentProgressControl.label.Content = "Getting SubJobs";

        await Task.Run(() => tempJob.GetSubJobs());

        CurrentProgressControl.Update("Getting Planning Lines and Ledger Entries.", 45);

        await Task.Run(() => tempJob.GetPlanningLinesandLedgerEntries());

        CurrentProgressControl.Update("Creating the Estimate Line List.", 60);

        await Task.Run(() => tempJob.CreateEstimateLineList());

        CurrentProgressControl.Update("Separating into Labor and Material.", 75);

        await Task.Run(() => tempJob.SeparateTableLines(tempJob.TableLines));

        CurrentProgressControl.Update("Creating SubTotals.", 85);

        await Task.Run(() =>
        {
            tempJob.CreateSubtotalLines(tempJob.LaborLines);
            tempJob.CreateSubtotalLines(tempJob.MaterialLines);
        });

        CurrentProgressControl.Update("Calculating Totals.", 95);

        await Task.Run(() =>
        {
            tempJob.CalculateTable(tempJob.MaterialLines);
            tempJob.CalculateTable(tempJob.LaborLines);
            tempJob.CalculateTotals();
        });

        CurrentProgressControl.Update("Completed Successfully.", 100);

        _masterJob = tempJob;
        RaisePropertyChanged("MasterJob");
    }
}

Or use a single Task.Run call and update the UI by its Dispatcher:

private async Task SearchMethod()
{
    CurrentProgressControl.Visibility = Visibility.Visible;
    CurrentProgressControl.BringIntoView();

    var tempJob = new Job(SearchBox.Text, CurrentProgressControl);

    if (tempJob.MasterJob != null)
    {
        CurrentProgressControl.progressBar.Value = 25;
        CurrentProgressControl.label.Content = "Getting SubJobs";

        await Task.Run(() =>
        {
            tempJob.GetSubJobs());

            Dispatcher.Invoke(() => CurrentProgressControl.Update("Getting Planning Lines and Ledger Entries.", 45));

            tempJob.GetPlanningLinesandLedgerEntries();

            Dispatcher.Invoke(() => CurrentProgressControl.Update("Creating the Estimate Line List.", 60));

            tempJob.CreateEstimateLineList();

            Dispatcher.Invoke(() => CurrentProgressControl.Update("Separating into Labor and Material.", 75));

            tempJob.SeparateTableLines(tempJob.TableLines);

            Dispatcher.Invoke(() => CurrentProgressControl.Update("Creating SubTotals.", 85));

            tempJob.CreateSubtotalLines(tempJob.LaborLines);
            tempJob.CreateSubtotalLines(tempJob.MaterialLines);

            Dispatcher.Invoke(() => CurrentProgressControl.Update("Calculating Totals.", 95));

            tempJob.CalculateTable(tempJob.MaterialLines);
            tempJob.CalculateTable(tempJob.LaborLines);
            tempJob.CalculateTotals();
        });

        CurrentProgressControl.Update("Completed Successfully.", 100);

        _masterJob = tempJob;
        RaisePropertyChanged("MasterJob");
    }
}

If possible, make the job methods async:

private async Task SearchMethod()
{
    CurrentProgressControl.Visibility = Visibility.Visible;
    CurrentProgressControl.BringIntoView();

    var tempJob = new Job(SearchBox.Text, CurrentProgressControl);

    if (tempJob.MasterJob != null)
    {
        CurrentProgressControl.progressBar.Value = 25;
        CurrentProgressControl.label.Content = "Getting SubJobs";

        await tempJob.GetSubJobsAsync();

        CurrentProgressControl.Update("Getting Planning Lines and Ledger Entries.", 45);

        await tempJob.GetPlanningLinesandLedgerEntriesAsync();

        CurrentProgressControl.Update("Creating the Estimate Line List.", 60);

        await tempJob.CreateEstimateLineListAsync();

        CurrentProgressControl.Update("Separating into Labor and Material.", 75);

        await tempJob.SeparateTableLinesAsync(tempJob.TableLines);

        CurrentProgressControl.Update("Creating SubTotals.", 85);

        await tempJob.ob.CreateSubtotalLinesAsync(tempJob.LaborLines);
        await tempJob.CreateSubtotalLinesAsync(tempJob.MaterialLines);

        CurrentProgressControl.Update("Calculating Totals.", 95);

        await tempJob.CalculateTableAsync(tempJob.MaterialLines);
        await tempJob.CalculateTableAsync(tempJob.LaborLines);
        await tempJob.CalculateTotalsAsync();

        CurrentProgressControl.Update("Completed Successfully.", 100);

        _masterJob = tempJob;
        RaisePropertyChanged("MasterJob");
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • That almost worked!! It now runs and the GUI is updating with the different parts of the method exactly how I would expect, however now when the method completes I am getting an error: **"System.ArgumentException: 'Must create DependencySource on same Thread as the DependencyObject.'"** The error is thrown at the exit of the AddMasterJob_Click() method. – Ben Lusk Jul 08 '19 at 14:57
  • No idea where that comes from. Does any of the Job methods create DependencyObjects, e.g. UI elements? – Clemens Jul 08 '19 at 15:42
  • The Job class contains lists that are bound in the GUI to a DataGrid. I was reading about the dependency property and I was thinking I might have to implement Freezable. [Freezable Documentation](https://learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/freezable-objects-overview?view=netframework-4.8) Is there another way to go about this? – Ben Lusk Jul 08 '19 at 15:54
  • I have isolated the problem to the line: `RaisePropertyChanged("MasterJob");` Removing that line stops the error, but also prevents the GUI from updating. – Ben Lusk Jul 08 '19 at 16:28
  • You're not accidentially calling it from inside some Task.Run action? – Clemens Jul 08 '19 at 16:34
  • I moved it to the end of the AddMasterJob_Click() method after the await search and replaced the tempJob with _masterJob to remove the copying at the end, and the error is still persisting. – Ben Lusk Jul 08 '19 at 16:59
  • I changed it back to `tempJob` and copied the value to `_masterJob` in the click method, with `tempJob` being a global variable. The error did not happen if I did nothing with `tempJob`, even with `RaisePropertyChanged("MasterJob");`. Once I try to use the value created within the tasks (`tempJob`) thats when the error is occurring. This leads me to believe that it is indeed caused by the binding that needs to happen, and the initialization that needs to occur on the same thread but is occurring on a different thread when using task. Does that make sense? – Ben Lusk Jul 08 '19 at 17:26
  • It ended up being a subclass that the Job class was using that contained a Brush item. The Brush is a Dependency Object and was causing the cross thread errors. My solution was to create a tempJob field for the new object and copy all of its values into _masterJob, and create new Brush Items for the subclass in the main thread. Marked your answer as correct because it solved the question. – Ben Lusk Jul 10 '19 at 20:05