3

I fetch data for a wpf window in a backgroundthread like this [framework 4.0 with 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
}

When I include the actions of GetGroups in GetInstances the UI shows the groups.
When I update in a seperate action the DataContext includes the groups correclty but the UI doesn't show them.

In the GetGroups() method I inlcuded NotifyCollectionChangedAction.Reset for the ObservableCollection of groups and this doesn't help.
Extra strange is that I call NotifyCollectionChangedAction.Reset on the list only once, but is executed three times, while the list has ten items?!

I can solve the issue by writing:

DataContext = await Task.Factory.StartNew(() => serviceagent.GetGroups(instances));

But is this the regular way for updating DataContxt and UI via a backgound process?
Actually I only want to update the existing DataContext without setting it again?

EDIT: serviceagent.GetGroups(instances) in more detail:

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();
}

Here ViewModelCollection<T> inherits from ObservableCollection<T> and I added the method:

public void RaiseCollectionChanged()
{
    var handler = CollectionChanged;
    if (handler != null)
    {
        Trace.WriteLine("collection changed");
        var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
        handler(this, e);
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Gerard
  • 13,023
  • 14
  • 72
  • 125
  • If the binding in the XAML file is done correctly I think that the problem is in the GetGroups method. Can you show us this method? – Michael Mairegger Nov 11 '13 at 10:00
  • Basically the code is just `{ instances.Groups = new ViewModelCollection(webcall); RaiseCollectionChanged(); }` – Gerard Nov 11 '13 at 10:26
  • Please put the code in the question, not the comments. Also add any missing relevant code. Where do you call Reset and what does it have to do with the rest of the question? – Panagiotis Kanavos Nov 11 '13 at 10:36

3 Answers3

4

Seems there's a bit of confusion on what DataContext is. DataContext is not some special object that you have to update. It's a reference to the object or objects that you want to bind to your UI. Whenever you make changest to these objects, the UI get's notified (if you implement the proper interfaces).

So, unless you explicitly change the DataContext, your UI can't guess that now you want to show a different set of objects.

In fact, in your code, there is no reason to set the DataContext twice. Just set it with the final set of objects you want to display. In fact, since you work on the same data, there is no reason to use two tasks:

async Task refresh()
{
    // returns object of type Instances
    DataContext=await Task.Factory.StartNew(() => {
             var instances = serviceagent.GetInstances();
             return serviceagent.GetGroups(instances);
    });
}

NOTE:

You should neer use the async void signature. It is used only for fire-and-forget event handlers, where you don't care whether they succeed or fail. The reason is that an async void method can't be awaited so no-one can know whether it succeeded or not.

Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • async void is working, why would I need async Task? Note that GetInstances() is not a void method, only the GetGroups() is void, I thought I could pass the DataContext so to speak by reference? – Gerard Nov 11 '13 at 10:23
4

There's a few points that stand out in the async portion of your code:

Based on these, I also recommend my intro to async blog post.

On to the actual problem...

Updating data-bound code from background threads is always tricky. I recommend that you treat your ViewModel data as though it were part of the UI (it is a "logical UI", so to speak). So it's fine to retrieve data on a background thread, but updating the actual VM values should be done on the UI thread.

These changes make your code look more like this:

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(),
  };
}

The next thing you need to check is whether Instances implements INotifyPropertyChanged. You don't need to raise a Reset collection changed event when setting Groups; since Groups is a property on Instances, it's the responsibility of Instances to raise INotifyPropertyChanged.PropertyChanged.

Alternatively, you could just set 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;
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thank's for the great advice w.r.t. `TaskEx.Run`, I had no idea. You also saw the flaw in my `PropertyChanged`. One disadvantage of your solution is that it would require to reference all data-contracts in the UI project. I want these references only in my ViewModel project. – Gerard Nov 11 '13 at 13:28
  • So instead of `instances.Groups = new ObservableCollection(groupResults.Groups);` in the UI project I do exactly the same in the VM project followed by `RaisePropertyChanged("Groups");`. Isn't that more along the lines of e.g. MVVM instead of setting the lists in the View? – Gerard Nov 11 '13 at 13:29
1

I discovered that RaiseCollectionChanged has no influence on the property Groups where the DataContext is bound to. I simply have to notify: instances.RaisePropertyChanged("Groups");.

Gerard
  • 13,023
  • 14
  • 72
  • 125