3

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

Anatoliy Nikolaev
  • 22,370
  • 15
  • 69
  • 68
Elliott Darfink
  • 1,153
  • 14
  • 34
  • I do not think so, because if I reverse the collection (so a different `Polygon` is first), it does not even overlap with the previously displayed `Polygon`. – Elliott Darfink Jul 10 '13 at 12:25
  • where do you set the property "Offices"? – chris6523 Jul 10 '13 at 12:26
  • In my view model constructor, that this canvas is related to (the `Canvas` resides in a `UserControl`, that is loosely coupled with my view model). – Elliott Darfink Jul 10 '13 at 12:31
  • What type of objects are in your `Floor` collection? – myermian Jul 10 '13 at 12:45
  • I do not believe I've mentioned a `Floor` collection, but the `Storage` area is described as a `Polygon`. All items are supposed to be rendered within the `Storage`. If I make the `Storage` `Polygon` invisible, there is still only 1 `Office` (the first in the collection) visible (so the `Storage` does not have a higher z index). – Elliott Darfink Jul 10 '13 at 12:53

2 Answers2

7

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.

Community
  • 1
  • 1
myermian
  • 31,823
  • 24
  • 123
  • 215
  • I couldn't have asked for a better answer. THANK YOU! – Elliott Darfink Jul 10 '13 at 13:42
  • 1
    I've updated my accepted answer to show a better way of using the `CompositeCollection`. I haven't used it in so long I had forgotten the proper way of using it. Hope the updated answer helps and makes it easier to understand. – myermian Jul 11 '13 at 17:06
  • This just saved my life, I couldn't find any good or deep explanation of the ItemsControl, and here I just found it – Tofandel Oct 10 '14 at 19:42
0

Try to set

yourItemsControl.DataContext = Offices;

in code behind.

chris6523
  • 530
  • 2
  • 8
  • 18