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:
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));
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:
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:
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:
Now, add a reference to Microsoft.ReportViewer.WinForms.Modified.dll:
.assembly extern Microsoft.ReportViewer.WinForms.Modified
{
}
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].
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.
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:
.assembly extern Aspose.Words.ReportingServices
{
}
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):
.method public hidebysig virtual instance class
[mscorlib]System.Collections.Generic.IEnumerable`1
<class Microsoft.Reporting.LocalRenderingExtensionInfo>
ListRenderingExtensions() cil managed
{
.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,
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,
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)
...
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)
...
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
}
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:
- Locate the following method:
.method assembly hidebysig static bool
ConfigContainsHandler(class [System.Configuration]
System.Configuration.Configuration config)
and change the HTTP handler reference from:
IL_0064: ldstr "Microsoft.Reporting.WebForms.HttpHandler, Microsof"
+ "t.ReportViewer.WebForms, Version=8.0.0.0, Culture=neutral, PublicKeyTok"
+ "en=b03f5f7f11d50a3a"
to
IL_0064: ldstr "Microsoft.Reporting.WebForms.HttpHandler, Microsof"
+ "t.ReportViewer.WebForms.Modified"
- Locate the class:
.class private abstract auto ansi sealed
beforefieldinit Microsoft.Reporting.WebForms.Constants
extends [mscorlib]System.Object
and replace the following field:
.field public static literal string HttpHandlerTypeName =
"Microsoft.Reporting.WebForms.HttpHandler, Microsof"
+ "t.ReportViewer.WebForms, Version=8.0.0.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a"
to
.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:
...
<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:
...
<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:
[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:
.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
6F 72 74 69 6E 67 2E 57 69 6E 46 6F 72 6D 73 2E
52 65 70 6F 72 74 56 69 65 77 65 72 44 65 73 69
67 6E 65 72 00 00 )
Search for DesignerAttribute
(there should be only one occurrence over the whole file) and replace it with the above code.
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.
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:
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:
- Disassemble Aspose.Words.ReportingServices.dll to IL (see Step 2).
- Remove the public key (see Step 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).
- Remove mangled resources (see Step 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:
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));
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 :)