I have a canvas which contains several different shapes, which are all static, and bound to different properties in the view model (MVVM). As of now, the canvas is defined as the following (simplified):
<Canvas>
<Polygon Fill="Red" Stroke="Gray" StrokeThickness="3" Points="{Binding StorageVertices}" />
<Ellipse Fill="Blue" Width="{Binding NodeWidth}" Height="{Binding NodeHeight}" />
<!-- And some more static shapes -->
<!-- ... -->
</Canvas>
To this canvas, I want to add a dynamic list where each entry is converted to a polygon. I thought that the best approach would be an ItemsControl
. This is what I've used in my approach but only the first item in the collection (list) is displayed.
<Canvas>
<!-- ... -->
<!-- Same canvas as earlier with the addition of the ItemsControl -->
<ItemsControl ItemsSource="{Binding Offices, Mode=OneWay, Converter={...}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Polygon Fill="AliceBlue" Stroke="Gray" StrokeThickness="1" Points="{Binding Points}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Canvas>
With this code, only the first item in the Offices
collection is displayed. How come? If I view the visual tree all polygons are within it. I'm very new to WPF, so I can only guess, but my first thought was that the default of a StackPanel
as an ItemPresenter
might be inappropriate in this case, but I can only guess...
Well, a few things to note here. Firstly, when working with the Canvas
panel, each item within the panel will be placed at the top-left unless a relative location is specified. Here is an example of a Canvas
with your elements, one placed near the top (40 pixels down, 40 to the right), the other placed at the bottom (100 pixels to the left from the right edge):
<Canvas>
<Polygon Canvas.Left="40" Canvas.Top="40" ... />
<Ellipse Canvas.Right="100" Canvas.Bottom="0" ... />
</Canvas>
Now, remember that a Canvas
is a type of Panel
. It's main purpose is not to be some sort of list, but to moreover define how a control (or controls) are presented. If you wish to actually present a collection/list (enumeration) of controls, then you should use a type of ItemsControl
. From there, you can specify the ItemsSource
and customize the ItemsPanel
(as well as the ItemTemplate
, which might be necessary).
Secondly, and this comes up often, is "How do I add static elements to an ItemsSource
that is databound?", to which the answer is to use the CompositeCollection
, and the subsequent CollectionContainer
. In your situation you have two (2) static items (plus more) that you wish to add to your Offices collection. I'm guessing that these "static shapes" are really a substitute to an image of a floorplan.
Here is a sample of what your XAML would look like if you wish to draw your floorplan:
<ItemsControl>
<ItemsControl.Resources>
<CollectionViewSource x:Key="cvs" Source="{Binding Floors}" />
</ItemsControl.Resources>
<ItemsControl.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource cvs}" />
<!-- Static Items -->
</CompositeCollection>
</ItemsControl.ItemsSource>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas ... />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
I'm not sure what each of your objects in your Floor
collection is, but they should not be any type of shape at all. They should be a some object that simply states information about location of the office, color, etc. Here is an example I'm guessing at since you didn't provide what the collection of items was composed of:
// This can (and should) implement INotifyPropertyChanged
public class OfficeViewModel
{
public string EmployeeName { get; private set; }
public ReadOnlyObservableCollection<Point> Points { get; private set; }
...
}
public class Point
{
public double X { get; set; }
public double Y { get; set; }
}
From here you would use a DataTemplate
to translate the object (model/viewmodel) into what it should look like on your view:
<ItemsControl>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Polygon Points="{Binding Points}" Color="AliceBlue" ... />
<DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Of course, if you wish to have multiple representations of what each item looks like from your collection, Offices
, then you'll have to take advantage of the DataTemplateSelector
(which will be set to the ItemsControl.ItemTemplateSelector
property) to select from a set of DataTemplate
s. Here's a good answer/reference to that: https://stackoverflow.com/a/17558178/347172
And finally, one last note... keep everything to scale and your points as types of double
. Personally I would always use the scale 0-1, or 0-100. As long as all your points and static items fit within that bounds, you can stretch out your ItemsControl
to any height/width and everything inside will also adjust and match up just fine.
Update: It's been quite some time and I forgot that the CompositeCollection
class is not a type of FrameworkElement
, so it doesn't have a DataContext. If you want to databind one of of your collections, you must specify a reference to a FrameworkElement
with the desired DataContext:
<CollectionContainer Collection="{Binding DataContext.Offices, Source={x:Reference someControl}}"/>
Update 2: After digging online for awhile, I found a better way to allow databinding to work with the CompositeCollection
, the answer section above has been updated to account for this by using CollectionViewSource
to create a resource bound to the collection. This is much better than using the x:Reference
. Hope that helps.