TechSpoken

"Any ideas?" is the most frequently-asked question in technical forums. My answer is: yes.

YAPS about Dynamic Layouts

Ehud Kafri wrote on a moribund thread in the SQL Server RS  forum to ask:

Hi Lisa, 
I have a simelar problem, I have a list item that recive a collection of objects that were made specificly for this report.
the problem is i need the size and location of the textboxs to be decided according to the parameters defind on each object in the collection.
ex' : i have:     String ValueForDisplay
int Position   // enum value like : Top; NewRow; SameRow and so on
Double Width
Double Hieght
now what I could not figure out is how to, in run time, set these values.
I prefer not to dynamicly create a new rdlc file on each run but so far that is the only way I have found to do this.
I'm using VS 2005, C#, .Net 2.0
Thanks

After some back-and-forth, some things became clear.

The problem Ehud is facing is this: some things can be dynamically set on a report object, such as font and color, and some cannot.  Top and Left, Width and Height, are among the things that cannot be set dynamically for each instance of each object during a report run.

Why not, and what can we do about it?

First, understand why not

Think about the ramifications of setting these particular attributes on the fly, on the report layout as a whole.  When a textbox moves to a new position, for example lower on the page, what should happen to the other elements "below" it? 

You could answer "I'm going to set the top and left for all of them, too, so what's the problem?" Maybe you are, and maybe you aren't, I think Ehud is not doing that, but if you are, I don't really see the point of using the RDL to render your report any more.  You might as well call the rendering engine of your choice (be it PDF or whatever) directly, or just write your own, rendering items within a canvas of the appropriate page layout size however you want to.

Part of the point of the RDL data regions (whether we're talking table, matrix, tablix, or list) is to allow the report engine to iterate through repeating items and figure out the positioning and the pagination itself.  To a certain degree, you can issue runtime instructions to it that will force dynamic behavior, for example, you can use "Can shrink" and "Can grow" so that, should the content item have different content on each row.  When you do this, if there is also content below the item that's shrinking and growing, this other content will move appropriately too.

I'll have more to say on how you can use those properties to your advantage at some other point, because I can already tell I'm not going to get to the end of this topic in one "go". I have mentioned it in other contexts, such as building a strange report to support RDLDocumenter, in the past.

This is the magic of any data-handling rendering construct, whether it's a repeater in ASP.NET, an HTML table, or a data-region in an RDL.  When you seek to interfere with this behavior -- by setting the top, left, height, and width values yourself -- you're really asking for trouble.   You're not giving the RDL a chance to do what it was designed to do.

Or, we could talk some more

That being said...  Ehud's situation is a little different.  He's not trying to re-do the dimensions and positions of every textbox in his form.  He's trying to make one report serve for many different customers with some variation -- a classic "corporate stationery" problem.  Some people like their header to the left of their corporate logo, some people want it to the right.  Some people want the address of the recipient in a different place than others, because they have different window envelopes.

Ehud's case is a little more business-domain specific than that, but, when you put it in the proverbial nutshell, it's a situation we have all experienced.  So, how do we crack this proverbial nut?

Ehud uses an object data source, which I won't attempt to reproduce here. I don't have a lot of experience with that type of thing, but I imagine that each row of his object dataset represents one instance of an object, with various attributes of various items to be displayed -- such as font and position information -- available for that row. 

For my attempt to reproduce his situation, I'm going to torture some sales data to give me some random results that represent instructions for one textbox and one image, such as you might have in "corporate stationery". 

SELECT
   DAY (Requested_Ship ) * 6 As [Text1Left],
   (
Month(Requested_Ship) %6 ) * 10 AS [Text1Top] ,
   ID + CHAR(13)+CHAR(10) + Revenue_Type + CHAR(13)+CHAR(10) + Juris AS [Text1Content],
   CASE 
     
WHEN ID LIKE '%w%' THEN 'Red' 
      WHEN ID LIKE '%5%' THEN 'Green' 
     
ELSE 'Blue' END AS [Text1FontColor],
   CASE
     
WHEN Sales_No LIKE '%3%' THEN '10'
     
ELSE '16' END AS [Text1FontSize],
   CAST( RIGHT(Sales_No,2) AS INTEGER) * 3 AS [Image1Left] ,
   (
DAY(Released_To_Mfg) % 20) * 10 AS [Image1Top],
   CASE
     
WHEN DAY(Requested_Ship) % 2 = 0 THEN 'gear'
     
ELSE 'leaf' END AS [Image1Content],
   25
AS [Image1Width]
FROM OrderHeader  

The resulting (silly) data looks like this:

Text1 Left

Text1 Top

Text1 Content

Text1 Font Color

Text1 Font Size

Image1 Left

Image1 Top

Image1 Content

Image1 Width

120

0

1-63ABO  Sales  ARGE

Blue

10

132

30

gear

25

126

10

1-714CI  Loan  NVDA

Blue

10

183

30

leaf

25

60

0

1-63D3L  Conversion 

Blue

10

123

30

gear

25

36

0

1-63D59  Conversion 

Green

10

3

90

gear

25

42

0

1-63D6X  Participation 

Blue

10

183

140

leaf

25

168

30

1-KMHWI  Sales  MSST

Red

10

213

20

gear

25

120

0

1-644TS  Loan  NVDA

Blue

16

228

90

gear

25

To create the right conditions for applying the above values, we'll follow these principles:

  1. Determine the lowest values that top and left are allowed to have for each movable report item.
  2. Create a suitable layout control (in this case, one image and one textbox) for each item at that position.
  3. Consider that top/left as 0,0, and the "real" top left as a pair of offsets for that item.
  4. Similarly, determine the highest values that width and height are allowed to have for each item.  Make those values the actual width and height for the item.

Don't worry that this strategy may overlay various layout controls on top of each other. 

In my example, I've created a list in the layout (because that's what Ehud is using).  You can see it as a red border in the screenshot of the Report Designer below.  I've created an image control and a text box that each uses almost the entire space of the list; that is, each item is allowed to be anywhere in the list. 

