WPF Multipage Reports Part III Rendering

By Jan Repisky at June 18, 2008 15:18
Filed Under:

After some time I'm back to continue my WPF reporting post series. I have started with the overall architecture post , then I dived into a data preparation part. Today I will switch back to WPF and describe a report definition and a report rendering part.

Why have I decided to use  flow documents for report creation? . The reason is obvious, they do support automatic pagination. The only problem is that you have to use the controls that are suitable for flow documents. In reality it means you are confined to the System.Windows.Documents namespace. Of cource you can host Grid , StackPanel, ListBox or other familiar ItemsControls inside FlowDocument using the BlockUIContainer , but you loose pagination support. These standard layouou controls do not support pagination and a ListBox will be rendered out of page boundaries.

My solution will use mainly standard Table, Paragraph and Run controls. Table as a layout control, Paragrpah and Run for displaying data. A small isuue is they do not support standard databinding and no "Itemtemplate" (like in Listview) is available.

Using Databinding  for pure data display with simple formatting is something I can live without . Maybe it is even undesired to use powerfull WPF databinding for a simple task like "printf" ;-). The result will use less memory and will be faster.

The templating would be usefull. There are repeating parts  like headers and footers and of course the item detail part. I need to handle the templating-like behaviour  during report generation , so my report defininition contains  properties for template definitions as a string with Xaml content.

Here is the report definition class with inner parts for page layout and inner groups:

  public class ReportDefinition
    {
        string headerTemplate;

        public string HeaderTemplate
        {
            get { return headerTemplate; }
            set { headerTemplate = value; }
        }

        PageDefinition page;

        public PageDefinition Page
        {
            get {
                if (page == null)
                    page = new PageDefinition();

                return page; }
            set { page = value; }
        }       

        string footerTemplate;

        public string FooterTemplate
        {
            get { return footerTemplate; }
            set { footerTemplate = value; }
        }

       
        string itemTemplate;

        public string ItemTemplate
        {
            get { return itemTemplate; }
            set { itemTemplate = value; }
        }

     
        string tableDefinition;

        public string TableDefinition
        {
            get { return tableDefinition; }
            set { tableDefinition = value; }
        }

      

        List<GroupDefinition> groups;

        public List<GroupDefinition> Groups
        {
            get 
            {
                if (groups == null)
                    groups = new List<GroupDefinition>();

                return groups; 
            }
          
        }
    }

    public class GroupDefinition
    {
        string headerTemplate;

        public string HeaderTemplate
        {
            get { return headerTemplate; }
            set { headerTemplate = value; }
        }



        string footerTemplate;

        public string FooterTemplate
        {
            get { return footerTemplate; }
            set { footerTemplate = value; }
        }

        bool newPageOnGroupBreak;

        public bool NewPageOnGroupBreak
        {
            get { return newPageOnGroupBreak; }
            set { newPageOnGroupBreak = value; }
        }

        bool resetPageNumberOnGroupBreak;

        public bool ResetPageNumberOnGroupBreak
        {
            get { return resetPageNumberOnGroupBreak; }
            set { resetPageNumberOnGroupBreak = value; }
        }

    }

    public class PageDefinition
    {
       Size margin;

        public Size Margin
        {
            get { return margin; }
            set { margin = value; }
        }

        string headerTemplate;

        public string HeaderTemplate
        {
            get { return headerTemplate; }
            set { headerTemplate = value; }
        }

        double headerHeight;

        public double HeaderHeight
        {
            get {
                if (headerHeight < 0) 
                    return 0;
                return headerHeight; }
            set { headerHeight = value; }
        }

        double footerHeight;

        public double FooterHeight
        {
            get { 
                if (footerHeight<0)
                    return 0;
                return footerHeight; }
            set { footerHeight = value; }
        }


        string footerTemplate;

        public string FooterTemplate
        {
            get { return footerTemplate; }
            set { footerTemplate = value; }
        }


    }
 

Templates are loaded during a report generation using XamlLoader.Load. I didn't find better solution for replicating the content for each TableRow in a Table.

The report engine class is responsible for rendering the report content. During report generation the following occurs

  1. Empty FlowDocument and TableRowGroup

    are created.
  • The rows for ReportHeader are added to the TableRowGroup.

  • For each group the group headers, footers and data are added to the TableRowGroup.

  • The rows for ReportFooter are added to the TableRowGroup.

  • A Table is created and the TableRowGroup is added to the RowGroups collection. !!! This is important to do at the end , becasu otherwise the perfromance will decrase for bigger reports.

  • The Table is added to the FlowDocument.

  • Custom report paginator is creatd  for managing the page header and footer.

  • XpsSerializationManager creates XpsDocument..

  •  

         public class ReportEngine
        {
            static List<FormattedRun> displayItems;
    
            static ParserContext xamlContext;
            /// <summary>
            /// Helps in namespace mapping during Xaml loading for each template
            /// </summary>
            public static ParserContext XamlContext
            {
                get
                {
                    if (xamlContext == null))
                    {
                        xamlContext = new ParserContext();
                        xamlContext.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
                        xamlContext.XmlnsDictionary.Add("c", "http://reportcontrols");
                    }
                    return xamlContext;
                }
            }
    
            /// <summary>
            /// Creates report part according to specific template , and "binds" the data 
            /// </summary>
            internal static T  createReportPart<T>(string template,object data) where T:TextElement
            {
                T templatedItem;
             
                MemoryStream ms = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(template));
                templatedItem = (T)XamlReader.Load(ms, XamlContext);
                if (data != null)
                {
    
                    DocumentWalker dw = new DocumentWalker();
                    dw.VisualVisited += new DocumentVisitedEventHandler(dw_VisualVisited);
                    displayItems = new List<FormattedRun>();
                    if (typeof(T) ==typeof(Paragraph))
                        dw.TraverseParagraph(templatedItem as Paragraph);
                    else if (typeof(T) == typeof(Section))
                    {
                        Section sec = templatedItem as Section;
                        foreach (Block b in sec.Blocks)
                        {
                            Paragraph p =b as Paragraph;
                            if (p!=null)
                                dw.TraverseParagraph(p);
                        }
                    }
                    else if (typeof(T) == typeof(TableRow))
                    {
                        TableRow tr = templatedItem as TableRow;
                        foreach (TableCell tc in tr.Cells)
                            dw.TraverseBlockCollection(tc.Blocks);
                    }
                    else if (typeof(T) == typeof(TableRowGroup))
                    {
                        TableRowGroup trg = templatedItem as TableRowGroup;
                        foreach (TableRow tr in trg.Rows)
                        {
                            foreach (TableCell tc in tr.Cells)
                                dw.TraverseBlockCollection(tc.Blocks);
                        }
                    }
    
                    foreach (FormattedRun fRun in displayItems)
                    {
                        if (data is DataRow)
                        {
                            DataRow row=data as DataRow;
                            if (row.Table.Columns.Contains(fRun.PropertyName))
                                fRun.Data = row[fRun.PropertyName];
                        }
                        else if (data is GroupData)
                        {
                            GroupData gd = data as GroupData;
                            fRun.Data = gd.GetComputedValue(fRun.PropertyName);
                        }
                    }
                }
    
                return templatedItem;
            }
    
            static void dw_VisualVisited(object sender, object visitedObject, bool start)
            {
                FormattedRun fRun = visitedObject as FormattedRun;
                if (fRun != null)
                    displayItems.Add(fRun);
            }
    
    
            void copyFromRowGroup(string template, TableRowGroup trg,GroupData r)
            {
                TableRowGroup gf = createReportPart<TableRowGroup>(template, r);
                TableRow[] rows = new TableRow[gf.Rows.Count];
    
                gf.Rows.CopyTo(rows, 0);
                gf.Rows.Clear();
    
                for (int j = 0; j < rows.Length; j++)
                {
                    TableRow tr = rows[j];
                    trg.Rows.Add(tr);
                }
            }
           
            void addGroup(ReportDefinition rd, TableRowGroup trg, GroupData group,DataTable rData)
            {
               copyFromRowGroup(rd.Groups[group.Level].HeaderTemplate, trg, group);
                if (group.HasNestedGroups)
                {
                    foreach (GroupData g in group.NestedDataGroups)
                        addGroup(rd,trg,g,rData);
                }
                else
                {
                    int endRow=group.StartRow+group.Count;
                    for (int i=group.StartRow;i<endRow;i++)
                    {
                        DataRow r = rData.Rows[i];
                        trg.Rows.Add(createReportPart<TableRow>(rd.ItemTemplate, r));
                    }
                }
                copyFromRowGroup(rd.Groups[group.Level].FooterTemplate,trg,group);
    
            }
         
            public XpsDocument  CreateReport(ReportDefinition rd, ReportData rData)
            {
                int pageWidth = 700;//just for testing !! get it from your printer
                FlowDocument fd = new FlowDocument();
                fd.ColumnWidth = pageWidth - 100;
                fd.Blocks.Add(createReportPart<Section>(rd.HeaderTemplate,rData.ReportGroup));
                TableRowGroup trg = new TableRowGroup();
               
                for (int i = 0; i < rData.Groups.Count;i++ )
                {
                    addGroup(rd,trg, rData.Groups[i],rData.Rows);
                }
                MemoryStream tableStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(rd.TableDefinition));
                Table table = (Table)XamlReader.Load(tableStream,XamlContext);
                table.RowGroups.Add(trg);
                fd.Blocks.Add(table);
                fd.Blocks.Add(createReportPart<Section>(rd.FooterTemplate, rData.ReportGroup));
                return ReportPaginator.CreateXpsDocument(fd,rd.Page); 
    
            }
        }

    For report definition I will create a descendant class from standard Run , that supports simple formatting:

         /// <summary>
        /// Implements support class for formating data to various text outputs. E.g. dates number formats, etc.
        /// </summary>
        public class FormattedRun : Run
        {
    
            object data;
    
            public object Data
            {
                get { return data; }
                set { data = value; formatText(); }       }       
            string format;
    
            public string Format
            {
                get { return format; }
                set { format = value; }
            }
    
            string propertyName;
    
            public string PropertyName
            {
                get { return propertyName; }
                set { propertyName = value;  }
            }
            }
    
          
            void formatText()
            {
                if (data != null )
                {
                    if (format != null)      {
                        if (data.GetType() == typeof(DateTime))
                        {
                            DateTime dt = Convert.ToDateTime(data);
                            this.Text = dt.ToString(format);
                            return;
                        }
                        else if (data.GetType() == typeof(Decimal))
                        {
                            Decimal dt = Convert.ToDecimal(data);
                            this.Text = dt.ToString(format);
                            
                            return;
                        }
                    }
                    else
                        this.Text = data.ToString();
                }
                else
                    Text = "";
            }
        }

    Assume you have your report definition in one flowdocument like this

    Report1

    That is a flowdocument containing ale the necessary templates for report generation. The Xaml definition is:

    <FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
        <Section Name="pageHeader">
            <Paragraph>Page Header</Paragraph>
        </Section>
        <Section Name="reportHeader">
            <Paragraph>
                ReporttHeader definition
            </Paragraph>
        </Section>
        <Table CellSpacing="5">
                <Table.Columns>
                    <TableColumn Width="100"/>
                    <TableColumn Width="100"/>
                    <TableColumn Width="150"/>
                </Table.Columns>
            <TableRowGroup Name="Group_1_Header">
                <TableRow>
                    <TableCell ColumnSpan="3">
                        <Paragraph FontWeight="Bold">Product:
                            <c:FormattedRun PropertyName="Project"/>
                        </Paragraph>
                    </TableCell>
                </TableRow>
                <TableRow>
                    <TableCell>
                        <Paragraph >Created</Paragraph>
                    </TableCell>
                    <TableCell>
                        <Paragraph TextAlignment="Right">Amount</Paragraph>
                    </TableCell>
                    <TableCell>
                        <Paragraph TextAlignment="Right">Tax</Paragraph>
                    </TableCell>
                </TableRow>
                <c:RowLine ColSpan="3"/>
            </TableRowGroup>
    
            <TableRowGroup Name="ItemDetail">
                <TableRow>
                    <TableCell >
                        <Paragraph>
                            <c:FormattedRun PropertyName="Created" Format="dd.MM.yyyy"/>
                        </Paragraph></TableCell>
                    <TableCell >
                        <Paragraph TextAlignment="Right">
                            <c:FormattedRun PropertyName="Amount"/>
                        </Paragraph>
                    </TableCell>
                    <TableCell >
                        <Paragraph TextAlignment="Right">
                            <c:FormattedRun PropertyName="Tax"/>
                        </Paragraph>
                    </TableCell>
                </TableRow>
            </TableRowGroup>
            <TableRowGroup Name="Group_1_Footer">
                <c:RowLine ColSpan="3"/>
                <TableRow>
                    <TableCell ColumnSpan="3" TextAlignment="Right">
                        <Paragraph TextAlignment="Right">
                            Total:<c:FormattedRun PropertyName="Tax"/>
                        </Paragraph>
                    </TableCell>
                </TableRow>
                <TableRow>
                    <TableCell ColumnSpan="3"></TableCell>
                </TableRow>
            </TableRowGroup>
    
        </Table>
        <Section Name="reportFooter">
            <Paragraph>Report Footer</Paragraph>
        </Section>
        <Section Name="pageFooter">
        <Paragraph>Page Footer</Paragraph>    
            <Paragraph TextAlignment="Right">
                Page @PageNumber from @PageCount
            </Paragraph>
        </Section>
    </FlowDocument>

    Each named section represents a template that gets replicated for each corresponding part during report generation.
    In this sample for example the report header template is :  "<Paragraph>  ReporttHeader definition  </Paragraph>"
    As a result of rendering we get

    this report

    .

    In my next post I will focus on the DocumentWalker and ReportPaginator classes.

    kick it on DotNetKicks.com

    Comments

    11/16/2010 1:17:49 PM #

    Puma shoes

    I'm just surprised to discover how a lot of details I received on this specific topic. I m so really thankful of you. A single issue I could assert that, after reading this put up I became preserved from the whole ineffective search I ought to have performed on this particular matter. Your write-up is really a authentic fantastic point in disguise.

    Puma shoes Slovakia |

    11/25/2010 4:11:45 PM #

    Nike Shox Clearance

    I'm just surprised to discover how a lot of data I received on this specific topic. I m so extremely thankful of you. 1 factor I could assert that, after reading this article I became preserved from the whole ineffective search I ought to have performed on this particular matter. Your write-up can be a authentic fantastic factor in disguise.

    Nike Shox Clearance United States |

    11/26/2010 7:34:05 PM #

    windows 7 professional

    I read lots of weblogs lately and your own is one of the best. I like reading through you that obvious as well as well crafted. Your web page goes right to my personal bookmarks. I acquired a few nice inspirational thoughts reading this.

    windows 7 professional United States |

    Comments are closed

    About the author

    I have started writing code in 1986 and since 1993 I'm working on application frameworks and solution architecture. I worked for five years for Microsoft Consulting Services. Currently I live in Prague.

    I work now as a Solution Architect at Gradual Systems.

    Page List