1

I have a canvas in one of my controls that I'd like to add/remove children to as a collection grows/shrinks.

I know that I can do this:

<Canvas>
    <Canvas.Children>
        <Rectangle/>
        <TextBox/>
    </Canvas.Children>
</Canvas>

But these elements are statically defined. I envision something like this:

<Canvas>
    <Canvas.Children ItemsSource={Binding Path="ItemCollection", Converter="{StaticResource VisualConverter}}/>
</Canvas>

Is there something analogous to my above code, which actually works? I am aware of others posts, such as this one and this one but a problem I see with these answers (for my purposes) is that you lose the capability of naming the canvas and accessing it in code behind: it's an element in a template, and is therefore out of scope.

Community
  • 1
  • 1

1 Answers1

2

When it comes to showing a collection of items (and support Binding), you should think about ItemsControl. In this case you can just set its ItemsPanel to some ItemsPanelTemplate holding a Canvas, something like this:

<ItemsControl ItemsSource="{Binding ItemCollection, 
                           Converter={StaticResource VisualConverter}}">
   <ItemsControl.ItemsPanel>
      <ItemsPanelTemplate>
          <Canvas/>
      </ItemsPanelTemplate>
   </ItemsControl.ItemsPanel>
</ItemsControl>

Now the ItemsControl acts like a Canvas holding some dynamic collection of children.

Update:

I doubt that you are following a wrong approach. Not sure how necessary those methods are so that you have to create a custom Canvas and require some easy access to it in codebehind. Using the standard Canvas would not be able to set some Binding for the Children property because it's readonly. Here I introduce a simple implementation for an attached property supporting binding instead of the standard non-attached Children property:

public static class CanvasService
{
    public static readonly DependencyProperty ChildrenProperty = DependencyProperty.RegisterAttached("Children", typeof(IEnumerable<UIElement>), typeof(CanvasService), new UIPropertyMetadata(childrenChanged));
    private static Dictionary<INotifyCollectionChanged, Canvas> references = new Dictionary<INotifyCollectionChanged, Canvas>();
    public static IEnumerable<UIElement> GetChildren(Canvas cv)
    {
        return cv.GetValue(ChildrenProperty) as IEnumerable<UIElement>;
    }
    public static void SetChildren(Canvas cv, IEnumerable<UIElement> children)
    {
        cv.SetValue(ChildrenProperty, children);
    }
    private static void childrenChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        var canvas = target as Canvas;
        repopulateChildren(canvas);
        var be = canvas.GetBindingExpression(ChildrenProperty);
        if (be != null)
        {
            var elements = (be.ResolvedSourcePropertyName == null ? be.ResolvedSource : be.ResolvedSource.GetType().GetProperty(be.ResolvedSourcePropertyName).GetValue(be.ResolvedSource)) as INotifyCollectionChanged;
            if (elements != null)
            {
                var cv = references.FirstOrDefault(i => i.Value == canvas);
                if (!cv.Equals(default(KeyValuePair<INotifyCollectionChanged,Canvas>)))
                     references.Remove(cv.Key);
                references[elements] = canvas;
                elements.CollectionChanged -= collectionChangedHandler;
                elements.CollectionChanged += collectionChangedHandler;
            }
        } else references.Clear();            
    }
    private static void collectionChangedHandler(object sender, NotifyCollectionChangedEventArgs e)
    {
        Canvas cv;
        if (references.TryGetValue(sender as INotifyCollectionChanged, out cv))
        {                
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    foreach (var item in e.NewItems) cv.Children.Add(item as UIElement);
                    break;
                case NotifyCollectionChangedAction.Remove:
                    foreach (var item in e.OldItems) cv.Children.Remove(item as UIElement);
                    break;
                case NotifyCollectionChangedAction.Reset:
                    repopulateChildren(cv);
                    break;
            }
        }
    }
    private static void repopulateChildren(Canvas cv)
    {
        cv.Children.Clear();
        var elements = GetChildren(cv);
        foreach (UIElement elem in elements){
            cv.Children.Add(elem);
        }
    }
}

Usage in XAML:

<Canvas local:CanvasService.Children="{Binding ItemCollection, 
                                  Converter={StaticResource VisualConverter}}"/>

Again I think you should consider for another approach. You should have some solution around the ItemsControl.

King King
  • 61,710
  • 16
  • 105
  • 130
  • Thank you for your answer, though as my question mentions, I face additional problems with this approach because the Canvas is now the child element for a template, and is therefore out of scope if I were to attempt to access it elsewhere in xaml or codebehind. – Michael Alexander Nov 12 '14 at 18:26
  • @MichaelAlexander ah I did not read the answers linked in your question. But this is the only way supporting binding. Why do you want to access the Canvas in codebehind? – King King Nov 12 '14 at 18:28
  • @MichaelAlexander Note that we rarely have to do something directly with the instance of `Canvas`, we almost have to work with its children using some attached properties. – King King Nov 12 '14 at 18:34
  • well my Canvas is actually a map that inherits from Canvas, and my intention is to use it to add elements which are bound to coordinates in a similar fashion to how canvas children are set by top and left. The map also has additional functionality, like centering on a set of coordinates, and I will need to call these methods in code behind. – Michael Alexander Nov 12 '14 at 18:35
  • 1
    Thanks King King, I will begin working on making my child type friendlier to the ItemsControl functionality. – Michael Alexander Nov 14 '14 at 18:35