8

I wanted to bind to an ObservableCollection in XAML and also apply the grouping there. In principle, this worked fine.

<UserControl.Resources>
    <CollectionViewSource x:Key="cvs" Source="{Binding Path=TestTemplates}">
        <CollectionViewSource.SortDescriptions>
            <scm:SortDescription PropertyName="Title"/>
        </CollectionViewSource.SortDescriptions>
        <CollectionViewSource.GroupDescriptions>
            <PropertyGroupDescription PropertyName="TestCategory"/>
        </CollectionViewSource.GroupDescriptions>
    </CollectionViewSource>
</UserControl.Resources>

Then the data binding expression became ItemsSource="{Binding Source={StaticResource ResourceKey=cvs}}" instead of ItemsSource="{Binding Path=TestTemplates}".

At first, everything seemed cool, until I wanted to refresh the UI from the view model. The problem is, that CollectionViewSource.GetDefaultView(TestTemplates) returned a different view than the one from XAML where the grouping was applied. Thus, I could not set selection or do anything useful with it.

I could fix it by binding the list again directly to the view model's property and setting up the grouping in the code-behind. But I'm not that happy with this solution.

private void UserControlLoaded(object sender, RoutedEventArgs e)
{
    IEnumerable source = TemplateList.ItemsSource;
    var cvs = (CollectionView)CollectionViewSource.GetDefaultView(source);
    if (cvs != null)
    {
        cvs.SortDescriptions.Add(new SortDescription("Title", ListSortDirection.Ascending));
        cvs.GroupDescriptions.Add(new PropertyGroupDescription("TestCategory"));
    }
}

I assume, the reason for that is already given by John Skeet here.

Nevertheless, I would expect that there should be a way to get the right view. Am I wrong?

Community
  • 1
  • 1
primfaktor
  • 2,831
  • 25
  • 34
  • You are going about it the wrong way. A VM should have no knowledge of the view. If you want to update the view, ensure the property it binds to is either an ObservableCollection or your code raises NotifyPropertyChanged explicitly when modifying the collection. – Panagiotis Kanavos Jun 26 '12 at 10:20
  • @PanagiotisKanavos: The stuff in the list view actually *is* in an `ObservableCollection` and the items in the UI do update on a property change. But the grouping does not respect that. A known workaround is to force the update, i.e. `CollectionViewSource.GetDefaultView(…).Refresh`. – primfaktor Jun 26 '12 at 10:47
  • In .NET 4.5, this will be remedied with the [ICollectionViewLiveShaping](http://msdn.microsoft.com/en-us/library/system.componentmodel.icollectionviewliveshaping(v=vs.110).aspx). – primfaktor Jun 26 '12 at 10:48

3 Answers3

6

I tend to just expose the collection view from the VM rather than have the view define it:

public ICollection<Employee> Employees
{
    get { ... }
}

public ICollectionView EmployeesView
{
    get { ... }
}

That way your VM has full control over what is exposed to the view. It can, for example, change the sort order in response to some user action.

Kent Boogaart
  • 175,602
  • 35
  • 392
  • 393
  • That's a good way to do it, but it does not answer the question. I'd still like to know. – primfaktor Jun 26 '12 at 09:47
  • What do you mean by "refresh the view" and why do you want to do it? If the underlying collection exposed by your VM implements `INotifyCollectionChanged` (such as with `ObservableCollection`), then the collection view created by your view should stay up to date, thus saving you the trouble of "refreshing the view". – Kent Boogaart Jun 26 '12 at 11:16
  • 1
    I should have qualified the above. As long as you're only adding/removing items, the collection view should stay sorted. If you're modifying the underlying property by which you're sorting, that's one valid reason why you'd need to "refresh". If you really want the view to create the CV rather than the VM, then you'll have to raise an event in your VM whenever the view should refresh. IMHO, it's just cleaner having the VM handle it all, since it's business logic anyways. – Kent Boogaart Jun 26 '12 at 12:17
  • Yes, underlying collection is `ObservableCollection`. But no, it does not completely update. The sorted list `aaa, bbb, eee` turns to `aaa, zzz, eee` automatically on property change. To get the resort, you have to tell it. So yes, this is the valid reason you're mentioning. – primfaktor Jun 26 '12 at 12:27
  • +1 for your second comment which is another way to do it. Nevertheless, I'm cannot believe that the “right” CV is really inaccessible. – primfaktor Jun 26 '12 at 12:29
5

You could not just do that?

var _viewSource = this.FindResource("cvs") as CollectionViewSource;

If the data is connected, I assume that will have an updated view.

J. Lennon
  • 3,311
  • 4
  • 33
  • 64
  • This was also brought up by a colleague. The problem is that in the VM, I don't get to the `FindResource` of any `FrameworkElement` (separate assemblies, only Vs reference VMs). – primfaktor Jun 27 '12 at 05:58
  • As your answer comes closest, you shall have the bounty. Spend it wisely. ;-) – primfaktor Jul 02 '12 at 11:10
2

Found a way, based on J. Lennon's answer. If I pass something that has access to the resources with my command, then I can look up the CollectionViewSource there.

In XAML (CollectionViewResource as above):

<Button Command="{Binding Command}" CommandParameter="{Binding RelativeSource={RelativeSource Self}}">Do it!</Button>

And in the VM code:

private void Execute(object parm)
{
    var fe = (FrameworkElement)parm;
    var cvs = (CollectionViewSource)fe.FindResource("cvs");
    cvs.View.Refresh();
}

The Execute is the one that is given to the RelayCommand.

This would answer the question, but I don't like it very much. Opinions?

Community
  • 1
  • 1
primfaktor
  • 2,831
  • 25
  • 34
  • As this is more complete (and working) than the base of [J. Lennon](http://stackoverflow.com/users/1128106/j-lennon), I mark this (but he got the bounty). – primfaktor Jul 02 '12 at 11:13