Using SSRS In Angular / ASP.NET MVC Application

I blogged a long time ago about a pattern on how to show reports in an ASP.NET MVC application.  I have received a lot of request to share code, but I have lost the source code when I reimaged my machine.  Rather than recreating this from scratch, I decided to take a more advanced route and do the same but in an Angular app.  I have been working on a production Angular app since summer.  I also continuously educate myself on various concepts of web development, including technologies such as Angular.

My high level goals for the solution are still mostly the same

  • I would like to integrate reports into the existing application
  • I would like to show them in an overlay, not wanting to popup additional browser window and having to deal with popup issues in general
  • I would like to make the report viewing safe, trying to reveal as little as possible to the user or technical observer who could use Fiddler for example.
  • I want to show a list of reports
  • I want the user to select one, specify parameters, then preview the report in SSRS report viewer web control.

 

Here is an outline of my solution

  • I will create reports table in the database with the list of reports.
  • Each report will have parameters collection, stored in reports parameters table
  • Each parameter will have a partial MVC view that collects the data for that parameter.
  • Once parameter data is collected, I will log report print request and its parameter values into report request and report request parameters tables.
  • Once report request is generated, there will be a unique GUID generated and passed back into running Angular application.  The final report viewer page URL is created in format “UrlToReportViewerASPXPage?r=requestGUID”
  • Once the final URL is available, I will popup a dialog with embedded iframe with the src attribute pointing to report URL.
  • ASPX page will parse the URL and will get report GUID.  It will then retrieve report request data, including parameters from the database, and set all the values on report viewer control.
  • My dialog box in Angular app will have to maintain dialog and report iframe sizes to make sure that window resize will not cause problems.

This is a tall order and a big project, but luckily you will be able to download entire solution and look for yourself.  Just look for download link near the bottom of this post.

 

Let’s get started.  My starting point is an existing MVC project that houses my Angular app.  I blogged about why I use MVC views for Angular templates already, so I am not going to repeat this.  I am going to go step by step here.  First of all, here is my diagram of database tables.

image 

I am using Entity Framework for this project, and you can see classes and configurations in AngularAspNetMvc.Data project:

image 

Hence, I am not adding the classes for data tables in this post.  My Report viewer page is mostly still the same, I am just putting it into ViewsStatic folder under my MVC project.  I also use attribute to size the report control to report content, since it looks better this way in my preview window.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ReportForm.aspx.cs" Inherits="AngularAspNetMvc.Web.ViewsStatic.ReportForm" %>

<!DOCTYPE html>

<%@ Register Assembly="Microsoft.ReportViewer.WebForms, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
    Namespace="Microsoft.Reporting.WebForms" TagPrefix="rsweb" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
</head>
<body>
    <form id="reportForm" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server">
        </asp:ScriptManager>
        <div style="overflow: hidden">
            <rsweb:ReportViewer ID="mainReportViewer" SizeToReportContent="True" runat="server">
            </rsweb:ReportViewer>
        </div>
    </form>
</body>
</html>

The code behind file is exactly the same as in previous post, hence not repeated here.  I am using repository pattern for the data access this time though.  My repository is very simple, not much to explain.

using System;
using System.Collections.Generic;
using System.Linq;
using AngularAspNetMvc.Data.Models;

namespace AngularAspNetMvc.DataAccess.Repository
{
    public class ReportsRepository : Core.Repository, IReportsRepository
    {
        public IEnumerable<Report> GetReports()
        {
            return Context.Reports.OrderBy(one => one.ReportName);
        }

        public Report GetReport(int reportId)
        {
            return Context.Reports.Include("ReportParameters").FirstOrDefault(one => one.ReportId == reportId);
        }

        public bool CreateRequest(ReportRequest reportRequest)
        {
            Insert(reportRequest);
            return true;
        }

        public ReportRequest GetRequest(Guid reportRequestId)
        {
            return Context.ReportRequests.Include("ReportRequestParameters")
                .FirstOrDefault(one => one.UniqueId == reportRequestId);
        }
    }
}

The first method gets the report list, second one gets single report, thirds prepares new report request and saves it.  The last method is used by ASPX report viewer page to get the parameters and report data.

Now, let’s switch gears and look at the Angular aspects.  In my report list page I show report name and description and View button:

image

My controller for this view is very simple.  It talks to service to get the data and listens to click event of View button.  The idea behind separating controller from the service is to abstract my business logic (controller) from data access (service). Again, this is a TypeScript app, but the download will include compiled JS files as well.  When all said and down, there are only a few lines of critical code here: call getData and handle click event view viewReport button.

