Friday, May 11, 2018

SSRS Reports in an Angular App (Part 1 - Showing the Report)

I started a new job a few weeks ago as a senior C# developer. Although I prefer to work in the full stack (front-end all the way back to the database), I liked the opportunity overall. Within two days, however, my company's needs shifted and I was back doing full stack development with Angular 5 (YAY!). Unfortunately, the first project I was assigned to involves displaying some SSRS reports inside of an Angular application. I don't have to write the reports, but we had some pretty non-negotiable requirements put on this project and finding the right solution has been... interesting.

First, the requirements.

  • Display some reports from an internal SSRS instance without exposing the SSRS instance itself to the public
  • Print the report using the native browser dialog
  • Download a PDF of the report directly from the page
    • When downloading the PDF, provide an option to name the PDF something unique
  • Any interactivity (drilldowns, tooltips, etc.) on the original report should still be available in on the page
So this already seems pretty daunting. Like, how-badly-do-I-want-this-job daunting. Because we use Scrum and this is pretty obviously an epic, we broke it down into smaller, more doable pieces.
  1. Display a list of all reports in SSRS
  2. Allow the user to click on a single report to view the report
  3. Allow the user to print the report
  4. Allow the user to download the report
Now this looks easier. The first item (display a list of all reports in SSRS) ended up being more of a design task than much coding. That's because the team responsible for building the reports (which luckily did not include me or I really might have just quit right then) provided us a stored procedure that returns the information we needed about the reports that were available.
  1. Display a list of all reports in SSRS
  2. Allow the user to click on a single report to view the report
  3. Allow the user to print the report
  4. Allow the user to download the report
The second item was always going to be the trickiest piece. How do we show a report from SSRS without exposing the SSRS server to the public? Well, it turns out that SSRS offers a nifty web service we'd be able to access to get the data we want. You can read more about that in the official docs here. Great, we were going to access a SOAP-based web service... from .Net Core 2.0. Oops. .Net Core is great (so great), but it didn't initially have support for accessing SOAP services. In another lucky break they added that feature recently so I was able to - rather simply - right-click on Dependencies in my project and choose Add Connected Service. This auto-generated the reference.cs file I needed to get the data from the service. Awesome. I tried several different approaches (including this really cool, but not-quite-what-I-needed open source project from a guy named Alan Juden).

OK, so we're going to access the SSRS Web Service from an intermediary RESTful Web API, then access the Web API from Angular to display... what? Oh, we need to figure out how to get the actual report (not the data, but the actual report itself) from the web service. That's possible, but it's not intuitive and it caused me a significant amount of heartburn over the course of a week. Here's the breakdown (including code).

First, we have to instantiate the service client:
   1:  HttpBindingBase httpBinding = new HttpBindingBase
   2:  {
   3:      MaxReceivedMessageSize = int.MaxValue,
   4:      Security = 
   5:          {
   6:              Mode = BasicHttpSecurityMode.TransportCredentialOnly,
   7:              Transport = 
   8:              {
   9:                  ClientCredentialType = HttpClientCredentialType.Ntlm
   10:             }
   11:         }
   12:  };
   13:  EndpointAddress endpointAddress = new EndpointAddress("http://<ssrs-server>/reportserver/reportexecution2005.asmx");
   14:  ReportExecutionServiceSoapClient client = new ReportExecutionServiceClient(httpBinding, endpointAddress);

Once we have the service client, we need to load the report. Even though this method is called LoadReport, it doesn't actually give us the report back yet. We'll do that later.
   15:  LoadReportResponse loadedReport = await client.LoadReportAsync(new TrustedUserHeader(), "path/to/report", null);

Now that we've loaded the report we can assign parameters to it. Let's say the report has a parameter called Name (because I want to keep this simple). We'll first create an instance of ParameterValue, then add that instance to our call.
   16:  ParameterValue parameter = new ParameterValue {Name = "Name", Value = "Mickey"};
   17:  await client.SetExecutionParametersAsync(loadedReport.ExecutionHeader, new TrustedUserHeader(), new[] {parameter}, "en-us");

