tags:

views:

169

answers:

1

Scope

I am trying to have my application arrange lines of some complex controls and split those lines into pages. Something like this: alt text

Much like a word processor would do, only there are complex controls instead of words. Note that the requirement of splitting into pages is essential.

Also note that the obvious solution of inheriting from Panel and implementing MeasureOverride/ArrangeOverride wouldn't do, because I would like very much to have pages as separate framework elements - this way, I would be able to do some nice things with them, like dragging them around, rotating, etc. I am thinking of this solution (inheriting from Panel) as of last resort, but I'm sure there should be a better way.

Adopted strategy

This seems like something relatively simple, provided I know sizes of those controls. I use vertical-oriented stackpanels for pages, and horizontal-oriented ones for lines. I pack controls into line one by one until they exceed the line's width, and then create stackpanel for the next line and continue. When the next line doesn't fit into current page, I create next page, and the process goes on.

I expect the order of several hundred controls at once, so I figured this approach should do.

The problem

The problem is the very one I would expect from WinForms: I do not know the size of those controls! Turns out, if the control doesn't have Width and Height set explicitly, it will have DesiredSize of zero right after it is created.

This problem can be solved by calling .Measure( Size.Empty ). After that, DesiredSize is calculated (somewhat) correctly.

I say "somewhat" - because it is still not entirely correct, unfortunately. The latest problem I've found is with bindings to relative source. Consider the following XAML:

<ItemsControl x:Class="MyClass" xmlns=...>
    <ItemsControl.ItemTemplate>
        <Ellipse Width={DataBinding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type MyClass}}, Path=EllipseWidth} />
    </ItemsContro.ItemTemplate>
</ItemsControl>

Where "EllipseWidth" is a property on MyClass, which is my custom ItemsControl descendant.

Turns out, a call to Measure() causes the ItemsControl to create its items, but the binding doesn't get resolved. Check out the trace:

52 : Created BindingExpression (hash=15030582) for Binding (hash=15006601)
54 :   Path: 'EllipseWidth'
56 : BindingExpression (hash=15030582): Default mode resolved to OneWay
57 : BindingExpression (hash=15030582): Default update trigger resolved to PropertyChanged
58 : BindingExpression (hash=15030582): Attach to Ellipse.Width (hash=12274123)
62 : BindingExpression (hash=15030582): RelativeSource (FindAncestor) requires tree context
61 : BindingExpression (hash=15030582): Resolve source deferred

Specifically, note the last two lines. What is a "tree context" and how do I provide it? I tried to add the control to a "live" panel before calling Measure(), but it didn't help much.

I must say that, according to the trace, the source does get resolved eventually, but that's too late: I need to know the control size now.

Generalizing the problem

Even if I resolve this binding problem, I cannot be sure that there won't be any others. So the ultimate question is: I need a "clean" (as in "supported") way to know when a control's initialization is completely done, and the to find the size of a that control

Or (another option) maybe I'm taking the wrong approach, and there is another, cleaner, way to do what I'm trying to do?

A: 

I don't think you're going to be able to accomplish what you want just using control composition. I think you will definitely need to participate in the Measure/Arrange and provide your own algorithm. You don't have to inherit from Panel to do this, but it usually makes the most sense. As for the pages, your custom Panel can emit a backing for each logical grouping. Finally your custom Panel can be configured with the dimensions of what constitutes a logical grouping so that you can support different sizes (imagine supporting Letter vs. A4 or something like that).

So in the end what I think you should end up with is:

  1. A custom Panel implementation that supports laying things out and determining logical groupings based on dimensions configured via various properties.
  2. Expose a property called something like ItemGroupTemplate which, as you generate logical groupings, you use to render as the "background" of the logical grouping effectively creating the "page" they're on.
  3. Bind your actual item data to an ItemsControl
  4. Set your custom Panel as the ItemControl::ItemsPanel

Finally, keep in mind that you're probably going to want to use a virtualizing algorithm in your panel if you're going to have that many items to layout. That means you should inherit from VirtualizingPanel. For more on creating a custom VirtualizingPanel, check out Dan Crevier's awesome series on the subject.

Drew Marsh
Well, this is exactly the "last resort" solution I was keeping as a backup. If nobody gives another answer, I will certainly go down this path... But this way, I loose the ability to [easily] treat pages as separate framework elements - to rotate them, move around, "fold", etc...
Fyodor Soikin