Обновление DataContext из потока backgroud
Я получаю данные для окна wpf в backgroundthread, как это [framework 4.0 с async / await]:
async void refresh()
{
// returns object of type Instances
DataContext = await Task.Factory.StartNew(() => serviceagent.GetInstances());
var instances = DataContext as Instances;
await Task.Factory.StartNew(() => serviceagent.GetGroups(instances));
// * problem here * instances.Groups is filled but UI not updated
}
Когда я включаю действия GetGroups в GetInstances, пользовательский интерфейс показывает группы.
Когда я обновляю в отдельном действии DataContext включает группы correctly, но пользовательский интерфейс не показывает их.
В методе GetGroups() я включил NotifyCollectionChangedAction.Reset для ObservableCollection групп, и это не помогает.
Особенно странно то, что я вызываю NotifyCollectionChangedAction.Reset в списке только один раз, но это исполняется трижды, в то время как в списке есть десять пунктов?!
Я могу решить эту проблему, написав:
DataContext = await Task.Factory.StartNew(() => serviceagent.GetGroups(instances));
Но является ли это обычным способом обновления DataContxt и UI через фоновый процесс?
На самом деле я только хочу обновить существующий DataContext, не устанавливая его снова?
Правка: serviceagent.GetGroups(instances) Подробнее:
public void GetGroups(Instances instances)
{
// web call
instances.Admin = service.GetAdmin();
// set groups for binding in UI
instances.Groups = new ViewModelCollection<Groep>(instances.Admin.Groups);
// this code has no effect
instances.Groups.RaiseCollectionChanged();
}
Здесь ViewModelCollection<T> наследуется от ObservableCollection<T> и я добавил метод:
public void RaiseCollectionChanged()
{
var handler = CollectionChanged;
if (handler != null)
{
Trace.WriteLine("collection changed");
var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
handler(this, e);
}
}
3 ответа:
Есть несколько пунктов, которые выделяются в части
asyncвашего кода:
- я объясняю , почему мы должны избегать
async voidв моей статье MSDN. Таким образом,voidявляется неестественным типом возвращаемого значения для методовasync, поэтому он имеет некоторые особенности, в частности, связанные с обработкой исключений.- мы должны предпочесть
TaskEx.RunвместоStartNewдля асинхронных задач, Как я объясняю в своем блоге.- хотя не совсем требуется , это хорошая идея, чтобы следовать рекомендациям в асинхронном шаблоне на основе задач ; следование этим соглашениям именования (etc) поможет другим разработчикам поддерживать код.
Основываясь на них, я также рекомендую мое вступление к
asyncзапись в блоге.Переходим к актуальной проблеме...
Обновление привязанного к данным кода из фоновых потоков всегда сложно. Я рекомендую вам относиться к данным ViewModel так, как если бы они были частью пользовательского интерфейса (это "логический пользовательский интерфейс", так сказать). Так что это нормально, чтобы получить данные в фоновом потоке, но обновление фактических значений виртуальной машины должно выполняться в потоке пользовательского интерфейса.
Эти изменения делают ваш код более похожим на этот:
async Task RefreshAsync() { var instances = await TaskEx.Run(() => serviceagent.GetInstances()); DataContext = instances; var groupResults = await TaskEx.Run(() => serviceagent.GetGroups(instances)); instances.Admin = groupResults.Admin; instances.Groups = new ObservableCollection<Group>(groupResults.Groups); } public GroupsResult GetGroups(Instances instances) { return new GroupsResult { Admin = service.GetAdmin(), Groups = Admin.Groups.ToArray(), }; }Следующее, что вам нужно проверить, является ли
InstancesреализациейINotifyPropertyChanged. Вам не нужно вызывать событиеResetcollection changed при установкеGroups; так какGroupsявляется свойствомInstances, это обязанностьInstancesвызыватьINotifyPropertyChanged.PropertyChanged.В качестве альтернативы, вы можете просто установить
DataContextlast:async Task RefreshAsync() { var instances = await TaskEx.Run(() => serviceagent.GetInstances()); var groupResults = await TaskEx.Run(() => serviceagent.GetGroups(instances)); instances.Admin = groupResults.Admin; instances.Groups = new ObservableCollection<Group>(groupResults.Admin.Groups); DataContext = instances; }
Похоже, есть некоторая путаница в том, что такое DataContext. DataContext-это не какой-то специальный объект, который нужно обновить. Это ссылка на объект или объекты, которые вы хотите привязать к своему пользовательскому интерфейсу. Всякий раз, когда вы вносите изменения в эти объекты, пользовательский интерфейс получает уведомление (если вы реализуете правильные интерфейсы).
Таким образом, если вы явно не измените DataContext, ваш пользовательский интерфейс не сможет догадаться, что теперь вы хотите показать другой набор объектов.
На самом деле, в вашем коде нет причина установить DataContext дважды. Просто установите его с конечным набором объектов, которые вы хотите отобразить. На самом деле, поскольку вы работаете с одними и теми же данными, нет смысла использовать две задачи:
async Task refresh() { // returns object of type Instances DataContext=await Task.Factory.StartNew(() => { var instances = serviceagent.GetInstances(); return serviceagent.GetGroups(instances); }); }Примечание:
Вы не должны использовать подпись
async void. Он используется только для обработчиков событий fire-and-forget, где вам все равно, успешны они или нет. Причина в том, что методasync voidне может быть ожидаем, поэтому никто не может знать, удалось ли это или нет.