0

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!

Stacksatty
  • 191
  • 2
  • 13
  • Hmm, could you create a `ObservableCollection` Property in the ViewModel and copy the results from the Backgroundworker thread to it via Dispatcher? See https://learn.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcher?view=netframework-4.7.2 `BeginInvoke` Method is what you are looking for – Roland Deschain Mar 05 '19 at 14:08
  • 1
    Because you're working on a different thread, you cannot use that thread to modify UI elements on the first thread. Instead you should use `BeginInvoke` which lets you call a method on your form on that form's thread, allowing you to update elements. I don't know WPF, only WinForms, so I can't give you a proper answer, but this will be the solution. – NibblyPig Mar 05 '19 at 14:08
  • 1
    You should start considering using a ViewModel and use the [Progress Changed Event](https://learn.microsoft.com/it-it/dotnet/api/system.componentmodel.backgroundworker.progresschanged?view=netframework-4.7.2) of the background worker. – Daniele Sartori Mar 05 '19 at 14:11
  • I honestly don't know what a ViewModel is yet. Is there any good resources to learn using it? – Stacksatty Mar 05 '19 at 14:15
  • I just put the `searchResults.Add(...)` inside a `Dispatcher.Invoke(...)` and that seems to work fine! Before that I only tried putting the whole for-loop inside the `Dispatcher.Invoke(...)` which made the UI freeze. Is this an elegant solution or should I still look into other possibilities? – Stacksatty Mar 05 '19 at 14:25
  • Possible duplicate of [Change WPF controls from a non-main thread using Dispatcher.Invoke](https://stackoverflow.com/questions/1644079/change-wpf-controls-from-a-non-main-thread-using-dispatcher-invoke) – The One Mar 05 '19 at 14:26
  • @Stacksatty: You can't create UI elements on any other thread than the dispatcher thread. Why are you using a `BackgroundWorker` in the first place instead of populating the collection on the dispatcher thread? – mm8 Mar 05 '19 at 14:29
  • @mm8 Putting the whole for-loop inside the Dispatcher thread blocked the UI while it looped through all search results. I was looking for a way to perform the search in the background without affecting UI responsibility. – Stacksatty Mar 05 '19 at 14:32
  • @Stacksatty: Sure but you are you then creating `Inline` elements on the background thread? You should perform the search on the background thread and update the UI on the dispatcher thread. – mm8 Mar 05 '19 at 14:33
  • @mm8 I'm honestly not entirely sure. I am creating the `Inline` elements on the dispatcher thread which gets called within the background worker. – Stacksatty Mar 05 '19 at 14:43
  • @Stacksatty: So what exactly are you doing, or trying to do, on the background thread? – mm8 Mar 05 '19 at 14:44
  • @mm8 The background worker gets search results via IPC from another process. This part is nearly instant. But looping through those results to add them to my ListView takes time. That's why I want to put it inside a background worker so the UI stays responsive. – Stacksatty Mar 05 '19 at 14:54

1 Answers1

2

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.

The One
  • 4,560
  • 5
  • 36
  • 52
  • This works. However I put only the `searchResults.Add(...)` inside the Dispatcher thread like mentioned in the comments above. This makes the UI more responsible. Is that bad practice? – Stacksatty Mar 05 '19 at 14:34
  • Sorry please see the updated answer, you're getting the error in the completed event, not in the DoWork method. – The One Mar 05 '19 at 14:40
  • Your updated answer does not work. That way the `Inline` elements get created within the background worker thread which causes the exception to be thrown again. – Stacksatty Mar 05 '19 at 14:57
  • Well that's strange because you're binding the data in the completed method, any way you might want to start reading about Movel View View Model for WPF – The One Mar 05 '19 at 15:02
  • Yes, but I'm binding the data that belongs to the background worker. I don't know if MVVM would be possible for me because of the rich text implementation I am using. – Stacksatty Mar 05 '19 at 15:29