Reputation: 26324
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.
Upvotes: 59
Views: 52319
Reputation: 14256
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.
Upvotes: 20
Reputation: 156
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}"/>
Upvotes: 14
Reputation: 101555
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.
Upvotes: 26
Reputation: 29120
<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>
Upvotes: 146
Reputation:
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.
Upvotes: 12