59

I'm a little surprised that it is not possible to set up a binding for Canvas.Children through XAML. I've had to resort to a code-behind approach that looks something like this:

private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
    DesignerViewModel dvm = this.DataContext as DesignerViewModel;
    dvm.Document.Items.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Items_CollectionChanged);

    foreach (UIElement element in dvm.Document.Items)
        designerCanvas.Children.Add(element);
}

private void Items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    ObservableCollection<UIElement> collection = sender as ObservableCollection<UIElement>;

    foreach (UIElement element in collection)
        if (!designerCanvas.Children.Contains(element))
            designerCanvas.Children.Add(element);

    List<UIElement> removeList = new List<UIElement>();
    foreach (UIElement element in designerCanvas.Children)
        if (!collection.Contains(element))
            removeList.Add(element);

    foreach (UIElement element in removeList)
        designerCanvas.Children.Remove(element);
}

I'd much rather just set up a binding in XAML like this:

<Canvas x:Name="designerCanvas"
        Children="{Binding Document.Items}"
        Width="{Binding Document.Width}"
        Height="{Binding Document.Height}">
</Canvas>

Is there a way to accomplish this without resorting to a code-behind approach? I've done some googling on the subject, but haven't come up with much for this specific problem.

I don't like my current approach because it mucks up my nice Model-View-ViewModel by making the View aware of it's ViewModel.

H.B.
  • 166,899
  • 29
  • 327
  • 400
Rob
  • 25,984
  • 32
  • 109
  • 155

5 Answers5

146
<ItemsControl ItemsSource="{Binding Path=Circles}">
    <ItemsControl.ItemsPanel>
         <ItemsPanelTemplate>
              <Canvas Background="White" Width="500" Height="500"  />
         </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Ellipse Fill="{Binding Path=Color, Converter={StaticResource colorBrushConverter}}" Width="25" Height="25" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
    <ItemsControl.ItemContainerStyle>
        <Style>
            <Setter Property="Canvas.Top" Value="{Binding Path=Y}" />
            <Setter Property="Canvas.Left" Value="{Binding Path=X}" />
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>
kenwarner
  • 28,650
  • 28
  • 130
  • 173
  • ...Or you could just set Canvas.Left and Canvas.Top on the container that I mentioned. This is the way to go. – Josh G Mar 09 '10 at 21:40
  • 15
    hardly - your solution creates an individual canvas for each item - this solution is far more elegant and efficient as only one canvas is used to render all items – Xcalibur Oct 02 '11 at 13:45
  • One thing that made me look for bugs for 3 hours: {Binding Path=Circles}. When you bind to window class public property, you should do this like that: {Binding Path=Circles ElementName=$WindowName$}. Otherwise it just won't work (At least in VS 2012). – Nebril Jan 24 '13 at 16:21
  • 6
    Just in case some one tries to do this on WinRT like me: Bindings are not supported in setters there - http://stackoverflow.com/questions/11857505/how-do-i-do-bindings-in-itemcontainerstyle-in-winrt – Christoph Dec 27 '13 at 21:46
26

Others have given extensible replies on how to do what you actually want to do already. I'll just explain why you couldn't bind Children directly.

The problem is very simple - data binding target cannot be a read-only property, and Panel.Children is read-only. There is no special handling for collections there. In contrast, ItemsControl.ItemsSource is a read/write property, even though it is of collection type - a rare occurence for a .NET class, but required so as to support the binding scenario.

Pavel Minaev
  • 99,783
  • 25
  • 219
  • 289
20

ItemsControl is designed for creating dynamic collections of UI controls from other collections, even non-UI data collections.

You can template an ItemsControl to draw on a Canvas. The ideal way would involve setting the backing panel to a Canvas and then setting the Canvas.Left and Canvas.Top properties on the immediate children. I could not get this to work because ItemsControl wraps its children with containers and it is hard to set the Canvas properties on these containers.

Instead, I use a Grid as a bin for all of the items and draw them each on their own Canvas. There is some overhead with this approach.

<ItemsControl x:Name="Collection" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate DataType="{x:Type local:MyPoint}">
            <Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                <Ellipse Width="10" Height="10" Fill="Black" Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}"/>
            </Canvas>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

Here's the code behind that I used to set up the source collection:

List<MyPoint> points = new List<MyPoint>();

points.Add(new MyPoint(2, 100));
points.Add(new MyPoint(50, 20));
points.Add(new MyPoint(200, 200));
points.Add(new MyPoint(300, 370));

Collection.ItemsSource = points;

MyPoint is a custom class that behaves just like the System version. I created it to demonstrate that you can use your own custom classes.

One final detail: You can bind the ItemsSource property to any collection you want. For example:

<ItemsControls ItemsSource="{Binding Document.Items}"><!--etc, etc...-->

For further details about ItemsControl and how it works, check out these documents: MSDN Library Reference; Data Templating; Dr WPF's series on ItemsControl.

kukuch
  • 19
  • 1
  • 6
Josh G
  • 14,068
  • 7
  • 62
  • 74
  • 5
    Doesn't this have performance problems, since you're adding an entirely new canvas for each point you want to draw? – Benjamin Mar 08 '16 at 19:20
14
internal static class CanvasAssistant
{
    #region Dependency Properties

    public static readonly DependencyProperty BoundChildrenProperty =
        DependencyProperty.RegisterAttached("BoundChildren", typeof (object), typeof (CanvasAssistant),
                                            new FrameworkPropertyMetadata(null, onBoundChildrenChanged));

    #endregion

    public static void SetBoundChildren(DependencyObject dependencyObject, string value)
    {
        dependencyObject.SetValue(BoundChildrenProperty, value);
    }

    private static void onBoundChildrenChanged(DependencyObject dependencyObject,
                                               DependencyPropertyChangedEventArgs e)
    {
        if (dependencyObject == null)
        {
            return;
        }
        var canvas = dependencyObject as Canvas;
        if (canvas == null) return;

        var objects = (ObservableCollection<UIElement>) e.NewValue;

        if (objects == null)
        {
            canvas.Children.Clear();
            return;
        }

        //TODO: Create Method for that.
        objects.CollectionChanged += (sender, args) =>
                                            {
                                                if (args.Action == NotifyCollectionChangedAction.Add)
                                                    foreach (object item in args.NewItems)
                                                    {
                                                        canvas.Children.Add((UIElement) item);
                                                    }
                                                if (args.Action == NotifyCollectionChangedAction.Remove)
                                                    foreach (object item in args.OldItems)
                                                    {
                                                        canvas.Children.Remove((UIElement) item);
                                                    }
                                            };

        foreach (UIElement item in objects)
        {
            canvas.Children.Add(item);
        }
    }
}

And using:

<Canvas x:Name="PART_SomeCanvas"
        Controls:CanvasAssistant.BoundChildren="{TemplateBinding SomeItems}"/>
Ivan Shikht
  • 156
  • 1
  • 3
12

I don't believe its possible to use binding with the Children property. I actually tried to do that today and it errored on me like it did you.

The Canvas is a very rudimentary container. It really isn't designed for this kind of work. You should look into one of the many ItemsControls. You can bind your ViewModel's ObservableCollection of data models to their ItemsSource property and use DataTemplates to handle how each of the items is rendered in the control.

If you can't find an ItemsControl that renders your items in a satisfactory way, you might have to create a custom control that does what you need.