1

I am trying to update the UI from one of my thread dispatched via Task.Factory. I have having hard time properly updating my UI.

Here is the behavior I am observing:

Task.Factory.StartNew(() =>
    {
        // UI does get updated from here.
    }).ContinueWith(task =>
        {
            // UI does *not* get updated from here.
        });

What is the proper way to update the UI within a thread dispatched with Task Factory?

Here is my actual code for your reference:

private string CurrentProcess
{
    set { _eventAggregator.GetEvent<CurrentProcessUpdatedEvent>().Publish(value); }
}

private double ProgressPercentage
{
    set
    {
        _eventAggregator.GetEvent<ProgressPercentageUpdatedEvent>()
                        .Publish(Utilities.GetProgressPercentage(value));
    }
}

TaskScheduler uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
var tasks = new List<Task<DataTable>>();

string siteCollectionUrl;
string connectionString;

try
{
    Dictionary<string, object> session = ApplicationContext.Current.Session;

    try
    {
        if ((double) session["ProgressPercentage"] > 0) return;
    }
    catch
    {
    }

    siteCollectionUrl = (string) session["SiteCollection"];
    connectionString = (string) session["Database"];
}
catch
{
    return;
}

_eventAggregator.GetEvent<IsProcessingChangedEvent>().Publish(true);
CurrentProcess = "Loading resources.";

Task<DataTable> spTask = Task<DataTable>.Factory.StartNew(() =>
    {
        using (ChannelFactory<ISharePointService> service = Utilities.GetSharePointService())
        {
            ISharePointService sharePointService = service.CreateChannel();
            DataTable spDatatable = sharePointService.GetResources(siteCollectionUrl);

            Task.Factory.StartNew(() => { ProgressPercentage = 10; }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);

            return spDatatable;
        }
    });

tasks.Add(spTask);

