|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
OverviewThis 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. IntroductionThe 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 itThe 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. DisclaimerRead 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 itTo 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:
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
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 You can also notice that each rendering extension class derives from the 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 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 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 itNote 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 Step 1: DisassembleLike 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 keySince 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 extensionOur 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 resourcesThere 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
Save and close Aspose.Words.ReportingServices.il. Step 5: AssembleNow, 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 ReflectionThis 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 AddExtension(ReportViewer1, "DOC - Word Document via Aspose.Words",
typeof(Aspose.Words.ReportingServices.DocRenderer));
Step 7: Test new export formatsDone! Just run your project, and you should note there is a new export format that has appeared on the list:
ConclusionAs you can see, using private Reflection has a number of benefits compared to hacking the Report Viewer assemblies:
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||