Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / productivity / Office

Adding DOC, RTF, and OOXML Export Formats to the Microsoft Report Viewer Control

4.86/5 (37 votes)
31 Jul 2008CPOL16 min read 1   1.9K  
By following 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.

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. To achieve that, we will use several widely known .NET tools to modify the Report Viewer assemblies.

Essentially, what is described here is “white-box hacking” or “ethical hacking”. White-box means, you follow simple transparent steps yourself to produce a hacked version, and you can be sure there are no unwanted surprises in it. Ethical means you are not making any harm or annoyance to anyone.

The techniques described in this article will involve:

  • Using Reflector to peek into assembly files.
  • Using Microsoft’s ILASM And ILDASM tools to disassemble and assemble intermediate code.
  • Using Microsoft’s ILMerge tool to merge assemblies.

There is also a way of implementing the same thing by making much less effort. This method was suggested by the community member Bravo Niner (thank you so much for that!) after the article was published. The idea is that you can use private Reflection instead of hacking the Report Viewer control. You still have to hack the rendering extension you want to integrate with the control (unless it was developed by you, of course), but most of the annoying steps can still be omitted. This technique is described in the Adding Export Formats Dynamically Using Private Reflection section at the end of the article.

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 accomplish 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:

report-viewer-hack/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:

report-viewer-hack/image2.png

C#
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:

report-viewer-hack/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:

C#
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));

Now, let’s do that.

Implement it

Step 1: Merge ReportViewer assemblies

The first step is merging the extracted Report Viewer assemblies into one. Why would we need that? The answer is: necessity and convenience.

First of all, let’s think about what we are going to do next. We plan to hack assemblies by modifying their IL code; therefore, we will be forced to remove their strong names and public keys. This means we will have to update all references to those assemblies as well, and don’t forget we have six Report Viewer assemblies + a rendering extension assembly to work with.

Second, Microsoft.ReportViewer.Common.dll is marked with a pair of InternalsVisibleTo attributes, allowing access to its internal members to code from the Microsoft.ReportViewer.WinForms.dll and Microsoft.ReportViewer.WebForms.dll assemblies. That means we will need to update the assembly name passed to the attribute’s constructor; moreover, this attribute does not seem to work with unsigned assemblies at all.

So, let’s make it a single assembly, and get rid of all the problems at once.

However, we can’t merge all six assemblies straightaway, because the designer assemblies contain classes with the same fully qualified names. Besides, you would hardly need to have both the WinForms and ASP.NET controls in one large assembly. You can create two assemblies if needed. In this article, we will be working with the WinForms controls, so the assemblies you need to focus on are:

  • Microsoft.ReportViewer.Common.dll
  • Microsoft.ReportViewer.WinForms.dll
  • Microsoft.ReportViewer.Design.dll

To merge assemblies, download and install a simple utility by Microsoft named ILMerge. It is available here.

Change the current directory to C:\Work, and run the following command from the console:

"c:\program files\microsoft\ilmerge\ilmerge" 
    /out:Microsoft.ReportViewer.WinForms.Modified.dll
    microsoft.reportviewer.common.dll
    microsoft.reportviewer.processingobjectmodel.dll
    microsoft.reportviewer.winforms.dll
    microsoft.reportviewer.design.dll

Now, we have the single Microsoft.ReportViewer.WinForms.Modified.dll assembly containing all the necessary Report Viewer classes in one place:

report-viewer-hack/image4.png

Step 2: Disassemble

Now, we are ready to extract some IL code from our assemblies. We need to alter both Report Viewer and the rendering extension assemblies, so let’s disassemble both. 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 commands:

ildasm Microsoft.ReportViewer.WinForms.Modified.dll 
    /out=Microsoft.ReportViewer.WinForms.Modified.il

ildasm Aspose.Words.ReportingServices.dll 
    /out=Aspose.Words.ReportingServices.il /unicode

Step 3: Remove public keys

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. However, if you explore Microsoft.ReportViewer.WinForms.Modified.dll, you will find it is not strong named. This is because ILMerge builds an unsigned assembly by default, unless you explicitly instruct it to sign the target assembly.

