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.
I am using Entity Framework for this project, and you can see classes and configurations in AngularAspNetMvc.Data project:
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:
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">×</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">×</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:
Now about parameters. The demo includes one parameter view – for active flag:
I assign this view inside report parameters table:
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.
Pingback: Using SSRS In ASP.NET MVC Application | Sergey Barskiy's Blog
It’s very informative post. I am .NET developer and i want to learn more about advanced .Net applications and i think this post will definitely help me.
This seems to be what Im looking for, looks great !
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.
Yes, you will need to setup SSRS on your machine and create an RDL report. I added it to the download zip.
Pingback: Using SSRS In Angular / ASP.NET MVC Application | Sriramjithendra Nidumolu
I am very new to asp.net. Your description are damn good and informative. but as I say am very new I am not getting idea what code where to write. I am in which file.
hi,
cant find the download
Link is at the end of the post – http://dotnetspeak.com/Downloads/AngularSSRS.zip
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
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.
I don’t see how you pass credentials to the server to get data from the reporting server, do you have your reporting server pushing data anonymously?
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.
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
You probably did not setup SSRS and deployed the report with proper credentials. The report itself is aprt of the download. The rest of the setup you will need to do yourself though.
Thanks, any pointers to some materials on how i can do that? Please indulge me
This should give you a good start on configuring SSRS. Then just deploy the rdl file using the article’s instructions. You can check the web.config for exact location. You will gain enough basic knowledge to have it done I think. Here is the link http://msdn.microsoft.com/en-us/library/ms159624.aspx
Pingback: Using SSRS In Angular / ASP.NET MVC Application...
hi,
i’m not familiar with the typescript. Is it possible to convert on javascript syntax? though there are converters much prefer if the sample uses javascript. Appreciate your response.
Thanks for this great post.
@Bert
The download will contain compiled JS files as well as TypeScript files.
I am New to ssrs but already i can see how i am gonna implement this on my mvc 5 app
Marvellous !! I’m newbie. Where is the database script sql file for create tables and fill up data ?
It is all part of the download file. It is using EF code first, no tables per se.
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.
Check to make sure _Migrations table exists as well as the database.
This is really good start point. Thanks Sergey!
why you are showing report in overlay dialog. how to show report in existing page?
@tridip You could do that as well, no particular reason, just keeping the user on the same page in case they want to change the criteria and re-run the report
@Sergey i just like to know why u showed the ssrs report in overlay box? is there was any specific reason. thanks
Pingback: SSRS Local Reports in Angular Apps on Azure Web Sites | Sergey Barskiy's Blog
Thanks for the detailed information here and the example solution. Extremely helpful.
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.
Great to see your article….
Looks we are using ContactsDb for all the reports, where can I get that DB for running it in my local?
Is it SQL or Access DB?
Sorry, it was my mistake, I didn’t gone through complete blog. I got it now. We need to run Package Manager console with the command Update-Database -Verbose
It’s possible to implement in asp.net core application.
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
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
By default ef will look in main project for context. Make sure to specify project name. You can look up command line options for enable migrations for specifics
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)}
I think you need to publish the report to the server
Hi,
Any plans to reproduce this in angular2?
Not any time soon. Working on a different project now.
Any pointers as to how I can get that done? I will apprecite thst very much. Got off to great start on this angular 2 project when it suddenly dawned on me I have no idea how to presents reports in angular 2.
Your ASP.NET viewer code stays the same. So the code that will change is moving the code that is in a controller or directive and moving it into an Angular 2 component.
Thanks, I will give it try and hopefully get it working. Appreciate your support
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
thank you so much, it helped me a lot <3