Search code examples
c#wpfxamlcanvasitemscontrol

ItemsControl and Canvas - only the first item is visible


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...


Solution

  • 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 DataTemplates. 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.