Okay, but we still have Aspose.Words.ReportingServices.dll signed, so we need to remove the public key from the IL code. 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:

report-viewer-hack/image5.png

Step 4: Modify references in the rendering extension

Our next goal is to redirect the rendering extension assembly’s references so that it refers to the modified Report Viewer assembly instead of Microsoft.ReportingServices.Interfaces.dll and Microsoft.ReportingServices.ProcessingCore.dll.

Locate those references in Aspose.Words.ReportingServices.il and remove them:

report-viewer-hack/image6.png

report-viewer-hack/image7.png

Now, add a reference to Microsoft.ReportViewer.WinForms.Modified.dll:

MSIL
.assembly extern Microsoft.ReportViewer.WinForms.Modified
{
}

report-viewer-hack/image8.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.WinForms.Modified].

report-viewer-hack/image9.png

Step 5: Remove mangled resources

There is a small hassle specific to the 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.

report-viewer-hack/image10.png

Save and close Aspose.Words.ReportingServices.il.

Step 6: Modify the references in Report Viewer

We are almost ready to add the information about custom rendering extensions (and therefore additional export formats), but first, we need to add a reference to the rendering extension assembly.

Open Microsoft.ReportViewer.WinForms.Modified.il and add the reference:

MSIL
.assembly extern Aspose.Words.ReportingServices
{
}

report-viewer-hack/image11.png

Step 7A (for both WinForms and WebForms controls): Modify the ListRenderingExtensions method

We are ready to alter the Microsoft.Reporting.ControlService.ListRenderingExtensions method by adding LocalRenderingExtensionInfo objects that contain the information about additional rendering extensions. Basically, we can notice how this addition is implemented in IL and copy/paste this block of code and change it in a few places. Here is the modified method that adds the Aspose.Words for Reporting Services renderers (the changes are highlighted):

MSIL
.method public hidebysig virtual instance class 
    [mscorlib]System.Collections.Generic.IEnumerable`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo> 
    ListRenderingExtensions() cil managed 
{ 
// Code size 403 (0x193) 
.maxstack 41 
.locals init (class [mscorlib]System.Collections.Generic.List`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo> V_0, 
class Microsoft.ReportingServices.Rendering.HtmlRenderer.Html40RenderingExtension V_1, 
class Microsoft.ReportingServices.Rendering.ExcelRenderer.ExcelRenderer V_2,

// --- ADDED CODE
 
class [Aspose.Words.ReportingServices]Aspose.Words.ReportingServices.DocRenderer V_3, 
class [Aspose.Words.ReportingServices]Aspose.Words.ReportingServices.RtfRenderer V_4, 
class [Aspose.Words.ReportingServices]Aspose.Words.ReportingServices.WordMLRenderer V_5, 
class [Aspose.Words.ReportingServices]Aspose.Words.ReportingServices.DocxRenderer V_6,

// --- END OF ADDED CODE
 
class Microsoft.ReportingServices.Rendering.ImageRenderer.RemoteGdiReport V_7, 
class Microsoft.ReportingServices.Rendering.ImageRenderer.ImageReport V_8, 
class Microsoft.ReportingServices.Rendering.ImageRenderer.PdfReport V_9) 
IL_0000: ldarg.0 
IL_0001: ldfld class [mscorlib]System.Collections.Generic.List`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo> 
    Microsoft.Reporting.ControlService::m_renderingExtensions 
IL_0006: brtrue IL_018c 
IL_000b: newobj instance void class [mscorlib]System.Collections.Generic.List`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo>::.ctor() 
IL_0010: stloc.0 
IL_0011: newobj instance void Microsoft.ReportingServices.Rendering.
    HtmlRenderer.Html40RenderingExtension::.ctor() 
IL_0016: stloc.1 
IL_0017: ldloc.0 
IL_0018: ldstr "HTML4.0" 
IL_001d: ldloc.1 
IL_001e: callvirt instance string Microsoft.ReportingServices.Rendering.
    HtmlRenderer.RenderingExtensionBase::get_LocalizedName() 
IL_0023: ldc.i4.0 
IL_0024: ldtoken Microsoft.ReportingServices.Rendering.HtmlRenderer.
    Html40RenderingExtension 
