Mason Ticehurst
Mason Ticehurst

Reputation: 474

Get values from UI thread from cross thread

I am trying to add threading into my program such that I don't freeze the entire main UI thread while doing "expensive" computational work.

Currently, my program runs an async Task pointing to a function named startWork() when a button is pressed like so:

    async void startParse_Click(object sender, EventArgs e)
    {
        await Task.Run(() => startWork());
    }

Normally for setting values I do the following:

niceButton.BeginInvoke(new MethodInvoker(() =>
{
      niceButton.Text = "new text";
}));

However, for grabbing data from controls and using that data outside of the MethodInvoker, I'm having a bit of trouble. My goal is execute a foreach loop around my listView1.Items, outside of the UI thread.

Here are the contents of startWork():

    void startWork()
    {
        // Naturally I cannot reference my listView1 control because it is in a
        // different thread and is blocked the the "illegal" cross-thread check

        int overallProgress = 0;
        ListView.ListViewItemCollection items = null;

        // This unfortunately doesn't work (MethodInvoker is in a different scope?)
        listView1.BeginInvoke( new MethodInvoker(() => {
            items = listView1.Items;
        }));

        int totalItems = items.Count; // "items" isn't recognized

        foreach (ListViewItem files in items )
        {
            // slowwww work
        }
    }

I have also tried passing the ListView.ListViewItemCollection as an argument to the function with no avail.

Continuing to get Cross-thread operation not valid: accessed from a thread other than the thread it was created on

Note: The target framework is .NET 4.7 -- perhaps there is a better/more efficient method in newer versions of .NET?

I may just lack fundamental understanding of async/tasks, but I presume I am overlooking something important.

Upvotes: 0

Views: 415

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 456637

UI elements, including ListView.ListViewItemCollection and ListViewItem are "thread affine." This means they can only be accessed on the UI thread.

To do background work, you should only pass non-thread-affine objects. E.g., a List<string>, not a ListViewItemCollection or a List<ListViewItem>.

async void startParse_Click(object sender, EventArgs e)
{
  var items = listView1.Items;
  var data = /* extract List<string> from items */
  await Task.Run(() => startWork(data));
}

void startWork(List<string> files)
{
  int overallProgress = 0;
  foreach (var file in files)
  {
    // slowwww work
  }
}

Also, you shouldn't be using BeginInvoke. Use IProgress<T> with Progress<T> instead.

Upvotes: 1

vasily.sib
vasily.sib

Reputation: 4002

You don't have to iterate over items in a worker thread, as switching from one item in collection to another is pretty fast and do not freezes UI. Just move you "expensive" computational work to worker thread:

private async void StartParseButtonClick(object sender, EventArgs e)
{
    // disable button (we are on UI thread)
    var startParseButton = sender as Button;
    startParseButton.Enabled = false;

    try
    {
        // copy just in case if someone will add new item while we iterating over
        var items = listView1.Items.OfType<ListViewItem>().ToList();
        foreach (var item in items)
            await Parse(item); // this will be invoked in worker thread
    }
    finally
    {
        // enable button finally (we are on UI thread)
        startParseButton.Enabled = true;
    }
}

private async Task Parse(ListViewItem item)
{
    // slowwww work (we are on worker thread)
    await Task.Delay(500);
}

Upvotes: 1

Related Questions