Обновление 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 3

3 ответа:

Есть несколько пунктов, которые выделяются в части async вашего кода:

Основываясь на них, я также рекомендую мое вступление к 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 не может быть ожидаем, поэтому никто не может знать, удалось ли это или нет.

Я обнаружил, что RaiseCollectionChanged не оказывает никакого влияния на свойство Groups, к которому привязано DataContext. Я просто должен уведомить: instances.RaisePropertyChanged("Groups");.