How to Bind a Canvas's Children Property in Xaml

Is it possible to bind a Canvas's Children property in XAML?

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.

Binding WPF Canvas Children to an ObservableCollection

I think you can do this with ItemsControl + ItemsPanelTemplate. Like this:

<ItemsControl ItemsSource="{Binding YourCollection}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>

To read more about this approach refer to Dr.WPF: ItemsControl: A to Z (P is for Panel)

How to bind a canvas' children to a collection?

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.

Is it possible to bind to Canvas.Children?

If you look at the question Is it possible to bind a Canvas's Children property in XAML? and further down there is an answer from Ivan which uses an attached property you can bind to and it automatically updates the canvas children. I haven't tried it but looks like it should work. Seems the best solution to me. The other option is to use the ItemsControl with a DataTemplate for each type you want to show - however that seems a bit fiddly.

Canvas Children Property Binding

If you use an ItemsControl with an item type that is a UIElement (which is a bit unusual), the control will not create an additional item container - i.e. a ContentPresenter - element for it, but instead apply the ItemContainerStyle directly to the item. You can verify this by setting TargetType="espace:Entity" on the Style.

In this case, the ItemsControl does also not set the DataContext of the UIElement item, which means that Bindings without an explictly set source won't work. The Bindings in the ItemContainerStyle would use the item object directly as its source, i.e. use RelativeSource Self.

It is also useless to declare a DataTemplate for the item type (especially an empty one), because it would be ignored. The item is not considered to be "data", but UI.

<ItemsControl ItemsSource="{Binding CanvasChildren}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="espace:Entity">
<Setter
Property="Canvas.Left"
Value="{Binding X, RelativeSource={RelativeSource Self}}"/>
<Setter
Property="Canvas.Top"
Value="{Binding Y, RelativeSource={RelativeSource Self}}"/>
<Setter
Property="Panel.ZIndex"
Value="{Binding Z, RelativeSource={RelativeSource Self}}"/>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>

Can WPF Canvas Children Bind ObservableCollection contain ViewModel for different shapes and TextBlocks?

Your view model should contain a base class Node that defines the XPos and YPos properties, and derived classes for the specific node types, e.g. TextNode and ShapeNode:

public class Node
{
public double XPos { get; set; }
public double YPos { get; set; }
}

public class TextNode : Node
{
public string Text { get; set; }
}

public class ShapeNode : Node
{
public Geometry Geometry { get; set; }
public Brush Stroke { get; set; }
public Brush Fill { get; set; }
}

public class ViewModel
{
public ObservableCollection<Node> Nodes { get; } = new ObservableCollection<Node>();
}

In XAML, you would add DataTemplates for the specific nodes types like shown below. See the Data Templating Overview article on MSDN for details.

<ItemsControl ItemsSource="{Binding Path=Nodes}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:TextNode}">
<TextBlock Text="{Binding Text}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:ShapeNode}">
<Path Data="{Binding Geometry}" Stroke="{Binding Stroke}" Fill="{Binding Fill}"/>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Path=XPos}" />
<Setter Property="Canvas.Top" Value="{Binding Path=YPos}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>

You might now add different nodes to an instance of the main view model class and set the window's DataContext to that instance:

public MainWindow()
{
InitializeComponent();

var vm = new ViewModel();

vm.Nodes.Add(new TextNode
{
XPos = 50,
YPos = 100,
Text = "Hello, World."
});

vm.Nodes.Add(new ShapeNode
{
XPos = 100,
YPos = 200,
Geometry = new EllipseGeometry { RadiusX = 50, RadiusY = 50 },
Fill = Brushes.Red
});

DataContext = vm;
}

If you want your view to react on property changes of the nodes, the Node class should implement the INotifyPropertyChanged interface.

If the items should be selectable, you should replace the ItemsControl by a ListBox. The TargetType of the ItemContainerStyle would then be ListBoxItem, and you would bind its IsSelected property to an appropriate property on your Node class.

How to set children property in a canvas event handler?

Add the following lines at the bottom of your Initialize method:

Canvas.SetTop(ShapeRect, 0);
Canvas.SetLeft(ShapeRect, 0);

When you get to your handler, the values of Canvas.GetTop(...) is NaN.

WPF: Sizing Canvas to contain its children

You may use a custom Canvas that overrides the MeasureOverride method

public class MyCanvas : Canvas
{
protected override Size MeasureOverride(Size constraint)
{
base.MeasureOverride(constraint);

var size = new Size();

foreach (var child in Children.OfType<FrameworkElement>())
{
var x = GetLeft(child) + child.Width;
var y = GetTop(child) + child.Height;

if (!double.IsNaN(x) && size.Width < x)
{
size.Width = x;
}

if (!double.IsNaN(y) && size.Height < y)
{
size.Height = y;
}
}

return size;
}
}

and which would be used like this:

<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:MyCanvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

It requires that the item container element, besides Canvas.Left and Canvas.Top has its Width and Height set in the ItemContainerStyle:

<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
<Setter Property="Width" Value="{Binding Width}"/>
<Setter Property="Height" Value="{Binding Height}"/>
</Style>
</ItemsControl.ItemContainerStyle>

The ItemTemplate would then just look like this:

<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderThickness="1" BorderBrush="Black">
<Rectangle Fill="{Binding Color}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>

or

<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle StrokeThickness="1" Stroke="Black" Fill="{Binding Color}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>

You may also want to put the SCrollViewer into the ControlTemplate of the ItemsControl:

<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>


Related Topics



Leave a reply



Submit