views:

190

answers:

2

I'm trying to create a View in WPF and having a hard time figuring out how to set it up. Here's what I'm trying to build:

  • My ViewModel exposes an IEnumerable property called Items
  • Each item is an event on a timeline, and each one implements ITimelineItem
  • The ViewModel for each item has it's own DataTemplate to to display it
  • I want to display all the items on the timeline connected by a line. I'm thinking a WrapPanel inside of a ListView would work well for this.
  • However, the height of each item will vary depending on the information it displays. Some items will have graphic objects right on the line (like a circle or a diamond, or whatever), and some have annotations above or below the line.

So it seems complicated to me. It seems that each item on the timeline has to render its own segment of the line. That's fine. But the distance between the top of the item to the line (and the bottom of the item to the line) could vary. If one item has the line 50 px down from the top and the next item has the line 100 px down from the top, then the first item needs 50 px of padding so that the line segments add up.

I think I could solve that problem, however, we only need to add padding if these two items are on the same line in the WrapPanel! Let's say there are 5 items and only room on the screen for 3 across... the WrapPanel will put the other two on the next line. That's ok, but that means only the first 3 need to pad together, and the last 2 need to pad together.

This is what's giving me a headache. Is there another approach I could look at?

EDIT: I'm leaning towards replacing the WrapPanel with a custom panel that inherits from Canvas and overrides the MeasureOverride and ArrangeOverride methods.

+2  A: 

The ideal solution to this would probably be using Adorners. On the AdornerLayer you would have a custom Adorner for each item on the Panel (you define it in the DataTemplate). You will be able to retreive dimensions an positions of each item (Boundingbox of the Adorner). Also on the AdornerLayer you would then draw the lines between these boundingboxes. Using this solution you can layout your items any way you want (using any Panel you want) and the items will still be connected with lines.

A long time ago I used this approach for a graph visualizer. The nodes where arbitary UIElements that could be layouted in any way. The items where connected with lines.

To make all items align perfectly you could use a Canvas as the root Panel in you ItemTemplate. The Canvas can be 0x0 units big. You would then arrange your elements around that Canvas. The Canvas becomes your point of reference. Everything on top of the line will get negative Canvas.Top values. Another approach would be to use negative margins that are bound to the height of the top and bottom controls that surrond the line. Use a IValueConverter (Height*-1) to invert the Height.

bitbonk
That's definitely a useful solution for drawing the lines. Thanks! But I'm still looking for a way to arrange the items so that that they all line up where they should. The lines all be horizontal and straight, so it's the items that have to move to compensate.
Scott Whitlock
Scott Whitlock
I updated my post with a suggestion to solve your alginment problem.
bitbonk
+1  A: 

This won't solve your problem, but I bet it will simplify it:

Try defining a DataTemplate that looks something like this:

<Grid>
   <Grid.RowDefinitions>
      <RowDefinition Height="Auto" SharedSizeGroup="Top"/>
      <RowDefinition Height="10"/>
      <RowDefinition Height="Auto" SharedSizeGroup="Bottom"/>
   </Grid.RowDefinitions>
   <ContentControl Grid.Row="0" Content="{Binding TopAnnotations}/>
   <Rectangle Fill="Black" Grid.Row="1" Margin="0,4,0,4"/>
   <ContentControl Grid.Row="1" Height="10" Content="{Binding GraphicObject}"/>
   <ContentControl Grid.Row="2" Content="{Binding BottomAnnotations}"/>
</Grid>  

Now create an ItemsControl whose ItemsPanelTemplate is a horizontal WrapPanel with the Grid.IsSharedSizeScope attached property set to True. You will of course also need to define data templates for whatever the annotation and graphic object properties contain.

You still have the problem that when the items in the WrapPanel wrap, the objects above and below the timeline have the same padding irrespective of how much they actually need to fit on their current row of the WrapPanel. So to get the effect that I think you're looking for, you still have to monkey around with measure and arrange - basically, instead of creating a single WrapPanel, you'll create something that puts these items into a StackPanel on each "row", each of which is its own shared-size scope. But actually measuring and arranging the heights of the timeline items is not something you need to worry about. You only need their widths (so that your logic can determine when to wrap). The Grid will take care of calculating the heights for you.

Robert Rossney
Interesting idea... thanks! It restricts things a bit in that all the graphics have to be the same size (in the middle) so if one used a graphic that was twice as big, all the annotations on the other ones in the same line wouldn't be right up against the graphic. What if, for instance, one item only had a big graphic, and no annotations? As I said, it's a bit more restrictive than I was thinking, but it's an interesting approach.
Scott Whitlock
Actually, you can make the middle column auto-sized and have it belong to its own `SharedSizeGroup`. It'll still work; you just have to give the `Rectangle` or whatever you're using to produce the line a `VerticalAlignment` of `Center`. It'll be centered vertically in the row, and the row will be sized to the height of the biggest graphic it contains.
Robert Rossney