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.

48 Comments

  1. Pingback: Using SSRS In ASP.NET MVC Application | Sergey Barskiy's Blog

  2. very nice item, however since i am very much new to SSRS a little more help i need – i am getting an error like – “could not fine ContactContactsList : rsItemNotFound”, i think the report rdl file is what I am missing – can you please add the detail on the rdl file that i need to deploy on the report server.

  3. Pingback: Using SSRS In Angular / ASP.NET MVC Application | Sriramjithendra Nidumolu

  4. Hi
    I am new to entity frame work and and asp.net MVC, when i ran the AngularSSRS application i am getting bellow error

    “Migrations is enabled for context ‘ContactsContext’ but the database does not exist or contains no mapped tables. Use Migrations to create the database and its tables, for example by running the ‘Update-Database’ command from the Package Manager Console.”

    Can please provide the information for how to resolve the above error.

    • Hi,

      I have extracted the zip file and ran VS 2013 in administrator mode. I had also set up SSRS server on my local machine.

      Now in order to create the database from code first through Entity Framework, I installed the EF through nuget package.

      However when I gave Enable-Migrations command on the PM console it gave the following error:
      “No context type was found in the assembly ‘AngularAspNetMvc.Data’.”

      I could see dbContext in DataAccess project but it would try look at the above and give this error evrytime.

      Do you have a solution for this?

      Cheers,
      Kaushik

  5. Just follow the instructions in the message. Open up package manager console window, select the project where database context is and run update-database. You might want to read up on migrations though, or you will be struggling.

  6. The auth cookie should go with the request. You can scrub the info from the cookie in the server. However, the reports themselves are setup in SSRS to use a single login. You can change that if you want.

  7. Hi, Got every thing working except the report. cant seem to get that last piece connected.
    i get this message “The request failed with HTTP status 404: Not Found.” and the following highlighted
    “Line 48: mainReportViewer.ServerReport.SetParameters(parametersCollection)”
    will appreciate your assistance very much.
    Thanks

  8. Pingback: Using SSRS In Angular / ASP.NET MVC Application...

  9. I got this error “Migrations is enabled for context ‘ContactsContext’ but the database does not exist or contains no mapped tables. Use Migrations to create the database and its tables, for example by running the ‘Update-Database’ command from the Package Manager Console”
    even when my database is created with update-database command in DataAccess project. The connection strings in config files are identical.

  10. Pingback: SSRS Local Reports in Angular Apps on Azure Web Sites | Sergey Barskiy's Blog

  11. Hi Sergy,
    I tried adding parameter for different reports.. I m using this solution with webApi and writing a method with linq query and converting result to XML in a controller which will be a dataset to SSRS.. Could you help me here in finding how to get the single and multiple parameter, through our solution to webApi

    Datasource format : http://localhost/api//
    Dataset format : catalog{}/cd – which will be of XML format from above datasource

    • I am not sure I understand the question. The way I have done it is by logging all parameters and their values to some report request table in the database. When you setup SSRS report object, you would get one or more parameters and the values using the initial request, then set them on the report.

    • The problem is that viewer control itself is asp.net. So at least the viewer portion has to be hosted in a separate web app. The rest of the code should just compile in core

  12. Hi,

    In order to create the database from code first through Entity Framework, I installed the EF through nuget package.

    However when I gave Enable-Migrations command on the PM console it gave the following error:
    “No context type was found in the assembly ‘AngularAspNetMvc.Data’.”

    I could see dbContext in DataAccess project but it would try look at the above and give this error evrytime.

    Do you have a solution for this?

    Cheers,
    Kaushik

  13. Hi Sergey,

    nice article.

    I created an empty with report builder 3.0 ContactsList.rpt and still having the following issue.

    {Microsoft.Reporting.WebForms.ReportServerException: The item ‘/mcupryk/Contacts/ContactsList’ cannot be found. (rsItemNotFound)
    at Microsoft.Reporting.WebForms.ServerReportSoapProxy.OnSoapException(SoapException e)
    at Microsoft.Reporting.WebForms.Internal.Soap.ReportingServices2005.Execution.RSExecutionConnection.ProxyMethodInvocation.Execute[TReturn](RSExecutionConnection connection, ProxyMethod`1 initialMethod, ProxyMethod`1 retryMethod)
    at Microsoft.Reporting.WebForms.Internal.Soap.ReportingServices2005.Execution.RSExecutionConnection.LoadReport(String Report, String HistoryID)
    at Microsoft.Reporting.WebForms.ServerReport.EnsureExecutionSession()
    at Microsoft.Reporting.WebForms.ServerReport.SetParameters(IEnumerable`1 parameters)
    at AngularAspNetMvc.Web.ViewsStatic.ReportForm.Page_Load(Object sender, EventArgs e) in d:\AngularSSRS\AngularAspNetMvc.Web\ViewsStatic\ReportForm.aspx.cs:line 61
    at System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender, EventArgs e)
    at System.Web.UI.Control.OnLoad(EventArgs e)
    at System.Web.UI.Control.LoadRecursive()
    at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)}

  14. Hi,
    I started to implement ssrs in our angular application and the core of my design was the same as your design , this make me more happy and trust on the design
    the bad thing that I didn’t read this article from the beginning 🙂
    and the only core different that I send a token in the first request to insure that this user is authorized to see this report (I have table report permission) before generating the request ID
    and I didn’t save the partial views in DB instead it is a partial view for each data type

    really your article is very informative

Leave a Reply

Your email address will not be published. Required fields are marked *