Обновление 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
. Вам не нужно вызывать событиеReset
collection changed при установкеGroups
; так какGroups
является свойствомInstances
, это обязанностьInstances
вызыватьINotifyPropertyChanged.PropertyChanged
.В качестве альтернативы, вы можете просто установить
DataContext
last: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
не может быть ожидаем, поэтому никто не может знать, удалось ли это или нет.