Task<DataTable> buildTableTask = Task<DataTable>.Factory.ContinueWhenAll(tasks.ToArray(), t =>
    {
        DataTable spDatatable = t[0].Result;

        double percent = 10/spDatatable.Rows.Count;

        var columnMap = new Dictionary<string, string>
            {
                {"IsValid", null},
                {"Reason", null},
                {"SPID", "ID"},
                {"DBID", "EXTID"},
                {"Name", "Title"},
                {"Account", "SharePointAccount"},
                {"Email", "Email"},
                {"Generic", "Generic"},
                {"Department", "Department"},
                {"TempDept", "TempDept"},
                {"Role", "Role"},
                {"TempRole", "TempRole"},
                {"HolidaySchedule", "HolidaySchedule"},
                {"WorkHours", "WorkHours"}
            };

        DataTable spResources = BuildDataTable(columnMap);

        foreach (DataRow dataRow in spDatatable.Rows)
        {
            DataRow row = spResources.NewRow();

            foreach (var pair in columnMap)
            {
                try
                {
                    row[pair.Key] = dataRow[pair.Value];
                }
                catch
                {
                }
            }

            spResources.Rows.Add(row);

            Task.Factory.StartNew(() => { ProgressPercentage = percent; }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
        }

        return spResources;
    });

tasks.Add(buildTableTask);

Task<DataTable> dbTask = Task<DataTable>.Factory.StartNew(() =>
    {
        using (var sqlConnection = new SqlConnection(connectionString))
        {
            using (var sqlCommand = new SqlCommand(SQL, sqlConnection))
            {
                sqlConnection.Open();
                using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
                {
                    var dataTable = new DataTable();
                    dataTable.Load(sqlDataReader);

                    Task.Factory.StartNew(() => { ProgressPercentage = 10; }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);

                    return dataTable;
                }
            }
        }
    });

tasks.Add(dbTask);

Task.Factory.ContinueWhenAll(tasks.ToArray(), t =>
    {
        DatabaseResources = t[2].Result;
        DataTable sharePointResources = t[1].Result;

        if (sharePointResources != null)
        {
            int resourceIndex = 1;
            int totalResources = sharePointResources.Rows.Count;
            double percentPoint = 70/totalResources;

            foreach (DataRow row in sharePointResources.Rows)
            {
                DataRow currentRow = row;

                Task.Factory.StartNew(() =>
                    {
                        CurrentProcess = string.Format("[{0}/{1}] Processing: {2}",
                                                        resourceIndex++, totalResources,
                                                        currentRow["Name"]);
                    }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);

                bool isValid = true;
                var reasons = new List<string>();

                DataRow[] dataRows =
                    _databaseResources.Select(string.Format("ResourceID = {0}", row["DBID"]));
                if (dataRows.Any())
                {
                    DataRow dataRow = dataRows[0];

                    string tempDept = (row["TempDept"] ?? string.Empty).ToString();
                    string dept = (row["Department"] ?? string.Empty).ToString();

                    string tempRole = (row["TempRole"] ?? string.Empty).ToString();
                    string role = (row["Role"] ?? string.Empty).ToString();

                    string hs = (row["HolidaySchedule"] ?? string.Empty).ToString();
                    string dbhs = (dataRow["HolidaySchedule"] ?? string.Empty).ToString();

                    string wh = (row["WorkHours"] ?? string.Empty).ToString();
                    string dbwh = (dataRow["WorkHours"] ?? string.Empty).ToString();

                    if (string.IsNullOrEmpty(dept))
                    {
                        if (!dept.Equals(tempDept))
                        {
                            isValid = false;
                            reasons.Add("Department does not match Temp Dept");
                        }
                    }

                    if (string.IsNullOrEmpty(role))
                    {
                        if (!role.Equals(tempRole))
                        {
                            isValid = false;
                            reasons.Add("Role does not match Temp Role");
                        }
                    }

                    if (string.IsNullOrEmpty(hs))
                    {
                        if (!hs.Equals(dbhs))
                        {
                            isValid = false;
                            reasons.Add("Holiday Schedule does not match Holiday Schedule from database");
                        }
                    }

                    if (string.IsNullOrEmpty(wh))
                    {
                        if (!wh.Equals(dbwh))
                        {
                            isValid = false;
                            reasons.Add("Work Hours does not match Work Hours from database");
                        }
                    }
                }
                else
                {
                    isValid = false;
                    reasons.Add("Resource does not exist in database");
                }

                row["IsValid"] = isValid;
                row["Reason"] = string.Join("\n", reasons.ToArray());

                Task.Factory.StartNew(() => { ProgressPercentage = percentPoint; }, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
            }

            SharePointResources = sharePointResources;
        }

        _eventAggregator.GetEvent<ProgressPercentageUpdatedEvent>()
                        .Publish(Utilities.ResetProgressPercentage());
        _eventAggregator.GetEvent<IsProcessingChangedEvent>().Publish(false);
    });
halfer
  • 19,824
  • 17
  • 99
  • 186
Moon
  • 33,439
  • 20
  • 81
  • 132
  • When you say "UI does *not* get updated from here". Do you mean that that code is never executed, or that the code throws an exception? And what part in the "real" code are you referring to: the last `ContinueWhenAll` part of the code? – Matt Smith Apr 23 '13 at 18:48
  • @MattSmith I am referring to the code withing `ContinueWhenAll`. The code does get executed. But the UI does not get updated with proper status messages until everything inside that code section finishes execution. – Moon Apr 23 '13 at 19:01
  • got it. So, while this ContinueWhenAll code is executing, what is the main threading doing? Is it pumping messages or is it blocked (waiting for something)? Best way to see what is going on: while in the ContinueWhenAll code (but after an update has been sent to the UI), freeze the thread (in the Threads window choose the current thread and RMB->Freeze). Then hit "Run" and see if your update gets processed. If it doesn't, see what the Main thread is currently doing (i.e. break and see where it is waiting). – Matt Smith Apr 23 '13 at 21:43
  • @MattSmith: So, when I add the breakpoint inside of the `CurrentProcess setter` - it does not get hit until after the `ContinueWithAll` code is done executing. – Moon Apr 24 '13 at 00:37
  • yes, I already understand that--and that is a valid ordering since there is a race between the main thread and the background thread, so that test is not very interesting. You need to freeze the background thread just after it starts that ui bound task, and then hit run: does your breakpoint get hit or not. If not, it is most likely due to the main thread being blocked and thus not pumping any windows messages. – Matt Smith Apr 24 '13 at 01:11
  • The code is really hard to read with the formatting changes. – Matt Smith Apr 25 '13 at 22:14
  • @MattSmith: I did not make those formatting changes! But I agree. – Moon Apr 25 '13 at 23:12
  • ah. fyi, you should be able to edit and rollback to the previous revision. – Matt Smith Apr 26 '13 at 02:26
  • @MattSmith, I rolled back my editing. You can always improve formatting or rollback the bad one yourself. Or insert the code in an editor and format the code for your own ease of reading – Gennady Vanin Геннадий Ванин Apr 26 '13 at 05:04
  • I prefer to let the original author dictate formatting/style rather than edit it to suit myself, since it is just personal opinion as to what is "good". – Matt Smith Apr 26 '13 at 05:53

3 Answers3

4

// UI does get updated from here

You should launch a new Action(() => through DispatcherObject in WPF

Task.Factory.StartNew(() =>
    {
        // UI does get updated from here
        this.Dispatcher.BeginInvoke(new Action(() => 
        {

Please search for the last line in "Part 1 - Getting Started" of Alexandra Rusina's series "Parallel Programming in .NET Framework 4"

I am sure you will enjoy all sequel from this ref further on.

Part 2- Task Cancellation demonstrates how to use task scheduler instead:

var ui = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.ContinueWhenAll(tasks.ToArray(),
    result =>
    {
        var time = watch.ElapsedMilliseconds;
        label1.Content += time.ToString();
    }, CancellationToken.None, TaskContinuationOptions.None, ui);

instead of:

Task.Factory.ContinueWhenAll(tasks.ToArray(),
      result =>
      {
          var time = watch.ElapsedMilliseconds;
          this.Dispatcher.BeginInvoke(new Action(() =>
              label1.Content += time.ToString()));
      });  

In response to comments

"First of all, I am using PRISM. So, in my ViewModwl I have to use Dispatcher.Current.BeginInvoke --- I tried that. It did not help"

Please check the answer to "Is WPF Dispatcher the solution of multi threading problems?" related to use of dispatcher and accessing UI in Prism:

// Not a UI component
public class MyDomainService : IMyDomainService
{
   private readonly IDispatcher _dispatcher;

   public MyDomainService(IDispatcher dispatcher) 
   {
      _dispatcher = dispatcher;
   }

   private void GotResultFromBackgroundThread()
   {
       _dispatcher.Dispatch(() => DoStuffOnForegroundThread());
   }
}

"You need to make sure you are invoking on the actual UI Dispatcher, not necessarily the Current"

You can engage the PRISM event aggregator to ensure that you are on the UI thread or basic Dispatcher.CheckAccess Method

If you use TaskScheduler, then you should get TaskScheduler.FromCurrentSynchronizationContext on UI thread (for example, in Window.Loaded event handler, you will get on double clicking your form) and pass/share to/with tasks.

Community
  • 1
  • 1
  • First of all, I am using `PRISM`. So, in my ViewModwl I have to use `Dispatcher.Current.BeginInvoke` --- I tried that. It did not help. – Moon Apr 23 '13 at 13:14
  • @Moon, you tried what which failed: 1) mentioned by you `Dispatcher.Current.BeginInvoke` or mentioned in my answer 2)`Dispatcher.BeginInvoke` and 3)`TaskScheduler.FromCurrentSynchronizationContext();`? – Gennady Vanin Геннадий Ванин Apr 23 '13 at 13:47
  • I updated questing with the latest code. No luck yet. These are the only changes I see on the UI: (1) Status text changes to `Loading resources.` (2) Progress bar indicates the progress about `30%`. Importantly, I do not see any status updates from the last task which processes all resources. – Moon Apr 23 '13 at 16:46
0

Look at this video. DNRTV episode with Stephen Toub. It is focused on the then upcoming .NET 4.5 features, but in the initial recap, it covers the topic of GUI marshalling from tasks using taskschedulers nicely.

Tormod
  • 4,551
  • 2
  • 28
  • 50
0

The easiest way to update the UI is through the SynchronizationContext.Post/Send methods. SynchronizationContext hides the underlying UI library (WPF or WinForms) and ensures that the action you specify executes on the proper thread.

Send blocks until the UI finishes processing the action while Post executes the action asynchronously.

To update your UI in an asynchronous manner you can use code like this:

SyncrhonizationContext.Current.Post(value=>myTextBox.Text=(string)value,"23455");

A similar option is to specify TaskScheduler.FromCurrentSynchronizationContext() in ContinueWith to have your continuation execute in the UI thread:

.ContinueWith(t=>{
    myTextBox.Text="Some Value";
 });

This is equivalent to calling SynchronizationContext.Current.Send

You also mention that you use MVVM and PRISM and that the asynchronous operation executes in the ViewModel. Changes to ViewModel properties will not appear in the view unless the properties or your asynchronous code also raises the NotifyPropertyChanged event. This is a PRISM issue though, not a TPL issue.

I also noticed that you publish property changed events instead of raising the NotifyPropertyChanged event. Data Binding depends on receiving NotifyPropertyChanged from the source properties. Unless you add code to somehow raise the proper events, the view controls will not be updated.

ViewModels in PRISM typically inherit from NotificationObject which implements the INotifyPropertyChanged interface. Try calling the RaisePropertyChanged method to raise the NotifyPropertyChanged event inside your proeprties and see whether this solves your problem.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236