Stacksatty
Stacksatty

Reputation: 191

Populate ListView with BackgroundWorker: UI elements don't belong to thread

I am trying to populate a ListView with an ObservableCollection of SearchResult containing a ObservableCollection<Inline>. My (simplified) data structure is:

public class SearchResult
{
    public static ObservableCollection<Inline> FormatString(string s)
    {
        ObservableCollection<Inline> inlineList = new ObservableCollection<Inline>
        {
            new Run("a"),
            new Run("b") { FontWeight = FontWeights.Bold },
            new Run("c")
        };
        return inlineList;
    }

    public ObservableCollection<Inline> Formatted { get; set; }

    public string Raw { get; set; }
}

It contains a ObservableCollection<Inline> because these SearchResults will be displayed with a custom BindableTextBlock which supports rich text:

public class BindableTextBlock : TextBlock
{
    public ObservableCollection<Inline> InlineList
    {
        get { return (ObservableCollection<Inline>)GetValue(InlineListProperty); }
        set { SetValue(InlineListProperty, value); }
    }

    public static readonly DependencyProperty InlineListProperty = DependencyProperty.Register("InlineList", typeof(ObservableCollection<Inline>), typeof(BindableTextBlock), new UIPropertyMetadata(null, OnPropertyChanged));

    private static void OnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        BindableTextBlock textBlock = (BindableTextBlock)sender;
        textBlock.Inlines.Clear();
        textBlock.Inlines.AddRange((ObservableCollection<Inline>)e.NewValue);
    }
}

However when populating the ListView

<ListView Name="allSearchResultsListView">
    <ListView.ItemTemplate>
        <DataTemplate>
            <WrapPanel>
                <local:BindableTextBlock InlineList="{Binding Formatted}" />
                <TextBlock Text="{Binding Raw}" />
            </WrapPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

with the following BackgroundWorker

public partial class MainWindow : Window
{
    private readonly BackgroundWorker worker = new BackgroundWorker();
    ObservableCollection<SearchResult> searchResults = new ObservableCollection<SearchResult>();

    public MainWindow()
    {
        InitializeComponent();

        worker.DoWork += worker_DoWork;
        worker.RunWorkerCompleted += worker_RunWorkerCompleted;
        worker.RunWorkerAsync();
    }

    private void worker_DoWork(object sender, DoWorkEventArgs e)
    {
        for (long i = 0; i < 1000; i++)
        {
            searchResults.Add(new SearchResult()
            {
                Formatted = SearchResult.FormatString("a*b*c"),
                Raw = "abc"
            });
        }
    }

    private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        allSearchResultsListView.ItemsSource = searchResults;
    }
}

the program crashes with

Exception thrown: 'System.Windows.Markup.XamlParseException' in PresentationFramework.dll

Inner Exception: The calling thread cannot access this object because a different thread owns it

The problem is that within the background worker UI elements (Inline) get created which don't belong to the UI thread. When assigning the ItemsSource after the worker has completed the exception gets thrown.

Similar questions seem to have been asked a lot but I couldn't find anything for my particular case.

Any help is appreciated!

Upvotes: 0

Views: 163

Answers (1)

The One
The One

Reputation: 4784

To interact with UI elements you have to use "Invoke" or "BeginInvoke" in the UI thread dispatcher

 Application.Current.Dispatcher.Invoke((Action)delegate
       {
           //CHANGE DATA BOUND TO THE UI HERE
       });

I like to use a static method:

public static class Helpers
{
 public static void RunInUIThread(Action method)
   {
       if (Application.Current == null)
       {
           return;
       }
       Application.Current.Dispatcher.BeginInvoke((Action)delegate
       {
           method();
       });
   }
}

And you use it like this:

  private void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  { 
   Helpers.RunInUIThread(()=>allSearchResultsListView.ItemsSource = searchResults);
  }

BTW you should use background threads for long running operations, like getting data from a web service.

Upvotes: 2

Related Questions