#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.