Dealing with the Report Server Report Rendering Object Model

After creating a working stub of a Custom Rendering Extension, it was time to add the actual functionality. In order to do so, the Report Server Object Model used for Report Rendering has to be understood. In this blog post, I want to share some insights gained during this process. It will hopefully help to speed up the process of getting in touch with the Report Server Object Model, but isn't a replacement for existing reference and the need of digging deeper by yourself.

Namespaces

Depending on the Type of Reporting Extension (Processing, Rendering, Delivery etc.), different Namespaces are used. The Types itself are similar, but not equal. E.g. the Processing Extension uses the Microsoft.ReportingServices.RdlObjectModel-Namespace, containing details for the Report Parameter Layout - irrelevant for Report Rendering and therefore missing in the Microsoft.ReportingServices.OnDemandReportRendering-Namespace.

This Blog Post is focused on the Report Rendering Object Model. Nevertheless, it is useful to identify the corresponding element and structure in the Report Definition and then have a look after the corresponding elements in the Rendering Object Model.

E.g. Table => Tablix-Type - SSDT and RDL-File:

Type Hierarchies & Structure

Report Structure

The overall report is structured into Report Sections, containing Report Items in their Report Body:

Report Item Hierarchy

Report Items are the visual elements, dragged into a report. The Type for Table, Matrix and List is all Tablix. Also Data Bar and Chart are both of Type Chart.

Report Element Hierarchy

Report Element is the Base Type of Report Item with some other derived Types used for internal Report Structure.

Basic Type Structure

There are two kind of types, the ones for Report Definition, containing the uncalculated expressions, e.g. "=Fields!xyz.Value", and the ones with the actual calculated values. The latter usually have the Type-Postfix "Instance", e.g. "TextBox" vs. "TextBoxInstance". The Types for actual instances are much more differentiated than the definition ones in order to differentiate between static and dynamic ones, e.g. in case of a table, header columns are static and data bound columns are dynamic - requiring the row-by-row processing of the underlying data.

The following three types are the base Report Definition Types for many other types in the Report Object Model, containing some basic information of all elements:

Dynamic Instances / Row-Processing

If an element is bound to a dataset, the instance contains the value of the current row. To get the values of all rows, they have to be processed.

Common methods, e.g. according to TablixDynamicMemberInstance:

  • void ResetContext() => Reset before first instance
  • bool MoveNext() => Move to next instance, return true if successful
  • int GetInstanceIndex() => Index of current instance
  • bool SetInstanceIndex(int index) => Move to specific instance, return true if successful

Types supporting these methods:

  • ChartDynamicMemberInstance
  • DataDynamicMemberInstance
  • GaugeDynamicMemberInstance
  • MapDynamicMemberInstance
  • TablixDynamicMemberInstance

To process all instances, simply call ResetContext() and then process each instance inside a while(instance.MoveNext()). Depending on the complexity of the Report Item, several nested processings may be required to get all data, e.g. in a Tablix with dynamic rows and columns.

Understanding Tablix and Chart

Tablix and Chart are the common "more complex ones" of the Report Items.

Tablix

A tablix may contain several row- and column-hierarchies. Each of them may contain several TablixMember, which may be nested. To get a look on all items in SSDT, switch to Advanced Mode:

Depending on the nature of the Tablix Member - either static or dynamic (Property TablixMember.IsStatic), its instance is either of Type TablixMemberInstance or TablixDynamicMemberInstance.

Matrix-Example

Report Definition:

Report Preview:

Diagnostics Output:

 - Tablix1: Type = Tablix 
, Rows = 1, Columns = 1 
=== Row Hierarchies === 
 - 0: isStatic = False 
=== Column Hierarchies === 
 - 0: isStatic = False 
