Search code examples
wpfxamltreeviewcollectionviewsource

Binding child nodes to CollectionViewSource


So here is my data structure:

MyViewModel
  Docs (ObservableCollection<Doc>)
    Specs (ObservableCollection<Spec>)

i.e. the ViewModel has an ObservableCollection named Docs that is a collection of Doc objects, and in turn each Doc object has a collection of Spec objects. A property named position (available in both Doc and Spec classes) stores the logical position of each doc/spec.

I now need to bind this structure to a TreeView. I need to keep both both Docs and Specs sorted at all times (TreeView supports drag-n-drop rearrangement of nodes), so a direct binding cannot work here.

Therefore I use a CollectionViewSource to perform sorting at runtime.

<CollectionViewSource x:Key="DocumentsCVS" Source="{Binding Docs}">
  <CollectionViewSource.SortDescriptions>
    <componentmodel:SortDescription PropertyName="position" />
  </CollectionViewSource.SortDescriptions>
</CollectionViewSource>

and use it in my TreeView:

<TreeView ItemsSource="{Binding Source={StaticResource DocumentsCVS}}">

So far so good. The TreeView shows my Docs sorted.

But from here onward things become confusing. How/where do I create CollectionViewSource for my specs? I tried doing this in my HierarchicalDataTemplate:

<HierarchicalDataTemplate>
  <HierarchicalDataTemplate.ItemsSource>
    <Binding>
      <Binding.Source>
        <CollectionViewSource Source="{Binding Specs}">
          <CollectionViewSource.SortDescriptions>
            <componentmodel:SortDescription PropertyName="position" />
          </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
      </Binding.Source>
    </Binding>
  </HierarchicalDataTemplate.ItemsSource>
</HierarchicalDataTemplate>    

But this doesn't work. Only Docs are listed in the TreeView, with no children inside. My gut feeling is that CollectionViewSource probably doesn't live in the same DataContext as the parent TreeViewItem.

Or is this something else?

Edit

Here is the full XAML of my TreeView:

<TreeView ItemsSource="{Binding Source={StaticResource DocumentsCVS}}" 
           PresentationTraceSources.TraceLevel="High">

  <TreeView.ItemTemplate>
    <HierarchicalDataTemplate DataType="{x:Type vm:DocumentVM}">
      <HierarchicalDataTemplate.ItemsSource>
        <Binding>
          <Binding.Source>
            <CollectionViewSource Source="{Binding Specs}">
          <CollectionViewSource.SortDescriptions>
            <componentmodel:SortDescription PropertyName="position" />
          </CollectionViewSource.SortDescriptions>
        </CollectionViewSource>
          </Binding.Source>
        </Binding>
      </HierarchicalDataTemplate.ItemsSource>
      <HierarchicalDataTemplate.ItemTemplate>
        <DataTemplate DataType="{x:Type vm:SpecVM}">
          <Grid>
            <Grid.ColumnDefinitions>
              <ColumnDefinition Width="Auto" />
              <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>

            <fa:ImageAwesome Grid.Column="0" Icon="PuzzlePiece" Width="16" Margin="3,3,6,3" Foreground="Orange" />
            <Label Grid.Column="1" Content="{Binding name}" />
          </Grid>
        </DataTemplate>
      </HierarchicalDataTemplate.ItemTemplate>
      <Grid>
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="Auto" />
          <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <fa:ImageAwesome Icon="FileWordOutline" Height="16" Margin="3,3,6,3" Foreground="Crimson" />
        <Label Grid.Column="1" Content="{Binding name}" />
      </Grid>
    </HierarchicalDataTemplate>
  </TreeView.ItemTemplate>
</TreeView>

Solution

  • <CollectionViewSource Source="{Binding Specs}">
    

    The Binding on Source there doesn't know where to go look for a DataContext (no "framework mentor"). I've kicked this around a bit. I can't find a place to define the CollectionViewSource where it inherits the DataContext from the template.

    I did find a solution.

    C#

    public class SortConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var view = CollectionViewSource.GetDefaultView(value);
            view.SortDescriptions.Add(new SortDescription((string)parameter, ListSortDirection.Ascending));
            return view;
        }
    
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
    

    XAML

    <HierarchicalDataTemplate 
        DataType="{x:Type vm:DocumentVM}" 
        ItemsSource="{Binding Specs, Converter={StaticResource SortConverter}, ConverterParameter=position}"
        >
    

    This could be made more useful by giving the converter multiple PropertyName/SortDirection properties, or a collection of SortDescriptions. You could make it a MarkupExtension. You could also just create the collection view in a viewmodel property.