module app.reports.reportsController {

    import IUtilities = app.core.services.IUtilities;
    import IReport = app.reports.models.IReport;
    import IReportsService = app.reports.reportsService.IReportsService;

    interface IReportsScope extends app.core.controllers.ICoreScope {
        reports: IReport[];
        viewReport: (report: IReport) => void;
    }

    class ReportsController extends app.core.controllers.CoreController {

        constructor(reportsService: IReportsService, private utilities: IUtilities, private $scope: IReportsScope, $location: ng.ILocationService) {
            super($scope);

            var getData = () => {
                reportsService.getReports((data: IReport[]) => {
                    $scope.reports = data;
                });
            };

            $scope.viewReport = (report: IReport) => {
                $location.path("/reports/view/" + report.ReportId);
            };

            getData();

        }
    }

    angular.module('app.reports.reportsController', ['app.core.services.utilities', 'app.core.services.http', 'app.reports.reportsService'])
        .controller('app.reports.reportsController', ['app.reports.reportsService', 'utilities', '$scope', '$location',
            function (reportsService: IReportsService, utilities: IUtilities, $scope: IReportsScope, $location: ng.ILocationService) {
                return new ReportsController(reportsService, utilities, $scope, $location);
            }]);
} 

The template for the controller is as follows:

<div>
    <h1>Reports</h1>
</div>
<div>
    <div class="well" data-ng-cloak data-ng-repeat="report in reports">
        <div class="h2">{{report.ReportName}}</div>
        <div class="h6">{{report.ReportDescription}}</div>
        <div>
            <button class="btn btn-primary" data-ng-click="$parent.viewReport(report)">View</button>
        </div>
    </div>
</div>

As you can see, I am using ngRepeat directive.  I am looping through reports property in my scope, putting out a well with name and description, as well as button.  I have to use $parent because ngRepeat creates new scope for each item in the list, and the $parent points to parent scope, which is the scope I am using in the reports controller.  When this button is clicked, I am navigating to individual report route, handled by reportViewController:

module app.reports.reportsViewController {

    import IUtilities = app.core.services.IUtilities;
    import IReportRequest = app.reports.models.IReportRequest;
    import IReportsService = app.reports.reportsService.IReportsService;
    import IGlobals = interfaces.IGLobals;

    interface IReportScope extends app.core.controllers.ICoreScope {
        report: IReportRequest;
        runReport: () => void;
        reportViewUrl: string;
        reportSource: string;
    }

    interface IReportRouteParams extends ng.route.IRouteParamsService {
        id: number;
    }

    class ReportsViewController extends app.core.controllers.CoreController {

        constructor(
            reportsService: IReportsService,
            private utilities: IUtilities,
            private $scope: IReportScope,
            $location: ng.ILocationService,
            $routeParams: IReportRouteParams,
            globalsService: IGlobals) {

            super($scope);

            var getData = (reportId: number) => {
                reportsService.getReport(reportId, (data: IReportRequest) => {
                    $scope.report = data;
                });
            };

            getData($routeParams.id);

            $scope.runReport = () => {
                reportsService.createRequest($scope.report, (result: string) => {
                    if (result) {
                        $scope.report.UniqueId = result;
                        $scope.reportSource = globalsService.baseUrl + 'ViewsStatic/ReportForm.aspx?r=' + $scope.report.UniqueId;
                    }
                });
            };
            $scope.reportViewUrl = undefined;
        }
    }

    angular.module('app.reports.reportsViewController', ['app.core.services.utilities', 'app.reports.reportsService', 'app.globalsModule'])
        .controller('app.reports.reportsViewController', [
            'app.reports.reportsService', 'utilities', '$scope', '$location', '$routeParams', 'globalsService',
            function (
                reportsService: IReportsService,
                utilities: IUtilities,
                $scope: IReportScope,
                $location: ng.ILocationService,
                $routeParams: IReportRouteParams,
                globalsService: IGlobals) {
                return new ReportsViewController(reportsService, utilities, $scope, $location, $routeParams, globalsService);
            }]);
} 

