Crystal Reports Viewer with MVC3
If you don't mind some hacking it's actually pretty easy. (Assuming CR4VS2010)
First add a WebForms page to your project and add the crystal reports viewer control to it.
Verify it added references to:
CrystalDescisions.CrystalReports.Engine
,
CrystalDescisions.ReportSource
,
CrystalDescisions.Shared
,
and CrystalDescisions.Web
.
Then add a PageRoute to your application leading to the newly added page.
Finally, and this was the biggest pain the BLANK, you'll need to make Crystal's Image Handler work. There are many supposed ways, both around the net and here at SO, none of them really worked for me so I resorted to cheating:
public class CrystalImageHandlerController : Controller
{
//
// GET: /CrystalImageHandler.aspx
public ActionResult Index()
{
return Content("");
}
protected override void OnActionExecuted(ActionExecutedContext filterContext)
{
var handler = new CrystalDecisions.Web.CrystalImageHandler();
var app = (HttpApplication)filterContext.RequestContext.HttpContext.GetService(typeof(HttpApplication));
if (app == null) return;
handler.ProcessRequest(app.Context);
}
}
Add a route to this controller as /CrystalReportsImageHandler.aspx, this is where CR expects it handler to be. This can also be used in Areas, just change the handler and page routes as needed.
Bear in mind you will not be able to use your Razor layouts. So you'll need to resort to other means to get visual continuity. (I used IFrames)
In our projects we are returning the report directly in PDF format. We chosed to not use both WebForms and MVC in the same project mainly because of keepping the codebase clean.
The reports are generated against a "dumb" dataset created manually and with data filled by a service class that retreive all the information needed through NHibernate (because we're using a layer of ORM persistence and DB abstraction, the Crystal Reports must not connect directly to the database).
If your project does not have the necessity of showing the report in the browser as a "preview mode", here is the custom ActionResult that I wrote for this kind of situation:
public class PdfReportResult : ViewResult
{
/// <summary>
/// Crystal Report filename
/// </summary>
public string reportFileName { get; set; }
/// <summary>
/// DataSet used in the report
/// </summary>
public DataSet reportDataSet { get; set; }
/// <summary>
/// Report parameters
/// </summary>
IDictionary<string, object> parameters { get; set; }
public PdfReportResult(string reportFileName, DataSet reportDataSet, IDictionary<string, object> parameters)
{
if (string.IsNullOrEmpty(reportFileName))
throw new ArgumentException("Report filename not informed", "reportFileName");
if (reportDataSet == null)
throw new ArgumentException("DataSet not informed", "reportDataSet");
if (parameters == null)
this.parameters = new Dictionary<string, object>();
else
this.parameters = parameters;
this.reportDataSet = reportDataSet;
this.reportFileName = reportFileName;
}
public PdfReportResult(string reportFileName, DataSet reportDataSet)
: this(reportFileName, reportDataSet, null)
{ }
public override void ExecuteResult(ControllerContext context)
{
if ( context == null )
throw new ArgumentNullException("context");
if ( string.IsNullOrEmpty(this.ViewName) )
this.ViewName = context.RouteData.GetRequiredString("action");
// Alias to make the code more legible
HttpResponseBase response = context.HttpContext.Response;
// Generating the report
using ( ReportDocument report = new ReportDocument() )
{
// Load the report
report.Load(context.HttpContext.Server.MapPath("~/Reports/" + reportFileName));
// Set the dataset
report.SetDataSource(reportDataSet);
// Set the parameters (if any)
foreach (var parameter in parameters)
report.SetParameterValue(parameter.Key, parameter.Value);
// Send back the PDF to the user
using ( MemoryStream oStream = (MemoryStream)report.ExportToStream(ExportFormatType.PortableDocFormat) )
{
response.Clear();
response.Buffer = true;
response.AddHeader("Content-Disposition", this.ViewName + ".pdf");
response.ContentType = "application/pdf";
response.BinaryWrite(oStream.ToArray());
response.End();
}
}
}
}
Since crystal report is a server control, we need a webpage/usercontrol to display the report. And never put this webform/user control inside views folder in mvc, you will get broken buttons with 404 in CrViewer. You can also use an Iframe in a razor view to display the report. Following is a working model[VS2010], please go through.
Step-1: Setup Crystal Report
1. Create top level folder in website root directory.
2. Put your Crystal report.rpt file in this folder
3. Add a web page (.aspx) to this folder. This page serves as report viewer page. Add a CrystalReportViewer control in this page.
<div align="center" style="width:100%; height:100%;">
<CR:CrystalReportViewer ID="crViewer" runat="server" AutoDataBind="true" />
</div>
Following assembly registration will be added on the top of aspx page.
<%@ Register Assembly="CrystalDecisions.Web, Version=13.0.2000.0, Culture=neutral, PublicKeyToken=692fbea5521e1304" Namespace="CrystalDecisions.Web" TagPrefix="CR" %>
Check the version of CrystalReportViewer. For this, select “choose items” from Toolbox on VS Sidebar. Compare this version with CrystalDecisions.Web version on top of the aspx page. If both are same, leave it, else change the assembly registration version same as of CRViewer.
Go to web.config file under website root folder; check the assemblies starting with ‘CrystalDecisions’ under tag. Change their versions same as of CrystalReportViewer Version (here Version=13.0.2000.0).
Step-2: Set Up Controller, Action & View
1. Add a new action in report controller class.
2. Write necessary steps to load data from database/files.
3. Set the data into Session.
4. Do not add view for this action. Instead use Response.Redirect method.
public class ReportController : Controller
{
public ActionResult reportView(string id)
{
Session["ReportSource"] = GetdataFromDb();
Response.Redirect("~/Reports/WebForm1.aspx");
return View();
}
}
Add page load event to the .aspx page.
protected void Page_Load(object sender, EventArgs e) { CrystalDecisions.CrystalReports.Engine.ReportDocument report = new CrystalDecisions.CrystalReports.Engine.ReportDocument(); report.Load(Server.MapPath("~/Reports/CR_report.rpt")); report.SetDataSource(Session["ReportSource"]); crViewer.ReportSource =report; }
Step-3: Little hack in Global.asax
1. To avoid “Session state has created a session id, but cannot save it because the response was already flushed by the application.” error or a "blank crystal report page output" add following code in Global.asax.
void Session_Start(object sender, EventArgs e)
{
string sessionId = Session.SessionID;
}
Now you can call the reportView() action in ReportController to display the Crystal Report.
Have a nice day!