Связывание обновления WPF в фоновом потоке
У меня есть элемент управления, который привязан к стандарту ObservableCollection
и у меня есть фоновая задача, которая вызывает службу, чтобы получить больше данных.
Затем я хочу обновить свои данные поддержки за моим элементом управления, отображая при этом диалоговое окно "Пожалуйста, подождите", но когда я добавляю новые элементы в коллекцию, поток пользовательского интерфейса блокируется, пока он повторно привязывает и обновляет мои элементы управления.
Могу ли я обойти это так, чтобы мои анимации и прочее продолжали работать в моем диалоге "пожалуйста, подождите"?
Или, по крайней мере, дать пользователю "внешний вид", чтобы он не был заблокирован?
5 ответов
Если я правильно понимаю, вы уже используете BackgroundWorker для извлечения данных, и это просто присвоение этих данных ObservableCollection блокирует пользовательский интерфейс.
Один из способов избежать блокировки пользовательского интерфейса - назначить данные для ObservableCollection меньшими порциями, поставив в очередь несколько методов диспетчера. Между каждым вызовом метода могут обрабатываться события пользовательского интерфейса.
следующее добавило бы по одному элементу за раз, это немного экстремально, но это иллюстрирует концепцию.
void UpdateItems()
{
//retrievedItems is the data you received from the service
foreach(object item in retrievedItems)
Dispatcher.BeginInvoke(DispatcherPriority.Background, new ParameterizedThreadStart(AddItem), item);
}
void AddItem(object item)
{
observableCollection.Add(item);
}
ObservableCollection вызовет события CollectionChanged, которые заставят пользовательский интерфейс перепривязывать данные, измерять, упорядочивать и перерисовывать. Это может занять много времени, если у вас будет много обновлений.
Можно заставить пользователя думать, что пользовательский интерфейс жив, разделив работу на небольшие пакеты. Используйте Dispatcher из потока пользовательского интерфейса (любой элемент управления имеет ссылку на него), чтобы запланировать действия по обновлению коллекции с 10-100 элементами (определите число экспериментально, это просто для поддержки идеи).
Ваш фоновый код может выглядеть так:
void WorkInBackground()
{
var results = new List<object>();
//get results...
// feed UI in packages no more than 100 items
while (results.Count > 0)
{
Application.Current.MainWindow.Dispatcher.BeginInvoke(
new Action<List<object>>(FeedUI),
DispatcherPriority.Background,
results.GetRange(0, Math.Min(results.Count, 100)));
results.RemoveRange(0, Math.Min(results.Count, 100));
}
}
void FeedUI(List<object> items)
{
// items.Count must be small enough to keep UI looks alive
foreach (var item in items)
{
MyCollection.Add(item);
}
}
У меня есть DLL, которая запускает рабочий поток и отправляет события обратно в приложение - отлично работает на окнах форм, переключился на WPF и все перестало работать. Я 4 часа разбиваю голову о кирпичную стену, пытаясь заставить это работать. Но решение, с которым я в итоге справился, благодаря функции безопасности потока пользовательского интерфейса Microsoft, использующей EnableCollectionSynchronization, дает действительно чистую реализацию для решения этой проблемы.
Эта коллекция расширяет ObservableCollection и реализует EnableCollectionSynchronization, делая эти объекты пригодными для использования между WPF, а также фоновыми работниками.
Изменить: документы Microsoft говорят следующее, поэтому я собираюсь предположить, что совместное использование контекста объекта не имеет значения.
Параметр context - это произвольный объект, который можно использовать для информации, известной при включении синхронизации коллекции. Контекст может быть нулевым.
ThreadSafeCollection.cs
using System.Collections.ObjectModel;
using System.Windows.Data;
namespace NSYourApplication
{
/// <summary>
/// This ObservableCollection is thread safe
/// You can update it from any thread and the changes will be safely
/// marshalled to the UI Thread WPF bindings
/// Thanks Microsoft!
/// </summary>
/// <typeparam name="T">Whatever type of collection you want!</typeparam>
public class ThreadSafeCollection<T> : ObservableCollection<T>
{
private static object __threadsafelock = new object();
public ThreadSafeCollection()
{
BindingOperations.EnableCollectionSynchronization(this, __threadsafelock);
}
}
}
Пример WindowViewModelWindowViewModel.cs
namespace NSYourApplication
{
/// <summary>
/// Example View
/// BaseModelView implements "PropertyChanged" to update WPF automagically
/// </summary>
class TestViewModel : BaseModelView
{
public ThreadSafeCollection<string> StringCollection { get; set; }
/// <summary>
/// background thread implemented elsewhere...
/// but it calls this method eventually ;)
/// Depending on the complexity you might want to implement
/// [MethodImpl(MethodImplOptions.Synchronized)]
/// to Synchronize multiple threads to prevent chase-conditions,deadlocks etc
/// </summary>
public void NonUIThreadMethod()
{
// No dispatchers or invokes required here!
StringCollection.Add("Some Text from a background worker");
}
/// <summary>
/// Somewhere in the UIThread code it'll call this method
/// </summary>
public void UIThreadMethod()
{
StringCollection.Add("This text come from UI Thread");
}
/// <summary>
/// Constructor, creates a thread-safe collection
/// </summary>
public TestViewModel()
{
StringCollection = new ThreadSafeCollection<string>();
}
}
}
Использование в списке в окне xaml / элемент управленияMainWindow.xaml
<ListBox x:Name="wpfStringCollection" ItemsSource="{Binding StringCollection,Mode=OneWay}">
</ListBox>
Используйте BackgroundWorker для выполнения этой задачи. обновить obsrvablecollection в методе DoWork
Использовать этот:
Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Render, new Action(UpdateData), value);
private void UpdateData(int value)
{
BindingSourceProperty = value;
}