0

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.

MichalV
  • 3
  • 1
  • I can't help you with your canvas image, because i have no clue what image you want to draw on the canvas and how that image depends/relates to the items in your collections. But i want to point out that with the use of your converter, you are practically making it impossible for your ItemsSource to observe changes to the observable collections (in a practical sense, due to the converter, the observable collections act like "dumb" List as far as the ItemsControl is concerned). Not sure if you are aware of that or concerned about it, but i thought better safe than sorry and point it out. –  Sep 27 '22 at 18:30
  • Image should be just simple JPG from file, no relation to my collections. As for converter, I am pretty new to wpf and trying to learn as much as possible, so I was really happy this worked, altough, like you stated, it required few "refresh hacks". I would really appreciate some comments on how to do this correctly. – MichalV Sep 27 '22 at 18:39

1 Answers1

0

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>
EldHasp
  • 6,079
  • 2
  • 9
  • 24
  • 1
    Your suggestion with CompositeCollection was exactly what I needed, altough using your code as it is, I got "Cannot call MarkupExtension.ProvideValue because of a cyclical dependency" exception. Only way I got it to work was to move into ``, name it and add ItemsSource directly into ItemsControl tag as DynamicResource: `` – MichalV Sep 27 '22 at 20:50
  • @MichalV, I wrote the code here in the post editor, from my memory. I guess I was wrong (I forgot) and you are right. I will correct the code in my answer. – EldHasp Sep 27 '22 at 20:55