Sunday, April 22, 2012

Loading and Disposing Crystal Reports on ASP.net Page

Recently one of my friend mentioned strange Crystal Reports behavior on ASP.net page. The scenario was simple,

  1. ASP.net web page
  2. A Button
  3. A CrystalReportViewer Control
  4. A simple Crystal Report

What he doing was, on clicking of button he was loading the report in CrsytalReportViewer control. The report was being displayed correctly, but when he was trying to Zoom In, Zoom Out or Exporting the report he was getting error “No valid report source is available”.

Here is the code he wrote in the Submit button click handler.

C#
protected void btnSubmit_Click(object sender, EventArgs e)
{
    CrystalDecisions.CrystalReports.Engine.ReportDocument rpt;
    rpt = new CrystalDecisions.CrystalReports.Engine.ReportDocument();
    rpt.Load(Server.MapPath("TestReport.rpt"));

    this.CrystalReportViewer1.ReportSource = rpt;
}
Visual Basic
Protected Sub btnSubmit_Click(sender As Object, e As System.EventArgs) Handles btnSubmit.Click
        Dim rpt As CrystalDecisions.CrystalReports.Engine.ReportDocument
        rpt = New CrystalDecisions.CrystalReports.Engine.ReportDocument()
        rpt.Load(Server.MapPath("TestReport.rpt"))

        Me.CrystalReportViewer1.ReportSource = rpt
End Sub

I don’t know the internal working CrystalReportViewer control, but it seems that it is NOT loading the ReportSource property back from ViewState properly. Here is the following that I think is happening internally,

First time when we hit the Submit Button,
  • Page is POSTed back
  • Page_Load event
  • CrystalReportViewer controls’ Load event
  • Submit button Click event: in event handler, we load the report which is then rendered perfectly
Second time when we change the Zoom,
  • CrystalReportViewer control POST back the page using JavaScript
  • Page_Load event
  • CrystalReportViewer control’s Load event: In event handler, it tries to access the ReportSource property without first loading it from ViewState, on this point it does NOT find any ReportSource that’s why it returns back this message.

Note that above is my hypothesis, I don’t know actually how it works. Anyway after this I thought if my hypothesis is correct then we need to load the report on Page_Load event. So that when CrystalReportViewer control’s load event occurs, it should have the ReportSource property properly defined.

So I change changed the flow as,

  • I moved the Report Load code from Submit button click handler to Page_Load event handler
  • But in a way that for the first time report does NOT get displayed
  • Instead, when user hits Submit button, then I mark a flag in a hidden field (using JavaScripting)
  • And then when Page_Load event handler executes, it checks that hidden field for the flag, if flag is there then it loads the report.

Finally it is a good practice to dispose of the Report object in order to avoid any memory issues. Here is the full code of page.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class TestForm : System.Web.UI.Page
{
    CrystalDecisions.CrystalReports.Engine.ReportDocument rptDoc = null;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (this.IsPostBack)
        {
            // we will only display report on post back
            if (this.txtShowReport.Value == "1")
            {
                this.rptDoc = new CrystalDecisions.CrystalReports.Engine.ReportDocument();
                this.rptDoc.Load(Server.MapPath("TestReport.rpt"));

                this.CrystalReportViewer1.ReportSource = this.rptDoc;
                this.CrystalReportViewer1.DataBind();
            }
        }
    }

    protected void Page_Unload(object sender, EventArgs e)
    {
        if (this.rptDoc != null)
        {
            this.rptDoc.Close();
            this.rptDoc.Dispose();
        }
    }
}
Visual Basic
Partial Class TestFormVB
    Inherits System.Web.UI.Page

    Private rptDoc As CrystalDecisions.CrystalReports.Engine.ReportDocument = Nothing

    Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load
        If Me.IsPostBack Then
            If Me.txtShowReport.Value = "1" Then
                Me.rptDoc = New CrystalDecisions.CrystalReports.Engine.ReportDocument()
                Me.rptDoc.Load(Server.MapPath("TestReport.rpt"))

                Me.CrystalReportViewer1.ReportSource = rptDoc
            End If
        End If
    End Sub

    Protected Sub Page_Unload(sender As Object, e As System.EventArgs) Handles Me.Unload
        If Me.rptDoc IsNot Nothing Then
            Me.rptDoc.Close()
            Me.rptDoc.Dispose()
        End If
    End Sub
End Class

In case if anyone wondering how we can set value in a hidden field through JavaScript here is the code, its very simple. You need to add the following markup in the Head Section of your HTML

    <script type="text/javascript">
        function ShowReportFlag() {
            document.getElementById("<%:this.txtShowReport.ClientID %>").value = "1";
            }
    </script>

And the then you need to add the exeuction of this method in your Button

    <asp:Button ID="btnShowReport" runat="server" OnClientClick="javascript:ShowReportFlag();" text="Show Report" />

No comments: