3

I have a need to append some grid columns to a grid which initially only contains a few columns. Creating the columns is long-running, and I'm trying to use async/await, but am getting the "calling thread cannot access this object because a different thread owns it" exception, so can someone guide me as to the correct way to do this. The exception occurs on the AddRange call. Thanks in advance.

private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    List<GridViewColumn> cols = await NewGridColsAsync();
    viewGridControl.Columns.AddRange(cols);
}

private async Task<List<GridViewColumn>> NewGridColsAsync()
{
    List<GridViewColumn> cols = new List<GridViewColumn>();

    await Task.Run(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            cols.Add(new GridViewColumn());
        }
    });

    return cols;
}
Mike
  • 1,010
  • 1
  • 14
  • 33
  • `Task.Run()` runs the action passed to it on a different thread. It sounds like you're trying to update the control from a different thread. You're never going to be able to do that. It's just not allowed. The best you can do is prepare the data for insertion into the gridview asynchronously, but the actual update of the gridview has to be done on the UI thread. Can't really tell how to help you unless you show us what you're doing in `Task.Run()`. – itsme86 Aug 18 '16 at 22:33
  • 1
    _"can someone guide me as to the correct way to do this"_ -- there are already numerous Q&A on Stack Overflow which do _exactly_ that. If after reviewing all of the available resources you are still having trouble, you need to post a question with a good [mcve] that reliably reproduces whatever problem you're having, and explain _precisely_ what that problem is. What did you try to fix it, why didn't that attempt to fix it work, etc. In the meantime: if you are creating `GridViewColumn` objects outside the UI thread and trying to use them in the UI thread, that could cause the exception. – Peter Duniho Aug 18 '16 at 22:33
  • itsme86: Added minimal code to the Task.Run as an example. – Mike Aug 18 '16 at 22:45
  • Thanks. Alexei nailed it in his answer. – itsme86 Aug 18 '16 at 22:45
  • 1
    @Mike if you google for the exception message you'll find the duplicate questions. They are so many that it's hard to find the *best* duplicate. The rules are there to *prevent* that noise, so *you* can find the actual answer without getting lost. – Panagiotis Kanavos Aug 19 '16 at 09:11
  • 1
    Besides, what's the point of adding *columns* in the background? That isn't expensive. Loading *data* asynchronously makes sense. On the other hand, trying to *render* 100 columns where only a few can be visible at a time is an issue. Loading the data for 100 columns is another problem. If you have performance problems, there are *other* ways to solve them, eg. virtualized scrolling will only load and render the visible cells – Panagiotis Kanavos Aug 19 '16 at 09:13

2 Answers2

4

You are creating GridViewColumn control instances on one thread (created by Task.Run) and trying to add them on other thread (UI thread). Since objects created on different threads you get the exception.

Proper implementation should be similar to following code. Note that controls (instances of GridViewColumn) are created on UI thread with this code and can later be used to add as children of other controls from main UI thread:

private async Task<List<GridViewColumn>> NewGridColsAsync()
{
   IEnumerable<DataForColumn> dataForColumns;
   await Task.Run(() =>
   {
      // collect data for columns, runs on thread-pool (non-UI) thread
      dataForColumns = .... 
   });

    // runs on UI thread 
    return dataForColumns.Select(data => CreateGridViewColumn(data)).ToList();
}
Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
  • Thanks for the response, but the data is not what is needed here, just empty columns added to the grid. The creation of those columns is the process by which I'm trying to somehow background, but since the columns are created on a different thread and then attempted to be appended to a grid on the ui thread, that's the issue I'm trying to get around. – Mike Aug 18 '16 at 22:48
  • 1
    @Mike *why* do you want to add the columns in the background? Are you trying to solve a real performance problem or is this premature optimization? Adding 100 columns should take no time. *Rendering* them may be expensive – Panagiotis Kanavos Aug 19 '16 at 09:15
  • @Mike you could even use `Enumerable.Range(0,100).Select(_=>new GridViewColumn())` to generate all the columns at once. If you have a list of column names, you can use the same LINQ syntax to generate named columns all at once. – Panagiotis Kanavos Aug 19 '16 at 09:22
2

As others have noted, you must create UI objects on a UI thread. Since you have no CPU-bound work to do, there's no need for Task.Run:

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
  List<GridViewColumn> cols = NewGridCols();
  viewGridControl.Columns.AddRange(cols);
}

private List<GridViewColumn> NewGridCols()
{
  List<GridViewColumn> cols = new List<GridViewColumn>();

  for (int i = 0; i < 100; i++)
  {
    cols.Add(new GridViewColumn());
  }

  return cols;
}

If you have so many UI elements that this "can't" go on the UI thread, then you'll need to implement some form of virtualization. The UI thread is fast enough to create way more UI elements than the human brain can process; if you're seeing a noticeable delay, then there's no way that a human brain can process that many elements anyway, and the proper answer is to use virtualization.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810