user17753
user17753

Reputation: 3161

How to properly update the ObservableCollection from another thread?

If I use an ActionBlock for a database call, and need to update the GUI (perhaps an ObservableCollection). Is looping through the result set and using the Dispatcher.BeingInvoke a good solution, or is there a better way?

I wanted to load in one row at a time to the GUI, since even with virtualization enabled it seemed like if I updated the entire observable collection at once the GUI would hang till it could render the whole datagrid.

Some sample code which simulates the situation:

ActionBlock<Func<Task>> _block = new ActionBlock<Func<Task>>(action => action());

        _block.Post(async () =>
            {
                await Task.Delay(1000); // Perhaps Long database read

                for (int i = 0; i < 1000000; i++) // Perhaps looping over database result set
                {
                    await Dispatcher.BeginInvoke( // Need to update GUI
                        new Action(
                            () =>
                            {
                                // Add new object to collection (GUI will update DataGrid one row at a time).
                                MyModel.MyCollection.Add(new MyClass() { MyInt = i });
                            }
                        ), DispatcherPriority.Background
                    );
                }
            });

Upvotes: 0

Views: 601

Answers (2)

Liero
Liero

Reputation: 27338

If you add call Dispather.BeginInvoke inside loop, then you update you UI 100k times. Ideally I would to this:

//do as much work as possible in background thread
var items = new MyClass[100000];
for (int i = 0; i < 1000000; i++) {
   items[i] = new MyClass{ MyInt = i;}
}
Dispatcher.BeginInvoke(new Action(() => //update UI just once
   MyModel.MyCollection = new ObservableCollection(items);
));

if your virtualization really works, it should be no problem.

in order to avoid adding large number in UI thread, you could split it to smaller portions of data:

for (int i = 0; i < 100; i++){
   await Dispatcher.BeginInvokenew Action(() =>
   {
      for (int j = 0; j < 1000; j++) { //add thousand items at once
         MyModel.MyCollection.Add(items[i * 1000 + j])
   });
}

Upvotes: 1

Stephen Cleary
Stephen Cleary

Reputation: 456407

Is looping through the result set and using the Dispatcher.BeingInvoke a good solution, or is there a better way?

There's never a good reason to use Dispatcher.BeginInvoke in a modern application.

In your case, since you're already using TPL Dataflow, you can just change your ActionBlock to a TransformManyBlock, and link it to a separate ActionBlock that executes on the UI thread. Something like:

var _getRowsBlock = new TransformManyBlock<Func<Task<IEnumerable<TRow>>>, TRow>(
    action => action());
var _updateUiBlock = new ActionBlock<TRow>(row =>
{
  MyModel.MyCollection.Add(new MyClass() { MyInt = i });
}, new ExecutionDataflowBlockOptions
{
  TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext(),
});
_getRowsBlock.LinkTo(_updateUiBlock, new DataflowLinkOptions { PropagateCompletion = true });

_block.Post(async () =>
{
  await Task.Delay(1000); // Perhaps Long database read

  return result.Rows; // return the database result set
});

I wanted to load in one row at a time to the GUI, since even with virtualization enabled it seemed like if I updated the entire observable collection at once the GUI would hang till it could render the whole datagrid.

Well, then you're probably looking at the wrong solution. I don't see how adding data one row at a time would help. If the UI is hammered adding 1000000 rows at once, then adding 1000000 rows one at a time would hammer it even more...

You may have to consider a solution where you don't load 1000000 rows into your UI.

Upvotes: 0

Related Questions