Заполнить ListView BackgroundWorker: элементы пользовательского интерфейса не принадлежат потоку

#c# #wpf

#c# #wpf

Вопрос:

Я пытаюсь заполнить ListView наблюдаемой коллекцией, SearchResult содержащей ObservableCollection<Inline> . Моя (упрощенная) структура данных:

 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; }
}
  

Он содержит ObservableCollection<Inline> потому что эти результаты поиска будут отображаться с пользовательским, BindableTextBlock который поддерживает форматированный текст:

 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);
    }
}
  

Однако при заполнении ListView

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

с помощью следующего 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;
    }
}
  

программа завершает работу с

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

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

Проблема в том, что в фоновом режиме создаются рабочие элементы пользовательского интерфейса ( Inline ), которые не принадлежат потоку пользовательского интерфейса. При назначении ItemsSource после завершения рабочего процесса генерируется исключение.

Похожие вопросы, похоже, задавали много, но я ничего не смог найти для моего конкретного случая.

Любая помощь приветствуется!

Комментарии:

1.Хм, не могли бы вы создать ObservableCollection<Inline> свойство в ViewModel и скопировать результаты из потока Backgroundworker в него через Dispatcher? Видишь learn.microsoft.com/en-us/dotnet/api/… BeginInvoke Метод — это то, что вы ищете

2. Поскольку вы работаете в другом потоке, вы не можете использовать этот поток для изменения элементов пользовательского интерфейса в первом потоке. Вместо этого вы должны использовать BeginInvoke , который позволяет вам вызывать метод вашей формы в потоке этой формы, позволяя вам обновлять элементы. Я не знаю WPF, только WinForms, поэтому я не могу дать вам правильный ответ, но это будет решением.

3. Вам следует начать рассматривать возможность использования ViewModel и использовать событие Progress Changed фонового рабочего.

4. Я, честно говоря, пока не знаю, что такое ViewModel. Есть ли какие-нибудь хорошие ресурсы, чтобы научиться использовать его?

5. Я просто поместил searchResults.Add(...) внутрь Dispatcher.Invoke(...) , и это, кажется, работает нормально! До этого я только пытался поместить весь цикл for внутрь Dispatcher.Invoke(...) , из-за чего пользовательский интерфейс зависал. Это элегантное решение или мне все же следует изучить другие возможности?

Ответ №1:

Для взаимодействия с элементами пользовательского интерфейса вы должны использовать «Invoke» или «BeginInvoke» в диспетчере потоков пользовательского интерфейса

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

Мне нравится использовать статический метод:

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

И вы используете его следующим образом:

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

Кстати, вы должны использовать фоновые потоки для длительных операций, таких как получение данных из веб-службы.

Комментарии:

1. Это работает. Однако я поместил только searchResults.Add(...) внутри потока диспетчера, как указано в комментариях выше. Это делает пользовательский интерфейс более ответственным. Это плохая практика?

2. Извините, пожалуйста, посмотрите обновленный ответ, вы получаете ошибку в завершенном событии, а не в методе DoWork.

3. Ваш обновленный ответ не работает. Таким образом, Inline элементы создаются в фоновом рабочем потоке, что приводит к повторному генерированию исключения.

4. Ну, это странно, потому что вы привязываете данные в методе completed любым способом, которым вы, возможно, захотите начать читать о модели представления Movel View для WPF

5. Да, но я привязываю данные, которые принадлежат фоновому рабочему. Я не знаю, будет ли для меня возможен MVVM из-за используемой мной реализации rich text.