=== Stats: dynamic rows = 1, columns = 1 === 
=== Tablix Data === 
Data in format row hierarchy | column hierarchy | row recursion level | column recursion level | dataRowCounter-Prefix: val-1, val-2 ... 
0 | 0 | 1 | 0 | /1/1:458, 487, 498, 274 
0 | 0 | 1 | 0 | /1/2:1.159.731, 1.175.475, 1.201.006, 646.509 
0 | 0 | 1 | 0 | /2/1:342, 395, 416, 167 
0 | 0 | 1 | 0 | /2/2:778.424, 969.668, 962.583, 381.662 
0 | 0 | 1 | 0 | /3/1:350, 335, 367, 127 
0 | 0 | 1 | 0 | /3/2:859.417, 779.221, 804.262, 351.505 
0 | 0 | 1 | 0 | /4/1:229, 251, 296, 139 
0 | 0 | 1 | 0 | /4/2:514.022, 617.589, 757.623, 410.201 
0 | 0 | 1 | 0 | /5/1:1.027, 1.115, 1.248, 488 
0 | 0 | 1 | 0 | /5/2:2.448.470, 2.685.292, 2.888.463, 1.128.373 
0 | 0 | 1 | 0 | /6/1:454, 571, 643, 258 
0 | 0 | 1 | 0 | /6/2:1.161.575, 1.330.468, 1.540.239, 586.586

Code to get the Diagnostics Output:

 public static void DebugItem(Tablix tablix)
{
  Debug.WriteLine(string.Format("- {0}: Type = {1}",
          tablix.Name,
            tablix.GetType().Name
           ));
 Debug.WriteLine(string.Format(", Rows = {0}, Columns = {1}", tablix.Body.RowCollection.Count, tablix.Body.ColumnCollection.Count));

 int i = 0;
  Debug.WriteLine("=== Row Hierarchies ===");
 List<TablixMember> dynamicRows = new List<TablixMember>();
  foreach (TablixMember m in tablix.RowHierarchy.MemberCollection)
    {
       Debug.WriteLine(string.Format(" - {0}: isStatic = {1}", i++, m.IsStatic));
      if (!m.IsStatic)
        {
           dynamicRows.Add(m);

     }
   }

   i = 0;
  Debug.WriteLine("=== Column Hierarchies ===");
  List<TablixMember> dynamicColumns = new List<TablixMember>();
   foreach (TablixMember m in tablix.ColumnHierarchy.MemberCollection)
 {
       Debug.WriteLine(string.Format(" - {0}: isStatic = {1}", i++, m.IsStatic));
      if (!m.IsStatic)
        {
           dynamicColumns.Add(m);
      }
   }

   // Print data: if classic table 
    Debug.WriteLine(string.Format("=== Stats: dynamic rows = {0}, columns = {1} ===", dynamicRows.Count, dynamicColumns.Count));

    Debug.WriteLine("=== Tablix Data ===");
 if (dynamicColumns.Count == 0)
  {
       // Process rows
     foreach (TablixMember m in tablix.RowHierarchy.MemberCollection)
        {
           if (m.IsStatic)
         {
               Debug.WriteLine(CreateRowCsv(tablix, m, ";"));
          }
           else
            {
               TablixDynamicMemberInstance dynamicInstance = (TablixDynamicMemberInstance)m.Instance;
              dynamicInstance.ResetContext();
             while (dynamicInstance.MoveNext())
              {
                   Debug.WriteLine(CreateRowCsv(tablix, m, ";"));
              }
           }
       }
   }
   else 
   {
       Debug.WriteLine("Data in format row hierarchy | column hierarchy | row recursion level | column recursion level | dataRowCounter-Prefix: val-1, val-2 ...");
        // Process rows
     foreach(TablixMember mR in tablix.RowHierarchy.MemberCollection)
        {
           DebugRowHierarchy(tablix, mR, 0, "");
       }
   }
}

