Search code examples
c#wpfdata-bindingmultibindingimultivalueconverter

WPF - How to bind different datatypes in ItemsControl.Resources


I am drawing different types of paths on canvas using databinding. Canvas is in ItemsControl and I use MiltiBinding Converter.

    <ItemsControl x:Name="Items" ClipToBounds="True">
        <ItemsControl.ItemsSource>
            <MultiBinding Converter="{StaticResource CanvasDraw}">
                <Binding Path="Coords" />
                <Binding Path="Holes" />
                <Binding Path="MagnetAreas" />
                <Binding Path="PathElements" />
                <Binding Path="Image" />
            </MultiBinding>
        </ItemsControl.ItemsSource>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <Canvas HorizontalAlignment="Center" VerticalAlignment="Center" Width="0" Height="0"/>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.Resources>
            <DataTemplate x:Key="img">
                <Image Source="{Binding Image}" Width="200" Height="100"/>
            </DataTemplate>
            <DataTemplate DataType="{x:Type local:Coord}">
                <Path Data="{Binding Geometry}" Style="{StaticResource Coord}" />
            </DataTemplate>
            <DataTemplate DataType="{x:Type local:Hole}">
                <Path Data="{Binding Geometry}" Style="{StaticResource Hole}" />
            </DataTemplate>
            <DataTemplate DataType="{x:Type local:MagnetArea}">
                <Path Data="{Binding Geometry}" Style="{StaticResource MagnetArea}" />
            </DataTemplate>
            <DataTemplate DataType="{x:Type local:PathElement}">
                <Path Data="{Binding Geometry}" Style="{StaticResource PathElement}" />
            </DataTemplate>
        </ItemsControl.Resources>
        <ItemsControl.ItemContainerStyle>
            <Style TargetType="ContentPresenter">
                <Setter Property="Canvas.Left" Value="{Binding Path=PosX}" />
                <Setter Property="Canvas.Top" Value="{Binding Path=PosY}" />
                <Setter Property="Panel.ZIndex" Value="{Binding Path=ZIndex}" />
            </Style>
        </ItemsControl.ItemContainerStyle>
    </ItemsControl>

My ViewModel consists of 4 ObservableCollections of different types (Coords, Holes, MagnetAreas, PathElements), each derived from the same class (Element), so in converter, I just create combined collection of type Element. Every element has its own property Geometry, that is bound to DataTemplates' Path Data.

public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {          
        ObservableCollection<Element> combinedCollection = new();
        if (values == null || values.Length <= 0) 
            return combinedCollection;
        foreach (var element in (ObservableCollection<Coord>)values[0])
            combinedCollection.Add(element);
        foreach (var element in (ObservableCollection<Hole>)values[1])
            combinedCollection.Add(element);
        foreach (var element in (ObservableCollection<MagnetArea>)values[2])
            combinedCollection.Add(element);
        foreach (var element in (ObservableCollection<PathElement>)values[3])
            combinedCollection.Add(element);
        return combinedCollection;
    }

Until now, everything works perfectly. But I would like to draw also one image on the canvas. I guess I have to do that through DataTemplate as well (manually adding Image element into canvas did not work), but I have no idea how to change my Converter and binding Paths to do that, since this DataTemplate has different type. Property ImageSource Image is also in my ViewModel. Code above obviously does not work, but at least, converter is correctly triggered when property Image is changed.


Solution

  • You don't need multibinding here. It will not work correctly when changing source collections at runtime. You need to use CompositeCollection. A slight difficulty with its use is that it is not Freezable and therefore bindings with the default source do not work in it. In addition, you need to convert a single property to an IEnumerable with one element.

    Here is an example of such an implementation:

            <ItemsControl x:Name="Items" ClipToBounds="True"
                          ItemsControl="{DynamicResource collection}">
                <ItemsControl.Resources>
                    <CompositeCollection x:Key="collection">
                        <CollectionContainer
                            Collection="{Binding DataContext.Coords,
                                                 Source={x:Reference Items}}"/>
                        <CollectionContainer
                            Collection="{Binding DataContext.Holes,
                                                 Source={x:Reference Items}}"/>
                        <CollectionContainer
                            Collection="{Binding DataContext.MagnetAreas,
                                                 Source={x:Reference Items}}"/>
                        <CollectionContainer
                            Collection="{Binding DataContext.PathElements,
                                                 Source={x:Reference Items}}"/>
                        <CollectionContainer
                            Collection="{Binding DataContext.Image,
                                                 Source={x:Reference Items},
                                                 Converter={local:ObjectToIEnumerable}}"/>
                    </CompositeCollection>
                </ItemsControl.Resources>
    
        [ValueConversion(typeof(object), typeof(IEnumerable))]
        public class ObjectToIEnumerableConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                return new Enumer(value);
            }
    
            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                throw new NotImplementedException();
            }
    
            private struct Enumer : IEnumerable
            {
                private readonly object _value;
    
                public Enumer(object value)
                {
                    _value = value;
                }
    
                public IEnumerator GetEnumerator()
                {
                   yield return _value;
                }
            }    
    
            public static ObjectToIEnumerableConverter Instance { get; } = new ObjectToIEnumerableConverter();
        }
    
        [MarkupExtensionReturnType(typeof(ObjectToIEnumerableConverter))]
        public class ObjectToIEnumerableExtension : MarkupExtension
        {
            public override object ProvideValue(IServiceProvider serviceProvider)
            {
                return ObjectToIEnumerableConverter.Instance;
            }
        }
    

    There is also a solution using ContentPresenter (for ListBox - ListBoxItem). But I haven't tested it to work.

                    <CompositeCollection>
                        -------------
                        -------------
                        <ContentPresenter
                            Content="{Binding DataContext.Image,
                                              Source={x:Reference Items}}"/>
                    </CompositeCollection>