Overview
This article is a continuation of Adding DOC, RTF, and OOXML Export Formats to the Microsoft Report Viewer Control. In the first part of the series, I suggested a technique that allows to add Microsoft Word export formats to the Microsoft Report Viewer control. This technique required modifying .NET assemblies of both Report Viewer and a custom rendering extension supposed to be integrated with the control. The article was commented intensively, and one of the readers (Bravo Niner) suggested a way of simplifying the process dramatically by involving private Reflection instead of modifying IL code. I’m thankful to him for this great idea as it allows to achieve the same goal with much less efforts indeed. I added a short description of the idea to the end of the article, but then decided to dedicate a separate article to it because it certainly deserves that.
But, let’s first repeat what we currently have and what we want to have at the end. The following few sections are almost identical to those in the first article, so if you have read it and are familiar with the subject, skip this fragment and jump directly to the Implement it section.
Introduction
The Microsoft Report Viewer 2005 control does not support exporting to Microsoft Word formats, by default, but following the steps outlined in this article, you will be able to get the Report Viewer to generate reports in Microsoft Word formats (DOC, RTF, WordprocessingML, and OOXML) when working in the local mode.
I came across this issue while working on a project for one of my clients recently. The project initially relied on using the Microsoft Report Viewer, but later, the client realized they needed reports in the DOC format. I have found this was possible when using Microsoft SQL Server 2005 Reporting Services with a third-party tool, Aspose.Words for Reporting Services, but the client insisted that SQL Server should not be used; even the free SQL Server Express Edition was rejected by the client.
During the evaluation of SQL Server Reporting Services with Aspose.Words for Reporting Services, I thoroughly enjoyed the reports as Microsoft Word documents, and was very disappointed that the same was not possible in the Report Viewer. Out of curiosity, I did some digging, and would like to share my findings.
Understand it
The Microsoft Report Viewer is a powerful .NET control allowing to embed RDL and RDLC reports in WinForms and ASP.NET applications. It enables users to view and export reports to different formats such as PDF or HTML. The control is included with Microsoft Visual Studio 2005, and it is also available as a free download from Microsoft.
The Report Viewer can generate reports independently using a built-in engine (which is called local mode), or it can display reports that are generated on a Microsoft SQL Server 2005 Reporting Services Report Server (remote mode).
When working in the remote mode, the Report Viewer is able to export reports to all formats installed on the Report Server it connects to. It means, the list of export formats available in the control dropdown is exactly the same as the one displayed in the Report Manager dropdown. The default rendering formats available in the remote mode are: Excel, MHTML, PDF, TIFF, XML, and CSV. The important thing here is that the list of export formats is expandable by installing custom rendering extensions on SQL Server. There are very useful third-party rendering extensions available on the market that allow exporting of reports to Microsoft Word formats (DOC, RTF, OOXML), for example, Aspose.Words for Reporting Services.
However, when working in the local mode, the list of export formats is limited to only a few formats, and cannot be expanded because the export is hard coded into the Report Viewer assemblies. This fact represents a serious disadvantage as many users would like to have the ability to export reports to DOC, RTF, OOXML, and other formats when using Report Viewer in the local mode.
This article describes a way of overcoming this limitation and making Report Viewer generate reports in Microsoft Word document formats. Our goal is to add a custom rendering extension to the standard Microsoft Report Viewer control.
Disclaimer
Read and use at your own risk. The author only expresses his personal views, and does not endorse or promote steps described in this article.
Investigate it
To get started, you need to have Microsoft Report Viewer 2005 installed on your computer. You can either select it as a feature during the Microsoft Visual Studio 2005 installation, or you can download the Microsoft Report Viewer 2005 Redistributable from the Microsoft website.
Let’s investigate how it works and what we want to get in the end. The Report Viewer control consists of several assemblies installed to the Global Assembly Cache (GAC). Here is the list:
- Microsoft.ReportViewer.Common – contains classes common for both WinForms and ASP.NET controls.
- Microsoft.ReportViewer.ProcessingObjectModel – contains classes responsible for local report processing (similar to those used by Reporting Services).
- Microsoft.ReportViewer.WinForms – contains classes specific for WinForms controls.
- Microsoft.ReportViewer.Design – contains designer classes for WinForms controls.
- Microsoft.ReportViewer.WebForms – contains classes specific for ASP.NET controls.
- Microsoft.ReportViewer.WebDesign – contains designer classes for ASP.NET controls.
The first step we should do is to extract the assemblies from the GAC. This may seem simple, but unfortunately, we can’t do that using Windows Explorer. When viewing the GAC folder, it invokes a special Shell extension that prevents assemblies from doing simple copy/paste. So, we are going to use the command line to complete this task.
Assuming your working directory is C:\Work, run the following commands from the console:
copy c:\windows\assembly\gac_msil\microsoft.reportviewer.common\
8.0.0.0__b03f5f7f11d50a3a\microsoft.reportviewer.common.dll c:\work
copy c:\windows\assembly\gac_msil\microsoft.reportviewer.processingobjectmodel\
8.0.0.0__b03f5f7f11d50a3a\microsoft.reportviewer.processingobjectmodel.dll c:\work
copy c:\windows\assembly\gac_msil\microsoft.reportviewer.winforms\
8.0.0.0__b03f5f7f11d50a3a\microsoft.reportviewer.winforms.dll c:\work
copy c:\windows\assembly\gac_msil\microsoft.reportviewer.design\
8.0.0.0__b03f5f7f11d50a3a\microsoft.reportviewer.design.dll c:\work
copy c:\windows\assembly\gac_msil\microsoft.reportviewer.webforms\
8.0.0.0__b03f5f7f11d50a3a\microsoft.reportviewer.webforms.dll c:\work
copy c:\windows\assembly\gac_msil\microsoft.reportviewer.webdesign\
8.0.0.0__b03f5f7f11d50a3a\microsoft.reportviewer.webdesign.dll c:\work
You can now see six DLLs in the C:\Work folder:
Next, download the excellent Reflector tool by Lutz Roeder (unless you already have it on your machine… almost no doubt you do) and open the Microsoft.ReportViewer.Common.dll assembly. Locate the Microsoft.Reporting.ControlService.ListRenderingExtensions
method. The disassembled method looks like the following:
public override IEnumerable<LocalRenderingExtensionInfo> ListRenderingExtensions()
{
if (this.m_renderingExtensions == null)
{
List<LocalRenderingExtensionInfo> list = new List<LocalRenderingExtensionInfo>();
Html40RenderingExtension extension = new Html40RenderingExtension();
list.Add(new LocalRenderingExtensionInfo("HTML4.0", extension.LocalizedName, false,
typeof(Html40RenderingExtension), false));
Microsoft.ReportingServices.Rendering.ExcelRenderer.ExcelRenderer renderer =
new Microsoft.ReportingServices.Rendering.ExcelRenderer.ExcelRenderer();
list.Add(new LocalRenderingExtensionInfo("Excel", renderer.LocalizedName, true,
typeof(Microsoft.ReportingServices.Rendering.ExcelRenderer.ExcelRenderer), true));
RemoteGdiReport report = new RemoteGdiReport();
list.Add(new LocalRenderingExtensionInfo("RGDI", report.LocalizedName, false,
typeof(RemoteGdiReport), false));
ImageReport report2 = new ImageReport();
list.Add(new LocalRenderingExtensionInfo("IMAGE", report2.LocalizedName, false,
typeof(ImageReport), true));
PdfReport report3 = new PdfReport();
list.Add(new LocalRenderingExtensionInfo("PDF", report3.LocalizedName, true,
typeof(PdfReport), true));
this.m_renderingExtensions = list;
}
return this.m_renderingExtensions;
}
As you can see, the method returns a generic list of LocalRenderingExtensionInfo
objects, each of them containing information about a certain rendering extension. You can see that the list of export formats is hardcoded, and new formats cannot be added using a configuration file like on the Report Server.
You can also notice that each rendering extension class derives from the RenderingExtensionBase
class which, in turn, implements the IExtension
and IRenderingExtension
interfaces. It is an interesting fact, because these interfaces and rendering extension classes look very similar to the ones used in the full-fledged and extensible Microsoft SQL Server 2005 Reporting Services.
Let’s make a hypothesis that Microsoft actually used the same code for rendering extensions on the server and in the viewer control, but just packaged it differently. They’ve made it possible to add new custom rendering extensions on the server, but disabled doing so on the client by hard-coding the list of export formats. We can endlessly argue about Microsoft’s reasons for doing this, but this is outside of the scope of this article.
For our purposes, it is enough to know that in Report Viewer, the rendering extensions are located in the Microsoft.ReportViewer.XXX assemblies, and on the Reporting Server, they are located in the corresponding Microsoft.ReportingServices.XXX assemblies.
The consequence of our theory is that if we somehow add a custom rendering extension that works on the Reporting Server to the ListRenderingExtensions
method in the Report Viewer, it will work, and Report Viewer will be able to generate reports in more formats.
We are going to take a popular commercial product Aspose.Words for Reporting Services that allows Microsoft SQL Server Reporting Services to export reports to Microsoft Word document formats (DOC, DOCX, RTF, and WordprocessingML). We are going to add Aspose.Words for Reporting Services to the Report Viewer’s list of rendering extensions so exporting to Microsoft Word formats is available in the control too.
You need to download an evaluation version of Aspose.Words for Reporting Services from its download page and place the \Bin\SSRS2005\Aspose.Words.ReportingServices.dll assembly in C:\Work. Leverage Reflector again, and you will notice that almost the whole assembly is obfuscated, except for four public classes, each of which represents a rendering extension for a specific format:
That is exactly what we need. Our ultimate goal is to add the information about these extensions to the list returned by the ListRenderingExtensions
method using the existing pattern. The code to add should look like the following:
DocRenderer docRenderer = new DocRenderer ();
list.Add(new LocalRenderingExtensionInfo("AWDOC",
docRenderer.LocalizedName, false, typeof(DocRenderer), false));
DocxRenderer docxRenderer = new DocxRenderer ();
list.Add(new LocalRenderingExtensionInfo("AWDOCX",
docxRenderer.LocalizedName, false, typeof(DocxRenderer), false));
RtfRenderer rtfRenderer = new RtfRenderer ();
list.Add(new LocalRenderingExtensionInfo("AWRTF",
rtfRenderer.LocalizedName, false, typeof(RtfRenderer), false));
WordMLRenderer wordMLRenderer = new WordMLRenderer ();
list.Add(new LocalRenderingExtensionInfo("AWWORDML",
wordMLRenderer.LocalizedName, false, typeof(WordMLRenderer), false));
In the first article, we merged the Report Viewer assemblies into one, and then modified it in order to inject the code directly to IL. Now, we are going to do the same thing from outside using a great technology provided by .NET Framework – Reflection.
Implement it
Note that steps 1 to 5 are only required if you wish to integrate the Report Viewer control with a third-party rendering extension. This is because such extensions normally implement the IRenderingExtension
interface located in the Microsoft.ReportingServices.Interfaces.dll assembly, and our goal is to redirect them to the Microsoft.ReportViewer.Common.dll assembly. If you are developing your own rendering extension from scratch, just reference Microsoft.ReportViewer.Common.dll and proceed to step 6.
Step 1: Disassemble
Like in the first article, we are going to use ILDASM.exe, which is a disassembler included with the .NET Framework SDK.
Launch the Visual Studio 2005 command prompt, and launch the following command:
ildasm Aspose.Words.ReportingServices.dll /out=Aspose.Words.ReportingServices.il /unicode
Step 2: Remove public key
Since we are going to modify the IL code, we have to get rid of the public keys as we won’t be able to reassemble the code then. Open Aspose.Words.ReportingServices.il in Notepad or any other text editor (it might take some time to load because it is around 37 MB in size). Search for “.publickey =”, select as shown below, and press Delete:
Step 3: Modify references in the rendering extension
Our next goal is to redirect the rendering extension assembly’s references so that it refers to the Microsoft.ReportViewer.Common.dll assembly instead of Microsoft.ReportingServices.Interfaces.dll and Microsoft.ReportingServices.ProcessingCore.dll.
Locate those references in Aspose.Words.ReportingServices.il and remove them:
Now, add a reference to Microsoft.ReportViewer.Common.dll:
We are not done yet though. As you might know, identifiers in IL are always fully qualified, and are preceded with [assembly_name] where assembly_name is the name of the assembly containing the referenced object. Hence, we have to replace all these references with new assembly names. Press Ctrl-H, and replace all occurrences of the [Microsoft.ReportingServices.Interfaces] and [Microsoft.ReportingServices.ProcessingCore] strings with [Microsoft.ReportViewer.Common].
Step 4: Remove mangled resources
There is a small hassle specific to Aspose.Words for Reporting Services’ obfuscation, or at least, to the version I worked with (2.0.2.0). The obfuscation tool they use mangles the names of some embedded resources, and for some weird reason, ILASM fails to assemble the code, although I was sure it should accept any Unicode name.
Anyway, the easiest way I found out is to merely get rid of those resources (they seem to be some test harness leftovers).
Locate where the embedded resources are specified (a bunch of .mresource
tokens) and simply delete all those that have unreadable Unicode names (such as Ӕ.ӗ.resources). I located four such mangled names for version 2.0.2.0 of the rendering extension.
Save and close Aspose.Words.ReportingServices.il.
Step 5: Assemble
Now, we are going to assemble our modified IL code.
Run the following command from the Visual Studio command prompt while in the C:\Work folder:
ilasm Aspose.Words.ReportingServices.il /dll
Step 6: Inject rendering extension using private Reflection
This is the final and crucial step. Now, instead of modifying the Report Viewer’s code, we will do the same thing using private Reflection. Add the following method to your project:
private static void AddExtension(ReportViewer viewer, string name, Type extensionType)
{
const BindingFlags Flags = BindingFlags.NonPublic |
BindingFlags.Public | BindingFlags.Instance;
FieldInfo previewService =
viewer.LocalReport.GetType().GetField("m_previewService", Flags);
MethodInfo ListRenderingExtensions =
previewService.FieldType.GetMethod("ListRenderingExtensions", Flags);
IList extensions = ListRenderingExtensions.Invoke(
previewService.GetValue(viewer.LocalReport), null) as IList;
Type localRenderingExtensionInfoType = Type.GetType(
"Microsoft.Reporting.LocalRenderingExtensionInfo, " +
"Microsoft.ReportViewer.Common," +
"Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
ConstructorInfo ctor = localRenderingExtensionInfoType.GetConstructor(
Flags, null, new Type[] { typeof(string), typeof(string),
typeof(bool), typeof(Type), typeof(bool) }, null);
object instance =
ctor.Invoke(new object[] { name, name, true, extensionType, true });
extensions.Add(instance);
}
Now, you can call this method whenever you need to add a custom export format to the list of Report Viewer formats (neat places to consider could be the Form_Load
or Page_Load
event handlers in a WinForms or ASP.NET application, respectively). The viewer
parameter is a Report Viewer instance, the name
parameter is the name of the export format as it should appear on the list, and the extensionType
parameter is the .NET type of the rendering extension:
AddExtension(ReportViewer1, "DOC - Word Document via Aspose.Words",
typeof(Aspose.Words.ReportingServices.DocRenderer));
Step 7: Test new export formats
Done! Just run your project, and you should note there is a new export format that has appeared on the list:
Conclusion
As you can see, using private Reflection has a number of benefits compared to hacking the Report Viewer assemblies:
- The whole process is much easier. You only need to modify a third-party rendering extension to redirect it to Microsoft.ReportViewer.Common.dll; if you are developing your own rendering extension, all you need is to add a short method to your code.
- You don’t need to care about where the Report Viewer assemblies are placed; you don’t need to modify web.config; in other words, you are free of all difficulties described in the first article.
- You can add (and probably remove) export formats to the list dynamically.