/// <summary>
///  recursively process all row hiearchies, afterwards all column hierarchies and then the values itself
/// </summary>
/// <param name="tablix"></param>
/// <param name="hierarchy"></param>
/// <param name="rowDepth">0..n, depth of row: 0 = starting level, 1 = child, 2 = child of child... </param>
/// <param name="colDepth">0..n, depth of col: 0 = starting level, 1 = child, 2 = child of child... </param>
private static void DebugRowHierarchy(Tablix tablix, TablixMember rowMember, int rowDepth, string dataRowCounterPrefix)
{
 if (rowMember.Children == null || rowMember.Children.Count == 0)
    {
       // row-hierarchy has no children => process columns, either static or dynamic
        if (rowMember.IsStatic)
     {
           foreach (TablixMember mC in tablix.ColumnHierarchy.MemberCollection)
            {
               DebugColumnHierarchyForRow(tablix, rowMember, rowDepth, mC, 0, dataRowCounterPrefix);
           }

       }
       else
        {
           int dataRowCounter = 0;
         TablixDynamicMemberInstance dynamicInstance = (TablixDynamicMemberInstance)rowMember.Instance;
          dynamicInstance.ResetContext();
         while (dynamicInstance.MoveNext())
          {
               foreach (TablixMember mC in tablix.ColumnHierarchy.MemberCollection)
                {
                   dataRowCounter++;
                   DebugColumnHierarchyForRow(tablix, rowMember, rowDepth, mC, 0, string.Format("{0}/{1}",dataRowCounterPrefix, dataRowCounter));
              }
           }
       }
   } else
  {
       // row-hierarchy has children => process children, either static or dynamic
      if (rowMember.IsStatic)
     {
           foreach (TablixMember c in rowMember.Children)
          {
               DebugRowHierarchy(tablix, c, rowDepth + 1, dataRowCounterPrefix);
           }
       }
       else
        {
           int dataRowCounter = 0;
         TablixDynamicMemberInstance dynamicInstance = (TablixDynamicMemberInstance)rowMember.Instance;
          dynamicInstance.ResetContext();
         while (dynamicInstance.MoveNext())
          {
               foreach (TablixMember c in rowMember.Children)
              {
                   dataRowCounter++;
                   DebugRowHierarchy(tablix, c, rowDepth + 1, string.Format("{0}/{1}", dataRowCounterPrefix, dataRowCounter));
             }
           }
       }
   }
}

private static void DebugColumnHierarchyForRow(Tablix tablix, TablixMember rowMember, int rowDepth, TablixMember colMember, int colDepth, string dataRowCounterPrefix)
{
   if (colMember.Children == null || colMember.Children.Count == 0)
    {
       // col-hierarchy has no children => process values, either static or dynamic
     if (colMember.IsStatic)
     {
           foreach (TablixMember mC in tablix.ColumnHierarchy.MemberCollection)
            {
               Debug.WriteLine(string.Format("{0} | {1} | {2} | {3} | {4}: {5}",
                   rowMember.MemberCellIndex, colMember.MemberCellIndex,
                   rowDepth, colDepth,
                 dataRowCounterPrefix,
                   GetValue(tablix, rowMember.MemberCellIndex, colMember.MemberCellIndex)
                  )
                   );
          }

       }
       else
        {
           StringBuilder str = new StringBuilder();
            str.AppendFormat("{0} | {1} | {2} | {3} | {4}:",
                    rowMember.MemberCellIndex, colMember.MemberCellIndex,
                   rowDepth, colDepth,
                 dataRowCounterPrefix
                );
          TablixDynamicMemberInstance dynamicInstance = (TablixDynamicMemberInstance)colMember.Instance;
          bool addSeparator = false;
          dynamicInstance.ResetContext();
         while (dynamicInstance.MoveNext())
          {
               // add separator, except before first value
             if (addSeparator)
               {
                   str.Append(", ");
               }
               else
                {
                   addSeparator = true;
                }
               // add value
                str.Append(GetValue(tablix, rowMember.MemberCellIndex, colMember.MemberCellIndex));
         }
           Debug.WriteLine(str);
       }
   }
   else
    {
       // column-hierarchy has children => process children, either static or dynamic
       if (colMember.IsStatic)
     {
           foreach (TablixMember c in colMember.Children)
          {
               DebugColumnHierarchyForRow(tablix, rowMember, rowDepth, c, colDepth + 1, dataRowCounterPrefix);
         }
       }
       else
        {
           int dataRowCounter = 0;
         TablixDynamicMemberInstance dynamicInstance = (TablixDynamicMemberInstance)colMember.Instance;
          dynamicInstance.ResetContext();
         while (dynamicInstance.MoveNext())
          {
               dataRowCounter++;
               foreach (TablixMember c in rowMember.Children)
              {
                   DebugColumnHierarchyForRow(tablix, rowMember, rowDepth, c, colDepth + 1, string.Format("{0}/{1}", dataRowCounterPrefix, dataRowCounter));
               }
           }
       }
   }
}