There are a few new things here.  I am getting the report ID from the url using $routeParams service.  Then I am calling my web api controller, passing in report id and getting new report request for that report.  The request returns new request ID – GUID.  This GUID is added to the URL I talked about earlier in this post.  I am simply creating full URL and assigning it to the property on my scope.  Where do you ask the actual code to show the report is?  Since it involves the DOM manipulation, I wrote a directive for this purpose:

    export class ReportDirective extends app.directives.BaseDirective {

        constructor(utilities: IUtilities) {
            super();
            this.restrict = 'A';
            this.scope = {
                reportSource: '=',
                reportName: '='
            };
            this.link = function (scope: IReportScope, element: ng.IAugmentedJQuery) {
                element.html('<div><iframe style="border: transparent"></iframe></div>');
                scope.$watch('reportSource', function(value) {
                    if (value) {
                        var frame = element.find('iframe');
                        frame.attr('src', value);
                        utilities.showMessage(element.html(), true, null, scope.reportName);
                    }
                });
            };
        }
    }

You probably noticed the scope = {…  code.  This is a special syntax you can use in Angular directives.  When you do, the isolated scope in the directive defined there will stay in sync with attributes on that directive:

<div id="reportPreviewDiv" data-report data-report-source="reportSource" data-report-name="report.ReportName" class="hide"></div>

You also noticed that names in the attributes differ from properties on directive’s scope.  They simply follow Angular translation rule with “data-“ being optional.

attribute: data-something-else

property: somethingElse

Now the code in the report view controller is more obvious.  When I set property called reportSource on my controller, my directive gets notified inside $watch method.  There I set src property on iframe and call showMessage method inside my message dialog service (utilities).  Code in utilities is quite complex:

/// <reference path="../../home/interfaces.ts" />
module app.core.services {


    export interface IUtilities {
        showPleaseWait: () => void;
        hidePleaseWait: () => void;
        showMessage: (content: string, isHtml?: boolean, buttons?: IButtonForMessage[], header?: string) => void;
    }

    export interface IButtonForMessage {
        mehtod?: () => void;
        label: string;
    }

    class Utilities implements IUtilities {
        showPleaseWait: () => void;
        hidePleaseWait: () => void;
        showMessage: (content: string) => void;

        constructor(private $window: ng.IWindowService, private globalsService: interfaces.IGLobals) {
            var that = this;
            var pleaseWaitDiv = angular.element(
                '<div class="modal" id="globalPleaseWaitDialog" data-backdrop="static" data-keyboard="false">' +
                '  <div class="modal-dialog">' +
                '    <div class="modal-content">' +
                '      <div class="modal-header">' +
                '         <h1>Processing...</h1>' +
                '      </div>' +
                '      <div class="modal-body" id="globallPleaseWaitDialogBody">' +
                '         <div class="progress progress-striped active">' +
                '           <div class="progress-bar" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">' +
                '           </div>' +
                '         </div>' +
                '        <div class="progress-bar progress-striped active"><div class="bar" style="width: 100%;"></div></div>' +
                '      </div>' +
                '    </div>' +
                '  </div>' +
                '</div>'
                );

            var messageDiv = angular.element(
                '<div class="modal" id="globalMessageDialog" tabindex="-1" role="dialog" data-backdrop="static" data-keyboard="true">' +
                '  <div class="modal-dialog">' +
                '    <div class="modal-content">' +
                '      <div class="modal-header">' +
                '        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>' +
                '        <h4 class="modal-title"></h4>' +
                '      </div>' +
                '      <div class="modal-body">' +
                '      </div>' +
                '      <div class="modal-footer">' +
                '       <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>' +
                '      </div>' +
                '    </div>' +
                '  </div>' +
                '</div>'
                );


            var resize = function (event: JQueryEventObject) {
                var dialog = angular.element('#' + event.data.name + ' .modal-dialog');
                dialog.css('margin-top', (angular.element(that.$window).height() - dialog.height()) / 2 - parseInt(dialog.css('padding-top')));
                resizeHtmlDialog(dialog);

            };

            var animate = function (event: JQueryEventObject) {
                var dialog = angular.element('#' + event.data.name + ' .modal-dialog');
                dialog.css('margin-top', 0);
                var margin = (angular.element(that.$window).height() - dialog.height()) / 2 - parseInt(dialog.css('padding-top'));
                if (margin < 0) {
                    margin = 0;
                }
                dialog.animate({ 'margin-top': margin }, 'fast', function () {
                    if (event.data.name === 'globalMessageDialog') {
                        resizeHtmlDialog(messageDiv.find('.modal-body'));
                    }
                });
                pleaseWaitDiv.off('shown.bs.modal', animate);

            };

            this.showPleaseWait = function () {
                angular.element($window).on('resize', null, { name: 'globalPleaseWaitDialog' }, resize);
                pleaseWaitDiv.on('shown.bs.modal', null, { name: 'globalPleaseWaitDialog' }, animate);
                pleaseWaitDiv.modal();
            };

            this.hidePleaseWait = function () {
                pleaseWaitDiv.modal('hide');
                angular.element($window).off('resize', resize);
            };

            var resizeHtmlDialog = function (element: ng.IAugmentedJQuery) {
                var height = angular.element(that.$window).height() * 0.8;
                var width = angular.element(that.$window).width() * 0.8;
                messageDiv.find('.modal-dialog').css('width', width.toString() + 'px');
                messageDiv.find('.modal-dialog').css('height', height.toString() + 'px');
                var dialog = angular.element('#globalMessageDialog .modal-dialog');
                var margin = (angular.element(that.$window).height() - dialog.height()) / 2 - parseInt(dialog.css('padding-top'));
                console.log(margin);
                var frame = element.find('iframe');
                if (frame.length) {
                    frame.attr("width", width - 100);
                    frame.attr("height", height - 100 - parseInt(angular.element('.modal-dialog').css('margin-top')) / 2);
                }
            };

            this.showMessage = function (content: string, isHtml?: boolean, buttons?: IButtonForMessage[], header?: string) {
                angular.element($window).on('resize', null, { name: 'globalMessageDialog' }, resize);
                if (isHtml) {
                    var element = angular.element(content);
                    messageDiv.find('.modal-body').html(element);
                    resizeHtmlDialog(element);

                } else {
                    messageDiv.find('.modal-dialog').css('width', '');
                    messageDiv.find('.modal-dialog').css('height', '');
                    messageDiv.find('.modal-body').text(content);
                }

                messageDiv.on('shown.bs.modal', null, { name: 'globalMessageDialog' }, animate);
                if (buttons) {
                    messageDiv.find('.modal-header').children().remove('button');
                    var footer = messageDiv.find('.modal-footer');
                    footer.empty();
                    angular.forEach(buttons, function (button: IButtonForMessage) {
                        var newButton = angular.element('<button type="button" class="btn"></button>');
                        newButton.text(button.label);
                        if (button.mehtod) {
                            newButton.click(function () {
                                messageDiv.modal('hide');
                                button.mehtod();
                            });
                        } else {
                            newButton.click(function () {
                                messageDiv.modal('hide');
                            });
                        }
                        footer.append(newButton);
                    });

                } else {
                    messageDiv.find('.modal-header').html('<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button><h4 class="modal-title"></h4>');
                    messageDiv.find('.modal-footer').html('<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>');
                }
                messageDiv.find('.modal-title').text((header || globalsService.applicatioName));
                messageDiv.modal();
            };
        }
    }

    angular.module('app.core.services.utilities', ['app.globalsModule'])
        .factory('utilities', ['$window', 'globalsService', function ($window: ng.IWindowService, globalsService: interfaces.IGLobals) {
            return new Utilities($window, globalsService);
        }]);
}

The key part you will notice is resizing logic.  As the user resizes the browser window, I have to resize message dialog to keep the report view usable.  The end result is:

image

Now about parameters.  The demo includes one parameter view – for active flag:

image

I assign this view inside report parameters table:

image

The last column contains parameter name inside SSRS report.  So, any time I use this parameter for any report, I have to make sure to name report parameter inside SSRS report to match.  Now, all I need is the partial view for this parameter:

<div class="form-group">
    <label>{{parameter.ParameterName}}</label>
    <select data-ng-model="parameter.ParameterValue">
        <option value="1">Active</option>
        <option value="0">Inactive</option>
        <option value="2">All</option>
    </select>
</div>

The important this is that my <select> is bound to the parameter value property.  Now, you need to see how I am putting multiple parameters together into a single view I am using for template to collect all the parameters for the report:

<div>
    <div class="h2">{{report.ReportName}}</div>
    <div class="h6">{{report.ReportDescription}}</div>
    <br />
    <div data-ng-repeat="parameter in report.ReportRequestParameters">
        <div data-ng-include="'@Url.Content("~/Reports/")' + parameter.ParameterViewName"></div>
    </div>
    <br />
    <div>
        <button class="btn btn-primary" data-ng-click="runReport()">View</button>
    </div>
</div>
<div id="reportPreviewDiv" data-report data-report-source="reportSource" data-report-name="report.ReportName" class="hide"></div>

Again, I am using ngRepeat here to loop through parameters collection inside the report object.  I am also using ngInclude directive to inject in partial view for each parameter.

Please study my solution and ask any questions, I am sure there is room for improvement.  You can download entire solution here.

Enjoy.