Tuesday, September 14, 2010

Add Interactivity to ASP.NET ReportViewer

In one of our projects I had a task to add interactivity to charts located on (MS Report Server) reports that were viewed in ASP.NET web application using ReportViewer control.

Charts I worked with contained date series and user should had have the ability to add notes to data in series by clicking on that data on chart.

In the final solution I used jQuery to find images (charts) in rendered report HTML that had special value encoded in image's alt tag. Client javascript (which was on the same page as ReportViewer control) made AJAX requests to server to calibrate chart axes. Then calibration result was used on client side to build HTML MAP with AREAs to which jQuery onclick handlers were attached.

Here's how working example looked in browser:



Client side implementation was written in pure jQuery with help of jQuery qTip plugin and takes around 450 lines of code. Nothing interesting.

The most interesting part in this approach was getting chart image from generated report on server side.

To get image on server side I used the same mechanism that ReportViewer web control uses. I must to say that I couldn't do this without Red Gate's .Net Reflector (I used free version, it was enough here). This is really helpful tool that makes (nearly all) .Net libraries open sourced. I highly recommend it.

Browser obtains chart image by URL, so I had to pass that URL to server side to grab that image from there.

            function calibrate($image) {
$.ajax({
type: "POST",
contentType: "application/json; charset=utf-8",
url: options.calibrateAxesUrl,
data: "{imageUrl: '" + $image.attr("src") + "'}",
dataType: "json",
success: function(msg) {
calibrationResult = msg.d;

// process calibration result...
}
});
}


As you see I used image's src tag to get image URL and invoked web method CalibrateAxes (which is a public static method with [WebMethod] annotation) of my ASP.NET page (the full URL looks like '<%= ResolveClientUrl("~/Reporting/ReportViewer.aspx") + "/CalibrateAxes" %>').

CalibrateAxes method gets chart image as bitmap and does some bitmap analysis to form the result:


        [WebMethod]
public static CalibrationResult CalibrateAxes(string imageUrl)
{
byte[] image = RenderImage(imageUrl);

using (var stream = new MemoryStream(image))
using (var bmp = new Bitmap(stream))
{
var result = new CalibrationResult();

// analyze bitmap...

return result;
}
}

private static byte[] RenderImage(string imageUrl)
{
var imageUri = new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + imageUrl);

var parameters = HttpUtility.ParseQueryString(imageUri.Query);

var reportSession = parameters["ReportSession"];
var controlID = parameters["ControlID"];
var culture = parameters["Culture"];
var uiCulture = parameters["UICulture"];
var reportStack = parameters["ReportStack"];
var streamID = parameters["StreamID"];

var rs = new ReportExecutionService
{
Url = ConfigurationManager.AppSettings["ReportExecutionServiceUrl"],
Credentials = new NetworkCredential(
ConfigurationManager.AppSettings["ReportServerUserName"],
ConfigurationManager.AppSettings["ReportServerUserPassword"],
ConfigurationManager.AppSettings["ReportServerUserDomain"]),
ExecutionHeaderValue = new ExecutionHeader {ExecutionID = reportSession}
};

string deviceInfo = GetDeviceInfo(reportSession, controlID, culture, uiCulture, reportStack);

string encoding;
string mimetype;

return rs.RenderStream("HTML4.0", streamID, deviceInfo, out encoding, out mimetype);
}

private static string GetDeviceInfo(string reportSession, string controlID, string culture, string uiCulture, string reportStack)
{
var writer = new StringWriter();
var xmlWriter = new XmlTextWriter(writer);
xmlWriter.WriteStartElement("DeviceInfo");
var url = CreateUrl(reportSession, controlID, culture, uiCulture, reportStack);
xmlWriter.WriteElementString("StreamRoot", url);
xmlWriter.WriteEndElement();
return writer.ToString();
}

private static string CreateUrl(string reportSession,
string controlID,
string culture,
string uiCulture,
string reportStack)
{
var uriBuilder = new UriBuilder(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority));
var applicationPath = HttpContext.Current.Request.ApplicationPath;
if (!applicationPath.EndsWith("/", true, CultureInfo.InvariantCulture))
{
applicationPath = applicationPath + "/";
}
applicationPath = applicationPath + "Reserved.ReportViewerWebControl.axd";
applicationPath = HttpContext.Current.Response.ApplyAppPathModifier(applicationPath);
uriBuilder.Path = applicationPath;

var builder = new StringBuilder();
builder.AppendFormat("{0}={1}", "ReportSession", reportSession);
builder.AppendFormat("&{0}={1}", "ControlID", HttpUtility.UrlEncode(controlID));
builder.AppendFormat("&{0}={1}", "Culture", culture);
builder.AppendFormat("&{0}={1}", "UICulture", uiCulture);
builder.AppendFormat("&{0}={1}", "ReportStack", reportStack);
builder.Append("&OpType=ReportImage&StreamID=");
uriBuilder.Query = builder.ToString();

return uriBuilder.Uri.PathAndQuery;
}


This code of getting rendered image from report turned to be very generic, so you may also use it in your web applications.