What is the correct way to update the UI in WPF fr

2019-08-06 06:44发布

问题:

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);
    });

回答1:

// 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.



回答2:

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.



回答3:

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.