Click here to Skip to main content
Click here to Skip to main content

Adding DOC, RTF, and OOXML Export Formats to the Microsoft Report Viewer Control: Implementing It Easier Using Private Reflection

, 18 Aug 2008
Rate this:
Please Sign up or sign in to vote.
By following the steps outlined in this article, you will be able to get Report Viewer to generate reports in Microsoft Word formats (DOC, RTF, WordprocessingML, and OOXML) when working in the local mode.

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:

image1.png

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:

image2.png

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:

image3.png

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:

image4.png

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:

image5.png

image6.png

Now, add a reference to Microsoft.ReportViewer.Common.dll:

image7.png

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].

image8.png

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.

image9.png

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:

image10.png

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Anton Ponomarev
Founder
United Kingdom United Kingdom
No Biography provided

Comments and Discussions

 
GeneralIntegration with ReportViewer from SQL Server 2008 PinmemberAlex Kostikov31-Mar-09 23:34 
GeneralRe: Integration with ReportViewer from SQL Server 2008 PinmemberAnton Ponomarev1-Feb-10 0:39 
Questionsmall error [modified] PinmemberBo3aBeD23-Dec-08 23:55 
AnswerRe: small error PinmemberAnton Ponomarev1-Feb-10 0:38 
QuestionReport Viewer Image problem? Pinmemberperspolis13-Dec-08 2:55 
AnswerRe: Report Viewer Image problem? PinmemberAnton Ponomarev1-Feb-10 0:37 
GeneralI am getting it PinmemberDhana Satish16-Nov-08 17:23 
GeneralRe: I am getting it PinmemberAnton Ponomarev1-Feb-10 0:36 
GeneralNot able to view the extension(doc) in Report viewer Pinmemberlekha161223-Oct-08 23:49 
GeneralRe: Not able to view the extension(doc) in Report viewer PinmemberAnton Ponomarev28-Oct-08 23:55 
GeneralYou can do all that now without any trouble modifying dlls etc. PinmemberAnton Ponomarev20-Sep-08 14:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140821.2 | Last Updated 18 Aug 2008
Article Copyright 2008 by Anton Ponomarev
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid