Is there a way to put a SQL Server Reporting Services report viewer control on an ASP.net MVC view? If not...what is the best way to accomplish this?

Implementing a SSRS ReportViewer control in MVC consists of two problems:

  1. Minimally, you'll need to add the right dependencies, handlers, and configuration for the ReportViewer control (regardless of project type).
  2. The trickier hurdle is in Mixing WebForms and MVC. We need a way of rendering and routing incoming requests so they will be handled by WebForms pages, controls, and actions.

Problem 1 - Configuring the ReportViewer

If you've done a lot with setting up ReportViewer controls in the past, this might be old hat and you can skip to section 2.

  1. Add package/reference - The ReportViewer control lives in the Microsoft.ReportViewer.WebForms.dll. You can include in your project by adding the Microsoft.ReportViewer.WebForms package from nuget:

Nuget - Microsoft.ReportViewer.WebForms

  1. Web.config Handlers - Per this article on Web.config Settings for ReportViewer, and this SO question you'll need to add the following to your web.config:

    <!-- language: lang-xml -->
     <system.web>
       <httpHandlers>
         <add verb="*" path="Reserved.ReportViewerWebControl.axd" 
              type="Microsoft.Reporting.WebForms.HttpHandler,
                    Microsoft.ReportViewer.WebForms, Version=10.0.0.0, Culture=neutral,
                    PublicKeyToken=b03f5f7f11d50a3a" />
       </httpHandlers>
     </system.web>
     <system.webServer>
       <handlers>
         <remove name="ReportViewerWebControlHandler" />
         <add name="ReportViewerWebControlHandler" preCondition="integratedMode"
              verb="*" path="Reserved.ReportViewerWebControl.axd" 
              type="Microsoft.Reporting.WebForms.HttpHandler, 
                    Microsoft.ReportViewer.WebForms, Version=10.0.0.0, Culture=neutral,
                    PublicKeyToken=b03f5f7f11d50a3a"/>
       </handlers>
     </system.webServer>
    

<sup>Per this question on duplicate keys, it's typically easiest to remove and then re-add webserver configs</sup>

  1. Fix broken Image Requests - there's a known defect in ReportViewer with blank.gif images not loading so you can add the following fix to your global.asax.cs:

    <!-- language: lang-cs -->
     protected void Application_BeginRequest(object sender, EventArgs e)
     {
         HttpRequest req = HttpContext.Current.Request;
         if (req.Url.PathAndQuery.StartsWith("/Reserved.ReportViewerWebControl.axd") &&
             !req.Url.ToString().ToLower().Contains("iteration") &&
             !String.IsNullOrEmpty(req.QueryString["ResourceStreamID"]) &&
             req.QueryString["ResourceStreamID"].ToLower().Equals("blank.gif"))
         {
             Context.RewritePath(String.Concat(req.Url.PathAndQuery, "&IterationId=0"));
         }
     }
    
  2. IgnoreRoute .axd - If it's not already there, make sure to allow ScriptResources in your RouteConfig.cs:

    <!-- language: lang-cs -->
     routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    
  3. Add ReportViewerPage.aspx - Add a WebForm page that will hold an instance of the ReportViewer control. In order to work, that control needs to find a ScriptManager control and be placed inside of a <form runat="server" >.
    So your new .aspx page should look something like this:

    <!-- language: lang-xml -->
     <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="ReportViewerPage.aspx.cs" Inherits="MVCAppWithReportViewer.ReportViewerPage" %>
     <%@ Register TagPrefix="rsweb" Namespace="Microsoft.Reporting.WebForms" Assembly="Microsoft.ReportViewer.WebForms, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" %>
     
     <!DOCTYPE html>
     <html xmlns="http://www.w3.org/1999/xhtml">
     <head runat="server">
         <title>Report Viewer</title>
     </head>
     <body>
         <form id="form1" runat="server">
             <rsweb:ReportViewer ID="ReportViewer" runat="server" 
                                 Height="100%" Width="100%" 
                                 SizeToReportContent="True" ProcessingMode="Remote" />
             <asp:ScriptManager ID="ScriptManager1" runat="server" />
         </form>
     </body>
     </html>
    
  4. Wire up ReportViewer on Page_Load - Assuming, you already have an SSRS report fully deployed to a reporting server which is available at an address like this:

    <code>http://<i>ReportServerName</i>/Reports/Pages/Report.aspx?ItemPath=<i>%2fCompany%2f<b>ClientReport</b></i></code>

    Then your code-behind in your new WebForm page should look like this:

    <!-- language: lang-cs -->
     public partial class ReportViewerPage : System.Web.UI.Page
     {
         protected void Page_Load(object sender, EventArgs e)
         {
             if (!Page.IsPostBack)
             {
                 // confirm report properties (also setable in attributes)
                 ReportViewer.ProcessingMode = ProcessingMode.Remote;
                 
                 // config variables
                 var reportServer = "ReportServerName";
                 var reportPath = "/Company/";
                 var reportName = "ClientReport";    
    
                 // report setup
                 var serverReport = new ServerReport();
                 serverReport = ReportViewer.ServerReport;
                 serverReport.ReportServerUrl = new Uri($@"http://{reportServer}/ReportServer");
                 serverReport.ReportPath = $@"{reportPath}{reportName}";
                 
                 // report input
                 var parameters = new List<ReportParameter>();
                 parameters.Add(new ReportParameter("User_uid", "1"));
                 serverReport.SetParameters(parameters);
    
                 // run report
                 serverReport.Refresh();
             }
         }
     }
    
  5. View Report - At this point you should be able to view your report on it's own by selecting View in Browser or <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>W</kbd>

