Search code examples
c#reporting-servicesms-wordopenxmlrdlc

How to repeat table group header on every page in SSRS(RDLC Report) when outputting to [Word] Documents?


As mentioned in MS Documenation:

Word does not repeat header rows on page two and greater, although you set the RepeatOnNewPage property of the static header row in a tablix (table, matrix, or list) to True. You can define explicit page breaks in your report to force header rows to appear on new pages. However, because Word applies its own pagination to the rendered report exported to Word, results might vary and the header row might not repeat predictably. The static header row is the row that contains the column headings.

  • Repeat Group Header on every page only works on PDF documents
  • Word Documents can only repeat on between group

So how to repeat group headers on every page when outputting RDLC report to Word documents?


Solution

  • This solution is using OpenXML as document processing.

    Who may also find this helpful

    • Output Word Documents with Dynamic Section Header
    • Port SSRS Report to the Word Open XML file types like docm, dotx, dotm...

    Limitations:

    • Only XML formated file is supported (.docx for SSRS), limited by OpenXML
    • Report Size <~= 32MB (text), limited by docx format
    • Increased report generation time cost
    • Additional logic need to be done to header if the same report need to provide .PDF format
    • The table header consume Header Section
    • Alignment issue if fields data cause field to grow

    Assumptions:

    • You already successfully prepared your grouped data in Tablix
    • Page break in between data group has been done

    Summary of the Solution:

    1. Prepare the report using specific identifier that the header appears on every page
    2. Render the report in .DOCX
    3. Prepare your data for replacement
    4. Open the report using OpenXML
    5. Prepare Section Header
    6. Find the Page Break between record group and replace it with Section Break
    7. Bind the section header to each section accordingly
    8. Done

    Key Steps

    1. Prepare the report using specific identifier that the header appears on every page

    Put and align you group header and table header in the Header Section (not inside Tablix)

    Report Design Sample

    [GroupHeaderText1] is the header text you want to repeat and different for every group.

    It will appears on every page in the Word document.

    3. Prepare your data for replacement

    Depends on your report and data structure, prepare your data accordingly.

    Example

    public class SampleData
    {
        public string GroupHeaderField { get; set; }
        public string Data1 { get; set; }
        public string Data2 { get; set; }
        public string Data3 { get; set; }
    }
    

    Part of function for report generation

    ReportViewer rv = new ReportViewer();
    ReportDataSource reportDataSource = new ReportDataSource();
    reportDataSource.Name = "SampleData";
    List<SampleData> dataForReport = GetData();
    reportDataSource.Value = dataForReport;
    rv.LocalReport.DataSources.Add(reportDataSource);
    List<string> lstReplace = new List<string>();
    //Prepare the data
    foreach (SampleData sa in dataForReport)
    {
        if (!lstReplace.Contains(sa.GroupHeaderField))
        {
            lstReplace.Add(sa.GroupHeaderField);
        }
    }
    

    lstReplace now contains the value for dynamic group headers.

    5. Prepare Section Header

    Get the header we prepared at Step 1 as template, we are using it for Section Header

    WordprocessingDocument wordDoc = WordprocessingDocument.Open(docPath, true);
    MainDocumentPart mainDocPart = wordDoc.MainDocumentPart;
    HeaderPart defaultHeaderPart = mainDocPart.HeaderParts.FirstOrDefault();
    

    Now we create header part for each section (group)

    List<string> newSectionHeaderIds = new List<string>();
    foreach (string groupHeaderText in lstReplace)
    {
        HeaderPart newGroupHeaderPart = mainDocPart.AddNewPart<HeaderPart>();
        string sId = mainDocPart.GetIdOfPart(newGroupHeaderPart);
        Header newHeader = (Header)defaultHeaderPart.Header.Clone();
    
        foreach (Paragraph p in newHeader.Descendants<Paragraph>())
        {
            foreach (Run r in p.Descendants<Run>())
            {
                foreach (Text t in r.Descendants<Text>())
                {
                    t.Text = t.Text.Replace("[GroupHeaderText1]", groupHeaderText);
                }
            }
        }
        newHeader.Save(newGroupHeaderPart);
        newSectionHeaderIds.Add(sId);
    }
    

    This code basically

    1. Create a new Header
    2. Save the Id to use later for section binding
    3. Clone the content in template to this header
    4. Replace the identifier with group header text

    To understand better, you should use Open XML SDK 2.5 for Microsoft Office and study a DOCX document structure.

    6. Find the Page Break between record group and replace it with Section Break

    Section Headers are ready. Now create some sections.

    By default, report generated by SSRS has Page Break between groups.

    We just need to replace them.

    Create a SectionProperties template to create sections in Word Document, such that we can have different headers between groups. (You can change PageSize and etc. using this property)

    SectionProperties defaultProperties = mainDocumentPart.Document.Body.Descendants<SectionProperties>().FirstOrDefault();
    defaultProperties.AppendChild(new SectionType { Val = SectionMarkValues.NextPage });
    

    Now find and replace page break

    foreach (Paragraph p in document.Body.Descendants<Paragraph>())
    {
        foreach (Run r in p.Elements<Run>())
        {
            foreach (Break b in r.Descendants<Break>())
            {
                if (b.Type != null)
                {
                    if (b.Type.Value == BreakValues.Page)
                    {
                        b.Remove(); //Remove the page break
                        p.Descendants<ParagraphProperties>().FirstOrDefault().AppendChild(defaultProperties.CloneNode(true)); //Replace by a cloned section break
                    }
                }
            }
        }
    }
    

    Note that properties like page size, width and height is independent for each section, you should set it in Step 5

    7. Bind the section header to each section accordingly

    Remember the newSectionHeaderIds List we created in Step 5? Now we bind them.

    int i = 0;
    foreach (SectionProperties sp in document.Body.Descendants<SectionProperties>())
    {
        //Replace them
        sp.RemoveAllChildren<HeaderReference>();
        sp.PrependChild(new HeaderReference() { Id = newSectionHeaderIds[i], Type = HeaderFooterValues.Default });
        i++;
    }
    

    8. Done

    The most important Step of all... Save it.

    wordDoc.Save();
    

    You should be good to go, let me know it you have questions.