I've staggered their positions slightly, and added a black rectangle inside the list that contains both the image and the text box, for the screenshots; otherwise you would not be able to see both controls at all; they could occupy exactly the same space in the layout if you want. Add any other, non-movable, controls wherever they are supposed to be, overlaying them on these "max sized" items as needed.

If Ehud wanted to use the RS Excel output, we couldn't pull this trick; overlaid objects are murder in the standard RS Excel. Of course you can use XSLT to fix that, as we have done in the past. For PDF and HTML output, as well as image file exports, this will work fine.

Now comes the cute trick

My painting teacher used to say "draw the negative space".  And that's what we have to do here.  

We can't adjust the positional and size attributes using expressions, but we can tell the RDL to reserve space in the layout, where the layout controls may be rendered.  That's what we've done so far.

Now all we have to do is subtract the part of this reserved space in which we don't want to render our content, using Padding properties, which we can, indeed, supply as expressions.

  • For our textbox, Left Padding will be this expression: = CSTR(Fields!Text1Left.Value)  & "pt" and for our image, Left Padding will be = CSTR(Fields!Image1Left.Value) & "pt".
  • Obviously, the textbox's Top Padding is =CSTR(Fields!Text1Top.Value) & "pt" and the image's is =CSTR(Fields!Image1Top.Value) & "pt" . 
  • Note that you may not need the CSTR conversion but I like to put it in.
  • Width and Height can be handled, similarly, using Right and Bottom Padding. For our example image, I've chosen the size characteristic FitProportional, because I've chosen two different images with very different aspect ratios.  I used an expression, = CSTR(300-(Fields!Image1Left.Value +Fields!Image1Width.Value)) + "pt", as Right Padding in this case, where 300 is the max width of the image control in the layout. 
    In this expression, I'm subtracting the actual left value and the expected width of the image from the full possible width of the image. 
    What remains is what I want to take away on the right side.

Both the image and the textbox also use a "content" expression derived from a field, and the textbox uses font and color expressions derived from fields as well.  This is garden-variety stuff, but I've done it to show how (I think) Ehud is using object attributes already. 

Here's the result (showing in a PDF thumbnail view:

Is that all she wrote?

That's enough for now.  Is it the right way to solve every dynamic layout problem?  Definitely not. 

Instead of thinking about padding and negative space, you can try an approach that I like to call "the report as a piece of graph paper".  The layout looks something like this:

As you can probably tell, it works by dividing up the whole area into blocks.  Then each block has a UDF that goes to work figuring out if any of the current report instance's contents should start there.   (The arguments for the UDF in this example are row, column).

All the textboxes start at a different spot, but they can all end at the same "right edge", if you want, so you can combine this approach with using right padding to handle the textboxes' actual width.  You use "Can Grow" on the textboxes to allow them stretch vertically, if the textboxes are inside a row of a table.  This will push down the next row.  In general, you get a lot of flexibility using a table-oriented layout even though your report seems superficially to be a free-form label or list-style layout.

In Ehud's case I think there are more possibilities.  The graph paper layout above is going to have a UDF that interrogates the data like this:  Does any item have a "left" and "top" value that should be rendered in the current textbox position?  If so, return that content, otherwise return "".  But Ehud could also put his business logic into UDFs, and come up with a specialized UDF for his purpose,and even for each specialized textbox for example:  Is this customer of type "A" or type "B"?  If type "A", return the customer's name in this position.  Otherwise, return a greeting.   Other requirements might be taken care of by a dynamic sort expression for a list or table -- has the user chosen layout variant "X" or "Y"?  If so, then all records of type "Z" come first, otherwise, all rows should be in alphabetical by last name.  Dynamic ordering is easy to take care of with IIF, usually, and can evaluate a UDF too.

What I've just suggested doesn't include any fussing about positions at all.  In fact, I think it is probably unnecessary in majority of cases. Instead of messing around with layout control positions, dynamically adjust what shows in each layout control -- we have a lot of leeway in doing this -- and you'll come out ahead.  This is not really what Ehud asked me to do, but IMHO it's still the right way to get where he's trying to go.

I can continue this discussion, and elaborate, if needed, and if anybody wants to give me the dynamic data scenario they have in mind.