Monday, February 20, 2012

XAML binding to CompositeCollection

Some times there is a need to display in your ItemsControl (like TreeView or ListBox) some compound collection, which consists from several collections of elements.
Well, microsoft provides us CompositeCollection and CollectionContainer classes for this purpose. However it is a real pain to use data binding with this classes in XAML. Here is one of solution suggested, which looks a bit ugly if you ask me: http://wilberbeast.com/2011/05/31/compositecollection-binding-problem/

I'll show you alternative you can use to make it looks a bit better and neat.

For example I have a view model of Survey, which has properties:

public class SurveyViewModel{
       public InstructionsModel Instructions {get;}
       public ObservableCollection<QuestionModel> Children {get;}
       public ReviewModel Review {get;}
}

And I need to display in a listbox the following items:
[Instructions]
[Question1]

[Question2]
...
[QuestionN]
[Review]

I keep in mind that Children collection can be updated - in that case ListBox must  be automagically refreshed (new element might be added/removed by some action buttons).
I want to each item to be displayed in a different manner based on its type and associated DataTemplate.In order to do that, i need to assign in myListBox.ItemsSource to CompositeCollection with CollectionContainers inside. But if you will try it you will see how many gabage you will have to put in different parts of your XAML. Here is the alternative approach how I do it now:

<TreeView
    x:Name="tvSurvey">
    <TreeView.ItemsSource>
        <MultiBinding>
            <MultiBinding.Converter>
                <local:CompositeCollectionConverter />
            MultiBinding.Converter>
            <Binding Path="ViewModel.Instructions" />
            <Binding Path="ViewModel.Children" />
            <Binding Path="ViewModel.Review" />
        </MultiBinding>
    </TreeView.ItemsSource>
...
     TreeView.Resources with datatemplates for:
     InstructionsModel, QuestionModel, ReviewModel
...
</TreeView>

As you already may see there is no magic here: just use MultiBinding, which supports current DataContext (unlike CollectionContainer) plus CompositeCollectionConverter (IMultiValueConverter) for converting multibinding to composite collection.

Here is the code of CompositeCollectionConverter:


    public class CompositeCollectionConverter:IMultiValueConverter
    {
 
        public object Convert(object[] values
            , Type targetType
            , object parameter
            , System.Globalization.CultureInfo culture)
        {
            var res = new CompositeCollection();
            foreach (var item in values)
                if (item is IEnumerable) 
                    res.Add(new CollectionContainer() 
                            { 
                                Collection = item as IEnumerable 
                            });
                else res.Add(item);
            return res;
        }
 
        public object[] ConvertBack(object value
            , Type[] targetTypes
            , object parameter
            , System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }



Enjoy :-)