Binding Shapes.Path items to a ItemsControl

2.3k views Asked by At

I have been trying to figure out how to bind an ObservableCollection<FrameworkElements> to an ItemsControl. I have an existing project which relies heavily on code behind and canvas's without binding which I am trying to update to use mvvm and prism.

The ObservableCollection is going to be populated with a number of Path items. They are generated from an extermal library which I use. The library functions correctly when I manually manipulate the canvas itself.

Here is a snippet of the code from the ViewModel:

    ObservableCollection<FrameworkElement> _items;
    ObservableCollection<FrameworkElement> Items
    {
        get { return _items; }
        set
        {
            _items = value;
            this.NotifyPropertyChanged("Items");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

Supporting XAML

    <ItemsControl ItemsSource="{Binding Items}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <Canvas x:Name="canvas" IsItemsHost="True">
                        <Canvas.Background>
                            <SolidColorBrush Color="White" Opacity="100"/>
                        </Canvas.Background>

                    </Canvas>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Path/>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>

The issue I am experiencing is that the Path's never draw. Any suggestion on where I am going wrong and where to start the debug process?

2

There are 2 answers

8
Clemens On

Your view model should contain an abstract representation of a Path, e.g.

public class PathData
{
    public Geometry Geometry { get; set; }
    public Brush Fill { get; set; }
    public Brush Stroke { get; set; }
    public double StrokeThickness { get; set; }
    // ... probably more Stroke-related properties
}

If you now have a collection of PathData objects like

public ObservableCollection<PathData> Paths { get; set; }

your ItemsControl could have an ItemsTemplate as shown below:

<ItemsControl ItemsSource="{Binding Paths}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Path Data="{Binding Geometry}" Fill="{Binding Fill}"
                  Stroke="{Binding Stroke}" StrokeThickness="{Binding StrokeThickness}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

You would now add PathData instance like this:

Paths.Add(new PathData
{
    Geometry = new RectangleGeometry(new Rect(100, 100, 100, 100)),
    Fill = Brushes.AliceBlue,
    Stroke = Brushes.Red,
    StrokeThickness = 2
});
9
AudioBubble On

ObservableCollection<FrameworkElement> Yeah, you can stop there. That isn't MVVM. Also, you define the item template as a (blank, I guess you could say) path object. You do NOT bind that to the properties of the Paths you throw in your observable collection.

A better implementation would be to have an ObservableCollection<string> containing path data, AKA the stuff that actually defines the path's shape, then bind this to your path.

<ItemsControl ItemsSource="{Binding Items}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas x:Name="canvas" IsItemsHost="True" />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Path Data="{Binding}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

If you had used Snoop to examine your UI at runtime, you'd have seen that you had lots of Path objects out there, but that they all had empty data.

Also ALSO, if you're using a canvas for the Path shapes, you need to set Canvas.Left and Canvas.Top attached properties on your Paths. A true MVVM hero would define a model thusly

public PathData{
    public double Left {get;set;}
    public double Top {get;set;}
    public string Data {get;set;}
}

then expose these in your ViewModel

public ObservableCollection<PathData> Items {get;private set;}

then bind your Path to them

<ItemsControl.ItemTemplate>
    <DataTemplate>
        <Path Canvas.Left="{Binding Left}"
              Canvas.Top="{Binding Top}"
              Data="{Binding Data}"/>
    </DataTemplate>
</ItemsControl.ItemTemplate>