public static string CreateRowCsv(Tablix tablix, TablixMember row, string separator)
{
 StringBuilder b = new StringBuilder();
  int count = tablix.Body.ColumnCollection.Count;
 int i = 0;
  while (i < count)
    {
       CellContents content = tablix.Body.RowCollection[row.MemberCellIndex][i].CellContents;
      b.Append(GetValue(content));

        // adjust column counter
        i += content.ColSpan;

       // add separator, if not last column
        if (i < count)
       {
           b.Append(separator);
        }
   }
   return b.ToString();
}

public static string GetValue(Tablix tablix, int rowHierarchy, int columnHierarchy)
{
   TablixCell tablixCell = tablix.Body.RowCollection[rowHierarchy][columnHierarchy];
   if (tablixCell != null) { 
      return GetValue(tablixCell.CellContents);
   }
   return null;
}

public static string GetValue(CellContents content)
{
   // add content, if textbox - TODO: support other cell content
   if (content != null && content.ReportItem != null)
  {
       TextBoxInstance textBoxInstance = content.ReportItem.Instance as TextBoxInstance;
       if (textBoxInstance != null)
        {
           return textBoxInstance.Value;
       }
       else if (content.ReportItem != null)
        {
           return string.Format("UNSUPPORTED REPORT-ITEM {0}", content.ReportItem.GetType().Name);
     }
   }
   return null;
}

Chart

The visual elements of a chart are grouped into Chart Areas.
The data itself is split into 4 dimensions:

  • Series Hierarchy
  • Category Hierarchy
  • Series Collection => containing a Chart Area Name and a Chart Series Type (Bar, Column, Line etc.)
  • Data Points

Example

Chart Definition:

Chart Preview:

Diagnostics Output:

 - Chart1: Type = Chart, Dataset = DSTableList 
=== Chart Titles === 
 - 0: Caption = Table-Count by Schema 
=== Areas === 
 - 0: Name = Default 
 - 1: Name = Area1 
=== Series Hierarchies === 
 - 0: Label = Table Name, isStatic = True 
=== Category Hierarchies === 
 - 0: Label = =Fields!SchemaName.Value, isStatic = False 
=== Series === 
 - 0: Name = TableName, Chart Area = , Chart Series Type = Bar 
=== Derived Series === 
 - 1: Name = DerivedSeries1, Chart Area = Area1, Chart Series Type = Line, Formula = MovingAverage 
=== Chart Details === 
 - Series Hierarchy | Category Hierarchy | Series-Name : Values (x|y) 
 - Table Name | Application | TableName : (|15) 
 - Table Name | dbo | TableName : (|1) 
 - Table Name | Purchasing | TableName : (|7) 
 - Table Name | Sales | TableName : (|12) 
 - Table Name | Warehouse | TableName : (|14)

Code to get the Diagnostics Output:

 public static void DebugItem(Chart chart)
{
 Debug.WriteLine(string.Format("- {0}: Type = {1}, Dataset = {2}",
   chart.Name,
   chart.GetType().Name,
   chart.DataSetName
   ));

 int i = 0;
 Debug.WriteLine("=== Chart Titles ===");
 foreach (ChartTitle t in chart.Titles)
 {
  Debug.WriteLine(string.Format(" - {0}: Caption = {1}", i++, ((ChartTitleInstance)t.Instance).Caption));
 }

 i = 0;
 Debug.WriteLine("=== Areas ===");
 foreach (ChartArea a in chart.ChartAreas)
 {
  Debug.WriteLine(string.Format(" - {0}: Name = {1}", i++, a.Name));
 }

 i = 0;
 Debug.WriteLine("=== Series Hierarchies ===");
 foreach (ChartMember m in chart.SeriesHierarchy.MemberCollection)
 {
  Debug.WriteLine(string.Format(" - {0}: Label = {1}, isStatic = {2}", i++, (m.Label.IsExpression ? m.Label.ExpressionString : m.Label.Value), m.IsStatic));
 }

 i = 0;
 Debug.WriteLine("=== Category Hierarchies ===");
 foreach (ChartMember m in chart.CategoryHierarchy.MemberCollection)
 {
  Debug.WriteLine(string.Format(" - {0}: Label = {1}, isStatic = {2}", i++, (m.Label.IsExpression ? m.Label.ExpressionString : m.Label.Value), m.IsStatic));
 }

 i = 0;
 Debug.WriteLine("=== Series ===");
 foreach (ChartSeries s in chart.ChartData.SeriesCollection)
 {
  Debug.WriteLine(string.Format(" - {0}: Name = {1}, Chart Area = {2}, Chart Series Type = {3}", i++, s.Name, s.Instance.ChartAreaName, s.Type.Value));
 }
 Debug.WriteLine("=== Derived Series ===");
 foreach (ChartDerivedSeries s in chart.ChartData.DerivedSeriesCollection)
 {
  Debug.WriteLine(string.Format(" - {0}: Name = {1}, Chart Area = {2}, Chart Series Type = {3}, Formula = {4}", i++, s.Series.Name, s.Series.Instance.ChartAreaName, s.Series.Type.Value, s.DerivedSeriesFormula));
 }

 Debug.WriteLine("=== Chart Details ===");
 Debug.WriteLine(" - Series Hierarchy | Category Hierarchy | Series-Name : Values (x|y)");
 foreach (ChartMember m in chart.SeriesHierarchy.MemberCollection)
 {
  DebugChartSeriesHierarchy(chart, m);
 }
}

private static void DebugChartSeriesHierarchy(Chart chart, ChartMember seriesHierarchy)
{
 if (seriesHierarchy.IsStatic)
 {
  foreach (ChartMember cm in chart.CategoryHierarchy.MemberCollection)
  {
   DebugChartCategoryHierarchy(chart, seriesHierarchy, cm);
  }
 }
 else
 {
  ChartDynamicMemberInstance dI = (ChartDynamicMemberInstance)seriesHierarchy.Instance;
  dI.ResetContext();
  while (dI.MoveNext())
  {
   foreach (ChartMember cm in chart.CategoryHierarchy.MemberCollection)
   {
    DebugChartCategoryHierarchy(chart, seriesHierarchy, cm);
   }
  }
 }
}

private static void DebugChartCategoryHierarchy(Chart chart, ChartMember seriesHierarchy, ChartMember categoryHierarchy)
{
 if (categoryHierarchy.IsStatic)
 {
  foreach (ChartSeries s in chart.ChartData.SeriesCollection)
  {
   DebugChartSeries(chart, seriesHierarchy, categoryHierarchy, s);
  }
 }
 else
 {
  ChartDynamicMemberInstance dI2 = (ChartDynamicMemberInstance)categoryHierarchy.Instance;
  dI2.ResetContext();
  while (dI2.MoveNext())
  {
   foreach (ChartSeries s in chart.ChartData.SeriesCollection)
   {
    DebugChartSeries(chart, seriesHierarchy, categoryHierarchy, s);
   }
  }
 }
}

private static void DebugChartSeries(Chart chart, ChartMember seriesHierarchy, ChartMember categoryHierarchy, ChartSeries series)
{
 StringBuilder sDataPoint = new StringBuilder();
 sDataPoint.AppendFormat(" - {0} | {1} | {2} : ", seriesHierarchy.Instance.Label, categoryHierarchy.Instance.Label, series.Name);
 for (int iDataPoint = 0; iDataPoint < series.Count; iDataPoint++)
 {
  ChartDataPoint p = series[iDataPoint];
  ChartDataPointValues pV = p.DataPointValues;
  ChartDataPointValuesInstance pVI = pV.Instance;
  sDataPoint.AppendFormat("({0}|{1})", pVI.X, pVI.Y);
  if (iDataPoint < series.Count - 1)
  {
   sDataPoint.Append(", ");
  }
 }
 Debug.WriteLine(sDataPoint);
}

Conclusion

During a few days, I've got only a first glimpse of the possibilities with Custom Rendering Extension. This post therefore covers only a part of the overall picture - but hopefully is a good start, if you have to proceed with this journey.

Further Reading