You can add more parameters if you need to, but I'm trying to keep this simple. You can also skip adding parameters altogether if your report doesn't have any or if you just want to use whatever defaults you've setup.
Now that we've added parameters (or not, depending on the usage) we need to render the report. But it's not as simple as just calling a method named Render. No, we first have to create a RenderRequest.
   18:  RenderRequest renderRequest = new RenderRequest(loadedReport.ExecutionHeader, new TrustedUserHeader(),
         "HTML5", @"<DeviceInfo><Toolbar>False</Toolbar></DeviceInfo>");

Now that we have the RenderRequest built we can finally render the report.
   19:  RenderResponse renderResponse = await client.RenderAsync(renderRequest);

The RenderResult object has a Result property on it that is a byte array. If you wanted to get the report as a PDF for display or download or something then the byte array is probably what you want. If you retrieved the report as HTML then you probably want to convert the byte array into a string. The byte array is UTF8 encoded so you might do something like this:
   20:  byte[] bytes = renderResult.Result;
   21:  string content = Encoding.UTF8.GetString(bytes);

With all of that done, we've finally retrieved our report from the SSRS Web Service in HTML5 format (YAY!!!).

Before we move on to showing the report in Angular, I want to mention a few things about the RenderRequest parameters we used. You can get a list of available formats to request from here. You can see all the options you can specify in the DeviceInfo string here.

Now that I have the report contents in HTML I need to actually display the report to my user. The "best" (I use that term loosely here) way to do this was to put the markup into an iframe. This is where I learned that an iframe can be used without a src attribute (at least in Chrome, I'm not sure about other browsers) by setting the content explicitly. So that's what I did. It looked like this:
   1:  bindIframe(content: string) {
   2:    this.reportContent = content;
   3:    const iframe = <HTMLIFrameElement>document.getElementById('report');
   4:    iframe.contentDocument.write(content);
   5:  }

That bound the markup returned from the service into the iframe, which is exactly what I wanted, but the iframe was tiny. By default, an iframe will size itself to something like 200px by 200px. I could have set the size of the iframe in CSS, but each report had a different height that I didn't know ahead of time. With new reports being added all the time, the only approach that made sense was to resize the iframe after I bound the contents. Here's how I did that:
   1:  bindIframe(content: string) {
   2:    this.reportContent = content;
   3:    const iframe = <HTMLIFrameElement>document.getElementById('report');
   4:    iframe.contentDocument.write(content);
   5:    iframe.height = iframe.contentDocumentbody.scrollHeight.toString();
   6:    iframe.width = iframe.contentDocumentbody.scrollWidth.toString();
   5:  }

This is exactly what I needed to do! Now I'm able to display the HTML from SSRS inside an iframe in my Angular app.
  1. Display a list of all reports in SSRS
  2. Allow the user to click on a single report to view the report
  3. Allow the user to print the report
  4. Allow the user to download the report

I started clicking through the reports to view each of them and I noticed that some of them looked really... wrong. There were lines that should have been below graphs and charts that were sitting on top of those graphs and charts. Images weren't large enough. Some of the reports just looked really bad and I had no idea why.
I'll dig into what I found (and how I fixed it) in the next post. I honestly don't know when I'll get to writing it, but it's on my list.

Update: All four parts of this series are complete now.
  1. SSRS Reports in an Angular App (Part 1 - Showing the Report) (this post)
  2. SSRS Reports in an Angular App (Part 2 - Showing the Report (correctly))
  3. SSRS Reports in an Angular App (Part 3 - Printing the Report)
  4. SSRS Reports in an Angular App (Part 4 - Downloading the Report)

3 comments:

  1. Reporting Services requires credentials be passed to it. How did you do that? I see that you're using an intermediary web service to call SSRS, but I'm going to have to create a webservice that isn't hosted on the same machine as the SSRS service and is on a different network.

    ReplyDelete
  2. Also can you include the complete source code in a zip file? As developers we always skip parts of what we did thinking it is intuitive. It's not believe me.

    ReplyDelete
  3. Great work!!! Any source code available??

    ReplyDelete