IL_0029: call class [mscorlib]System.Type 
    [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 
IL_002e: ldc.i4.0 
IL_002f: newobj instance void Microsoft.Reporting.
  LocalRenderingExtensionInfo::.ctor(string, string, bool, class [mscorlib]System.Type, bool) 
IL_0034: callvirt instance void class [mscorlib]System.Collections.Generic.List`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo>::Add(!0) 

...

// --- ADDED CODE

IL_0061: newobj instance void [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.DocRenderer::.ctor() 
IL_0066: stloc.3 
IL_0067: ldloc.0 
IL_0068: ldstr "AWDOC" 
IL_006d: ldloc.3 
IL_006e: callvirt instance string [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.DocRenderer::get_LocalizedName() 
IL_0073: ldc.i4.1 
IL_0074: ldtoken [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.DocRenderer 
IL_0079: call class [mscorlib]System.Type 
    [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 
IL_007e: ldc.i4.1 
IL_007f: newobj instance void Microsoft.Reporting.
  LocalRenderingExtensionInfo::.ctor(string, string, bool, class [mscorlib]System.Type, bool) 
IL_0084: callvirt instance void class [mscorlib]System.Collections.Generic.List`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo>::Add(!0) 
IL_0089: newobj instance void [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.RtfRenderer::.ctor() 
IL_008e: stloc.s V_4 
IL_0090: ldloc.0 
IL_0091: ldstr "AWRTF" 
IL_0096: ldloc.s V_4 
IL_0098: callvirt instance string [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.RtfRenderer::get_LocalizedName() 
IL_009d: ldc.i4.1 
IL_009e: ldtoken [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.RtfRenderer 
IL_00a3: call class [mscorlib]System.Type 
    [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 
IL_00a8: ldc.i4.1 
IL_00a9: newobj instance void Microsoft.Reporting.
  LocalRenderingExtensionInfo::.ctor(string, string, bool, class [mscorlib]System.Type, bool) 
IL_00ae: callvirt instance void class [mscorlib]System.Collections.Generic.List`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo>::Add(!0) 
IL_00b3: newobj instance void [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.WordMLRenderer::.ctor() 
IL_00b8: stloc.s V_5 
IL_00ba: ldloc.0 
IL_00bb: ldstr "AWWML" 
IL_00c0: ldloc.s V_5 
IL_00c2: callvirt instance string [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.WordMLRenderer::get_LocalizedName() 
IL_00c7: ldc.i4.1 
IL_00c8: ldtoken [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.WordMLRenderer 
IL_00cd: call class [mscorlib]System.Type 
    [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) 
IL_00d2: ldc.i4.1 
IL_00d3: newobj instance void Microsoft.Reporting.
  LocalRenderingExtensionInfo::.ctor(string, string, bool, class [mscorlib]System.Type, bool) 
IL_00d8: callvirt instance void class [mscorlib]System.Collections.Generic.List`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo>::Add(!0) 
IL_00dd: newobj instance void [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.DocxRenderer::.ctor() 
IL_00e2: stloc.s V_6 
IL_00e4: ldloc.0 
IL_00e5: ldstr "AWDOCX" 
IL_00ea: ldloc.s V_6 
IL_00ec: callvirt instance string [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.DocxRenderer::get_LocalizedName() 
IL_00f1: ldc.i4.1 
IL_00f2: ldtoken [Aspose.Words.ReportingServices]
    Aspose.Words.ReportingServices.DocxRenderer 
IL_00f7: call class [mscorlib]System.Type 
    [mscorlib]System.Type::GetTypeFromHandle(valuetype 
                     [mscorlib]System.RuntimeTypeHandle) 
IL_00fc: ldc.i4.1 
IL_00fd: newobj instance void Microsoft.Reporting.
    LocalRenderingExtensionInfo::.ctor(string, string, bool, 
               class [mscorlib]System.Type, bool) 
IL_0102: callvirt instance void class [mscorlib]System.Collections.Generic.List`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo>::Add(!0) 

// --- END OF ADDED CODE

...

IL_0185: ldarg.0 
IL_0186: ldloc.0 
IL_0187: stfld class [mscorlib]System.Collections.Generic.List`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo> 
       Microsoft.Reporting.ControlService::m_renderingExtensions 
IL_018c: ldarg.0 
IL_018d: ldfld class [mscorlib]System.Collections.Generic.List`1
    <class Microsoft.Reporting.LocalRenderingExtensionInfo> 
       Microsoft.Reporting.ControlService::m_renderingExtensions 
IL_0192: ret 
} // end of method ControlService::ListRenderingExtensions

Replace your ListRenderingExtensions method with the above listing, and proceed to the next step.

Step 7B (for WebForms control only): Modify the HTTP Handler type

We have to apply a bit more changes in order to get the WebForms control working. Since Microsoft.ReportViewer.WebForms.Modified.dll has no public key anymore, all HTTP handler type references should now point to this unsigned assembly. Here are two places in Microsoft.ReportViewer.WebForms.Modified.il you should additionally modify:

  1. Locate the following method:
  2. MSIL
    .method assembly hidebysig static bool 
      ConfigContainsHandler(class [System.Configuration]
           System.Configuration.Configuration config)

    and change the HTTP handler reference from:

    MSIL
    IL_0064:  ldstr      "Microsoft.Reporting.WebForms.HttpHandler, Microsof"
    + "t.ReportViewer.WebForms, Version=8.0.0.0, Culture=neutral, PublicKeyTok"
    + "en=b03f5f7f11d50a3a"
    

    to

    MSIL
    IL_0064:  ldstr      "Microsoft.Reporting.WebForms.HttpHandler, Microsof"
    + "t.ReportViewer.WebForms.Modified"
    
  3. Locate the class:
  4. MSIL
    .class private abstract auto ansi sealed 
           beforefieldinit Microsoft.Reporting.WebForms.Constants
           extends [mscorlib]System.Object

    and replace the following field:

    MSIL
    .field public static literal string HttpHandlerTypeName =
       "Microsoft.Reporting.WebForms.HttpHandler, Microsof"
       + "t.ReportViewer.WebForms, Version=8.0.0.0, Culture=neutral,
                                   PublicKeyToken=b03f5f7f11d50a3a"
    

    to

    MSIL
    .field public static literal string HttpHandlerTypeName =
    "Microsoft.Reporting.WebForms.HttpHandler, Microsof"
    + "t.ReportViewer.WebForms.Modified"
    

    Notice, you'll then have to modify the web.config accordingly: replace the original type references:

    XML
    ...
    <httpHandlers>
    
    <add path="Reserved.ReportViewerWebControl.axd" verb="*" 
    type="Microsoft.Reporting.WebForms.HttpHandler, Microsoft.ReportViewer.WebForms, 
          Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
    
    validate="false" />
    
    </httpHandlers>
    ...
    <buildProviders>
    
    <add extension=".rdlc" 
    type="Microsoft.Reporting.RdlBuildProvider, Microsoft.ReportViewer.Common, 
          Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
    
    </buildProviders>
    
    ...

    with:

    XML
    ...
    <httpHandlers>
    
    <add path="Reserved.ReportViewerWebControl.axd" verb="*" 
      type="Microsoft.Reporting.WebForms.HttpHandler, 
            Microsoft.ReportViewer.WebForms.Modified"
    
    validate="false" />
    
    </httpHandlers>
    ...
    <buildProviders>
    
    <add extension=".rdlc" 
      type="Microsoft.Reporting.RdlBuildProvider, 
            Microsoft.ReportViewer.WebForms.Modified />
    
    </buildProviders>
    
    ...

Step 8: Modify designer reference

The Report Viewer control uses a designer that adds some custom actions to the standard Visual Studio designer. As you might know, a designer is tied to a control by marking it with DesignerAttribute. The Microsoft.Reporting.WinForms.ReportViewer class (which is the control itself) is marked with this attribute as seen in Reflector:

C#
[Designer("Microsoft.Reporting.WinForms.ReportViewerDesigner, 
    Microsoft.ReportViewer.Design, Version=8.0.0.0, 
    Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(IDesigner))]

Note that the first parameter of the attribute constructor is a string representing the name of the designer class and the name of the containing assembly, separated by a comma. Remember, we have merged all Report Viewer assemblies including the designer assembly into one, so this reference becomes invalid, and the designer will fail to load when dropping the control onto a form. To get it working, we have to specify that the designer class is now located in the same assembly. The simplest way of doing that is merely using the overload of the DesignerAttribute constructor that accepts Type and passes typeof(Microsoft.Reporting.WinForms.ReportViewerDesigner). Assuming the designer class is not external anymore, the modified attribute looks as follows in IL:

MSIL
.custom instance void [System]System.ComponentModel.
    DesignerAttribute::.ctor(class [mscorlib]System.Type) = (
01 00 31 4D 69 63 72 6F 73 6F 66 74 2E 52 65 70 // ..1Microsoft.Rep 
6F 72 74 69 6E 67 2E 57 69 6E 46 6F 72 6D 73 2E // orting.WinForms. 
52 65 70 6F 72 74 56 69 65 77 65 72 44 65 73 69 // ReportViewerDesi 
67 6E 65 72 00 00 ) // gner..

Search for DesignerAttribute (there should be only one occurrence over the whole file) and replace it with the above code.

report-viewer-hack/image12.png

Save and close Microsoft.ReportViewer.WinForms.il.

Step 9: Assemble

We are almost there! All we have to do is assemble our modified IL code.

Run the following commands from the Visual Studio command prompt while in the C:\Work folder:

ilasm Microsoft.ReportViewer.WinForms.Modified.il /dll

ilasm Aspose.Words.ReportingServices.il /dll

Step 10: Test new export formats

It’s time to add our modified control to Visual Studio 2005. Launch the IDE, create a Windows Application project, right click on the Toolbox, and select Choose items. Now, browse to C:\Work and select Microsoft.ReportViewer.WinForms.Modified.dll.

report-viewer-hack/image13.png

The modified Report Viewer control should appear in the current Toolbox group. Drag and drop it onto a form, and finalize your application.

Notice that although Microsoft.ReportViewer.WinForms.Modified.dll is automatically copied to the output directory by default, it won’t happen to Aspose.Words.ReportingServices.dll, and you will have to copy it manually. If you wish to overcome this problem, you can choose between at least two options I believe:

  • Resign the assemblies with your own public key and install them to GAC. Note, you have to update the references accordingly when editing the IL code of the assemblies.
  • Merge the two assemblies into one large assembly.

After the modified Report Viewer is added to your application, you will notice that the appearance, behavior, and other properties of the control remain absolutely similar to those of the original one, and the only difference is a bunch of new formats appearing in the export formats dropdown:

report-viewer-hack/image14.png

Congratulations, you are now able to export your reports to Microsoft Word formats in the Report Viewer control!

Adding export formats dynamically using private Reflection

After the article was published, one of its readers (Bravo Niner) suggested a great idea that helps simplify the process dramatically. Actually, there is a much easier way of achieving the goal - why not just use private Reflection instead of hacking the control? That way, you can inject a rendering extension into the list dynamically, whenever you need. Of course, if you use a third-party rendering extension (such as Aspose.Words for Reporting Services), you still need to modify it so that it refers to the Microsoft.ReportViewer.Common.dll assembly, where the IRenderingExtension interface is defined. That means, you still have to:

  1. Disassemble Aspose.Words.ReportingServices.dll to IL (see Step 2).
  2. Remove the public key (see Step 3).
  3. Modify references (see Step 4, but note this time, you need to add a reference to Microsoft.ReportViewer.Common.dll as we don't merge/hack/do anything else to the Report Viewer assemblies).
  4. Remove mangled resources (see Step 5).
  5. Assemble the IL back to Aspose.Words.ReportingServices.dll (see Step 9).

However, if you create your own rendering extension from scratch, you don't need to hack anything at all; just make sure it implements the IRenderingExtension interface located in the Microsoft.ReportViewer.Common.dll assembly.

After you have your rendering extension ready, implement the following method:

C#
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:

C#
AddExtension(ReportViewer1, "DOC - Word Document via Aspose.Words", 
             typeof(Aspose.Words.ReportingServices.DocRenderer));

I didn't find any disadvantages to this technique compared to the steps described above... it does work! So, I strongly recommend it unless you are a fan of IL and hacking :)

License

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