View in Browser

Problem 2 - Mixing WebForms and MVC

First, let's quickly dissect the routing differences between how these controls are loaded and subsequently updated

  • MVC routes will look something like this {controller}/{action}/{id} where the routing engine will automatically find a Controller and Action with the specified name and incoming requests will be handled by that method. On any page request, whether from page load, form submit, button clicks, anchor navigation, or ajax calls, the exact method being executed is always specified in the url {action}.

  • WebForms routes to code by finding the physical .aspx page address, and then uses ViewState & PostData to wire up and fire events on that page / control.

    Here's an illustration of different routing formats in WebForms. And here's a simple button click event which will submit a post back to the parent page and raise the appropriate events within the page based on the event data submitted:

    ASP.NET WebForms - Postback

This is a pretty big constraint on our solutions available. Nothing is special about the ReportViewer control. It's just a sophisticated set of UserControl classes that respond to click and other input events by posting back the current address along with the ViewState and Event info. So whatever assumptions were baked into the routing and navigation of the ReportViewer will need to persist into our MVC wrapper.

  1. Option 1 - Add Route for .aspx page

As of MVC 4.0+, you can use URL Routing with WebForms. This mixes well with MVC by adding a <code>Map<b><i>Page</b></i>Route</code> (note the Page part) to map a route to a physical file. So add the following to your RouteConfig.cs:

<!-- language: lang-cs -->

    routes.MapPageRoute(
        routeName: "ReportViewer",
        routeUrl: "ReportViewer/{reportName}",
        physicalFile: "~/ReportViewerPage.aspx"
    );

The report will run when you navigate to the address ~/Reports/reportName. This will probably be invoked from inside a controller action, perhaps with some user entered parameters or web.config connection strings. There are lots of ways to manage state in ASP.NET and Pass Values to ASP.NET Web Forms Pages. One option would be to stash the info in the Session and Redirect like this in your controller:

<!-- language: lang-cs -->

    HttpContext.Session[reportSetup.ReportName] = new ReportSetup() {ReportName = "ClientReport"}; //reportSetup;}
    return RedirectToRoute("ReportViewer", new { reportName = reportSetup.ReportName});

Then, inside the .aspx page, and you can grab the reportName from the RouteData Values and any setup params from the session:

<!-- language: lang-cs -->

    // get report name from route
    string reportName = Page.RouteData.Values["reportName"].ToString();

    // get model from session and clear
    ReportSetup setup = (ReportSetup)HttpContext.Current.Session[reportName];

Pros:

  • Most of the routing seems to work by default, and AJAX controls work fine, so you can set AyncRendering=True

Cons:

  • It's hard to use an ASP Web Form with a Razor MVC Layout so rendering will take users out of the flow of the rest of the application.
  • Also, report values have to be exposed as part of the URL or passed indirectly via session (as opposed to hydrating directly onto the object).
  1. Option 2 - Nest .ascx inside PartialView on your Page

Adapted from How can I use a ReportViewer control with Razor?, you can consume .ascx controls in PartialViews as long as they inherit from System.Web.Mvc.ViewUserControl.

Create a new Web Forms User Control called ReportViewerControl.ascx that looks like this:

<!-- language: lang-xml -->


<pre><code>&lt;%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ReportViewerControl.ascx.cs" Inherits="MVCAppWithReportViewer.ReportViewerControl" %&gt;
&lt;%@ Register TagPrefix="rsweb" Namespace="Microsoft.Reporting.WebForms" Assembly="Microsoft.ReportViewer.WebForms, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" %&gt;

&lt;form id="form1" runat="server"&gt;
    &lt;rsweb:ReportViewer ID="ReportViewer" runat="server" 
                        Height="100%" Width="100%"  
                        SizeToReportContent="True" ProcessingMode="Remote"
                        <b>AsyncRendering="False"</b> /&gt;
    &lt;asp:ScriptManager ID="ScriptManager1" runat="server" 
                       <b>EnablePartialRendering="false"</b>  /&gt;
&lt;/form&gt;
</code></pre>

Note: You must set AsyncRendering="False" and EnablePartialRendering="false"

In the code behind you'll need to replace the inheritance type from System.Web.UI.UserControl to System.Web.Mvc.ViewUserControl.

And on Page_Init, you'll need to set the Context.Handler to Page so events are registered properly.

So the ReportViewerControl.ascx.cs should look like this:

<!-- language: lang-cs -->

    public partial class ReportViewerControl : System.Web.Mvc.ViewUserControl
    {
        protected void Page_Init(object sender, EventArgs e)
        {
            // Required for report events to be handled properly.
            Context.Handler = Page;
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            if (!Page.IsPostBack)
            {
                /* ... report setup ... */ 
                serverReport.Refresh();
            }
        }
    }

In order to render the report, add the following to your controller view:

<!-- language: lang-cs -->

    @Html.Partial("ReportViewerControl", Model)

And then in the ReportViewerControl.ascx.cs Page_Load event, you can retrieve the passed in model from the ViewUserControl.Model property like this:

<!-- language: lang-cs -->

    ReportSetup setup = (ReportSetup)Model;

Pros:

  • Can build into master _layout.cshtml and consume in regular views
  • Can pass model directly

Cons:

Further Reading: