Reputation: 1104
I want to load 2 datagrids on one wpf screen. The first one will contain shipment header info. The second will contain the details and potentially many records. I want to load the first grid in the background and then display it. It should show up fairly quickly. I want to load the second grid after the first is loaded. That way users can browsing the headers while details are being loaded.
I can make this working using BackgroundWorker, but I'm trying to switch to using Tasks, which I understand is the newer/better way to do this.
Here is the code to chain the tasks together.
Task LoadShipmentsTask = new Task(() =>
{
LoadShipments();
} );
Task LoadShipmentDetailsTask = LoadShipmentsTask.ContinueWith((t) =>
{
LoadShipmentDetails();
} );
LoadShipmentsTask.Start();
Here are the methods called by the tasks:
void LoadShipments()
{
App.Current.Dispatcher.Invoke((Action)delegate
{
ocShipments = Shipments.RetrieveObservableCollection(false);
txtBlockRecordCount.Text = string.Format("Load of shipments has COMPLETED. {0} records. ", ocShipments.Count.ToString());
});
}
void LoadShipmentDetails()
{
App.Current.Dispatcher.Invoke((Action)delegate
{
ocShipmentDetails = Shipments.RetrieveShipmentDetailObservableCollection();
txtBlockShipmentDetailRecordCount.Text = string.Format("Loading of Shipment Details Complete. {0} detail records loaded.", ocShipmentDetails.Count.ToString());
});
}
I had to use the ...Dispatcher.Invoke to get the UI Thread to update the grids.
Both observable collections load fine, but only the first grid is refreshed.
Strangely, this code worked once or twice, but not consistently.
The Datacontext is set once before either of the threads finish.
The Itemsource of the datagrids are set to the observable collections.
I'm guessing I have to notify the UI thread somehow, but not sure what I'm missing.
UPDATE - Working Code:
Task.Factory.StartNew(LoadShipments).ContinueWith(
w =>
{
ocShipments.Clear();
// Load from a temporary collection populated in background...
// Forces the UI to update since CollectionChanged is fired
foreach (Shipment s in ocShipmentsTemp)
{
ocShipments.Add(s);
}
txtBlockRecordCount.Text = string.Format("Load of shipments has COMPLETED. {0} records. ", ocShipments.Count.ToString());
txtBlockShipmentDetailRecordCount.Text = string.Format("Loading Shipment Details.");
LoadShipmentDetails();
ocShipmentDetails.Clear();
// Forces the UI to update since CollectionChanged is fired
foreach (ShipmentDetail sd in ocShipmentDetailsTemp)
{
ocShipmentDetails.Add(sd);
}
txtBlockShipmentDetailRecordCount.Text = string.Format("Loading of Shipment Details Complete. {0} detail records loaded.", ocShipmentDetails.Count.ToString());
}
, token, TaskContinuationOptions.None, scheduler);
See Comments below for explanation of reasons that my code was not work.
Upvotes: 3
Views: 248
Reputation: 649
I think you should use the Tasks as following (to update the Grid on the UI-Thread)
//Get UI-Thread Scheduler
var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
Task.Factory.StartNew<IEnumerable<Shipments>>(() =>
{
return Shipments.RetrieveObservableCollection(false);
}).ContinueWith((shipmentsTask) =>
{
txtBlockRecordCount.Text = string.Format("Load of shipments has COMPLETED. {0} records. ", shipmentsTask.Result.Count.ToString());
}, scheduler);
obviously you could chain that again with your second task. Keep in mind to retrieve the Scheduler BEFORE entering a background task
To understand the solution you have to know that you run tasks in background and need to handle their results on the UI-Thread (or another thread if they need further processing).
In your case, you can only set the data to the Datagrid
in the continuation task not in the background task
Upvotes: 0
Reputation: 5536
Why use Task if you are simply calling the Dispatcher inside, you are going right back to the UI thread and sending 2 messages one after the other. You should use Dispatcher.BeginInvoke and loose the tasks. they are pointless. Try the following:
App.Current.Dispatcher.BeginInvoke((Action)delegate
{
//LoadShipments
ocShipments = Shipments.RetrieveObservableCollection(false);
txtBlockRecordCount.Text = string.Format("Load of shipments has COMPLETED. {0} records. ", ocShipments.Count.ToString());
//LoadShipmentDetails
ocShipmentDetails = Shipments.RetrieveShipmentDetailObservableCollection();
txtBlockShipmentDetailRecordCount.Text = string.Format("Loading of Shipment Details Complete. {0} detail records loaded.", ocShipmentDetails.Count.ToString());
});
UPDATE
BTW, your problem might also occur because you are setting the collection. When using DataBinding you should never do Collection = new Collection
you should always do :
Collection.Clear();
Collection.AddRange(new collection);
Since your Grid is binded to the collection if you are setting the property to a new collection you will loose the binding as your Grid will stay binded to the old collection reference and will not recieve updates.
Upvotes: 0
Reputation: 4895
The recommended way to use tasks is via Factory.
System.Threading.Tasks.Factory.StartNew(() =>
{
// my routine
}).
ContinueWith(()=>
{
// my chained routine
});
Also, if you are using lambda expressions for delegates in first part why do you use (Action)delegate in the other instead of:
App.Current.Dispatcher.Invoke(new Action(() =>
{
ocShipments = Shipments.RetrieveObservableCollection(false);
txtBlockRecordCount.Text = string.Format("Load of shipments has COMPLETED. {0} records. ", ocShipments.Count.ToString());
}));
Everything else looks fine to me.
Upvotes: 1