How to Specify a Filename For Your Exported Reports in Microsoft Dynamics CRM 2011

As you probably already know, there’s no built-in way to specify the filename of a report you want to export to PDF, Excel, Word, etc. Then a colleague pointed me to this article.
 

First off, the code for the below solution can be downloaded here.

This one took a while to figure out so I hope this saves people a few hours of tinkering. As you probably already know, there’s no built-in way to specify the filename of a report you want to export to PDF, Excel, Word, etc. At first I thought of attempting to modify the viewer.aspx file by inserting Javascript there. It probably is possible to do it that route, but many people would be averse to attempting to do such a thing. Then a colleague pointed me to this article, which uses Javascript to rename the file using SSRS’s URL parameter FileName.

We decided to try out the code, so we added it as a web resource and placed the setHandler function in the onload event of the Quote form. Testing it out, it worked without a hitch, however, I wasn’t too fond of using setTimeout in Javascript; from past experience, using a paradigm that depends on timing is a mistake. So, I went about rewriting the code to utilize an event-driven approach (onload, onreadystatechange, etc.). After several hours, I finally understood why Alex (who wrote the above article) used setTimeout in his code:

  1. When viewer.aspx loads, it first loads a blank HTML file (/_static/blank.htm) into an iframe.
  2. Javascript is then used to dynamically build the HTML within the iframe using an object called reportViewer.
  3. When you export a report, a URL is dynamically generated based on the ExportUrlBase property of the reportViewer object.

The key here is to modify the ExportUrlBase property because that is what contains the FileName URL parameter. You can inspect ExportUrlBase in the report window using IE8’s Developer Tools (hit F12):

document.getElementById("resultFrame").contentWindow.$find("reportViewer")._getInternalViewer().ExportUrlBase

Which should result to something similar the following:

/Reserved.ReportViewerWebControl.axd?ReportSession=<SOME_SESSION_ID>&Culture=1033&CultureOverrides=True&UICulture=1033&UICultureOverrides=True&ReportStack=1&ControlID=<SOME_CONTROL_ID>&OpType=Export&FileName=<REPORT_FILE_NAME>&ContentDisposition=OnlyHtmlInline&Format=

Given the above, attempting to set the ExportUrlBase when the iframe finishes loading or when the DOM is ready won’t work (specifically because everything is generated via Javascript); in fact, the onload for the iframe will fire, but remember that the source for the iframe is a blank document! Unfortunately, the best way to tackle this is to use timing to ensure that the reportViewer object finally gets loaded. So, in the end, I did end up using a timing function but in this case, I used setInterval instead of a recursive setTimeout; in addition, I also utilized regex to change the FileName value, both of which resulted in a simpler approach.

To use the script below, create a new Web Resource in CRM and set the onload event for the form to setReportFilename. For convenience, you can download the script here.

function setReportFilename() {
  var newFileName = Xrm.Page.getAttribute("quotenumber").getValue(); // Change to anything you want
  window.open = function (open) {
    return function (url, name, features) {
      var newWindow = open(url, name, features);    // Set to variable so we can run additional Javascript
      if(url.indexOf("viewer.aspx") > 0) {          // Make sure it's the viewer.aspx file
        var inter = window.setInterval(function() { // Start the timer to keep looking for the reportViewer object
          try {
            var fr = newWindow.document.getElementById("resultFrame").contentWindow;
            var rv = fr.$find("reportViewer");
            var r = rv._getInternalViewer();
            r.ExportUrlBase = r.ExportUrlBase.replace(/&FileName=.+?&/, "&FileName=" + newFileName + "&")
            window.clearInterval(inter);            // Found it, stop the timer
          }
          catch (err) {
            return;
          }
        }, 100);
      };
      return newWindow;
    };
  }(window.open); // Override the window.open function without polluting global namespace
};