This article is the first in a series of articles that I hope to publish about creating some Reporting Services WebParts. These webparts will be used to display Reporting Services reports as SharePoint WebParts. The articles assume that you have a working knowledge of SharePoint, WebParts, Reporting Services, and JavaScript.
In this article on Reporting Services webparts I want to setup some of the background information that will be used in creating the different parts. One of the first challenges to using Reporting Services (RS) in a WebPart is that RS uses a web service to manipulate the reports. There are some authentication issues that you will face if you try to call these web services directly from your webpart's code. The specific issue you will hit is called the double-hop and it is caused by the way Windows Authentication works. Mainly, Windows Authentication does not actually send your credentials over the wire but instead does some challenge/response stuff to authenticate the user, all of which is way outside the scope of this article and my expertise. So the route that these articles will take will be to make the web service calls directly from the client using some client side script.
If you're not already familiar with web services, SOAP, and javascript, you may need to read up a little before this all makes sense. But even if you don't understand it you can probably still work your way through the article and get it to work.
So our first task is to figure out how to make web service calls via client side script. Fourtunately for us a really big company has already figured out how to do this. Microsoft uses client side script in a lot of their web parts in order to call the SharePoint web services. This serves as a good starting point for us and their code is pretty easy to modify to fit our needs. To take a look at their code you need to browse to the “[c:\]Program Files\Common Files\Microsoft Shared\web server extensions\60\TEMPLATE\LAYOUTS\1033“ directory on your SharePoint computer. In that directory you will need to look at two files: IE55UP.js and IE50UP.js. There are a few diferences between these and which one you use will depend on what browser your org is using. I will be using the IE55UP.js for these articles but feel free to post comments about any differences you find.
In the IE55UP.js file you will find a procedure called SPSoapRequestBuilder. This procedure is the one used to called the SharePoint (SP) web services. It is important to note the namespaces used in the soap call: http://microsoft.com/sharepoint/webpartpages. This is one of the main changes we will make in order to use this same procedure to call the RS web service. Below is the procedure that the RS webparts will be using:
function RSSoapRequestBuilder(functionName)
{
var object = new Object();
function AddParameter(parameterName, parameterValue)
{
var index = this.parameterNameList.length;
this.parameterNameList[index] = parameterName;
this.parameterValueList[index] = parameterValue;
}
function SendSOAPMessage(xmlhttp)
{
var funcName = this.functionName;
var paramNames = this.parameterNameList;
var paramValues = this.parameterValueList;
xmlhttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlhttp.setRequestHeader("SOAPAction",
"http://schemas.microsoft.com/sqlserver/2003/12/reporting/reportingservices/"
+ funcName);
var soapData = '<?xml version="1.0" encoding="utf-8"?>' +
'<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' +
'xmlns:xsd="http://www.w3.org/2001/XMLSchema" ' +
'xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">' +
'<soap:Body>' +
'<' + funcName +
' xmlns="http://schemas.microsoft.com/sqlserver/2003/12/reporting/reportingservices">';
for(var i=0; i < paramNames.length; i++)
{
var soapParam = (typeof(paramValues[i]) == "string") ? XmlEncode(paramValues[i]) : paramValues[i];
soapData += '<' + paramNames[i] + '>' + soapParam + '</' + paramNames[i] + '>';
}
soapData += '</' + funcName + '>' +
'</soap:Body>' +
'</soap:Envelope>'
xmlhttp.Send(soapData);
return xmlhttp;
}
object.functionName = functionName;
object.parameterNameList = new Array();
object.parameterValueList = new Array();
object.AddParameter = AddParameter;
object.SendSOAPMessage = SendSOAPMessage;
return object;
}
Notice that we are using a different namespace: http://schemas.microsoft.com/sqlserver/2003/12/reporting/reportingservices/. This is the namespace for the RS web service. Using this function we can now create the javascript object that will be used to call the RS web service. You will also need the XmlEncode function which is just the String2XML function from the SP script but renamed so that we don't run into conflicts and the Xml check was added so that we could pass in complex parameter types.
function XmlEncode(Value)
{
if (Value.substring(0,1) == '<') return Value;
var XmlString = "";
var re = /&/g;
XmlString = Value.replace(re,"&");
re = /</g;
XmlString = XmlString.replace(re,"<");
re = />/g;
XmlString = XmlString.replace(re,">");
re = /"/g;
XmlString = XmlString.replace(re,""");
re = /'/g;
XmlString = XmlString.replace(re,"'");
return XmlString;
}
This function just encodes any characters that would cause our Xml Soap request to be invalid.
To get started on our Visual Studio project, you will need to first download and install the WebPart Project Template from the SharePoint MSDN website. Once you have that downloaded and installed you can run through the following steps:
1) Start Visual Studio and create a new WebPart Library (name it something useful). You can create either a C# or a VB project. My code will be in C#, but I can provide the VB code if you get stuck in the translation.
2) Delete the WebPart1.cs and WebPart1.dwp files from the project.
3) Create a new folder called Scripts and add a new Javascript file called rs.js. Copy/Paste the two functions (XmlEncode and RSSoapRequestBuilder) into this new file and save it.
Now that we have the script to call, we need to make it easy for our webparts to call it. So we will add a utility class called Globals (style borrowed from .Text) that will encapsulate this for us. Here is what my Globals class looks like. You can Copy/Paste this into a new class called Globals in a new folder called Utility.
using System;
namespace Bml.RsWebParts.Utility
{
/// <summary>
/// Utility class containing common functions
/// </summary>
public sealed class Globals
{
/// <summary>
/// Static methods only, so use a private
/// constructor
/// </summary>
private Globals()
{}
/// <summary>
/// Property ResourceMap (String)
/// </summary>
public static string ResourceMap
{
get
{
return "/wpresources/Bml.RsWebParts/";
}
}
public static string GetResourceScriptTags(string scriptFileName)
{
return string.Format("<script language='javascript' src='{0}{1}'></script>",
ResourceMap,
scriptFileName);
}
}
}
Now you will have to modify what the ResourceMap function returns based on two things: (1) what the name of your assembly is and (2) how you deploy your assembly. When you deploy your assembly the extra files you include with it (such as script files) will get deployed into a folder that has the same name as your assembly. If you deploy your webpart using the STSADM tool, then you can just use the “/wpresources/[Your Assembly Name]” format. If you get fancy and create an installer tool then you might need to tweak this to fit where you install your assembly to. For now just change it to “/wpresources/[Your Assembly Name]“.
The next step is to create your first webpart which is going to be a very simple example so that this article isn't too long. So in your VS project select Add New Item and then select Web Part. Name it RsReportInfo. Next delete everything inside the class body so that you have an empty class with no variables, methods, or properties.
The first thing we are going to add is three properties (two of which we will eliminate later) that we will use to setup this webpart. Below is the C# code that you need to add to your class.
#region Properties
private string serverUrl = string.Empty;
private string reportPath = string.Empty;
private string propertyList = "Name|Description";
[Browsable(
true),
Category("Reporting Services"),
DefaultValue(""),
WebPartStorage(Storage.Shared),
FriendlyName("Server URL"),
Description("Server url such as http://localhost/reportserver")]
public string ServerUrl
{
get {return serverUrl;}
set {serverUrl = value;}
}
[Browsable(
true),
Category("Reporting Services"),
DefaultValue(""),
WebPartStorage(Storage.Shared),
FriendlyName("Report Path"),
Description("Report path such as /SampleReports/Product Line Sales")]
public string ReportPath
{
get {return reportPath;}
set {reportPath = value;}
}
[Browsable(true),
Category("Reporting Services"),
DefaultValue(""),
WebPartStorage(Storage.Shared),
FriendlyName("Property List"),
Description("List of properties to display such as Name|Description")]
public string PropertyList
{
get {return propertyList;}
set {propertyList = value;}
}
#endregion
These three properties will allow us to configure the webpart to display information about a particular report. The property list is a pipe separated list of properties that should be displayed. The properties that you can display for a report are found in the documentation here (if you have RS installed). Next we are going to add four overriden methods and one private method. Below are these methods along with a short description of what they do.
public override ToolPart[] GetToolParts()
{
ToolPart[] toolparts = new ToolPart[2];
WebPartToolPart wptp = new WebPartToolPart();
CustomPropertyToolPart custom = new CustomPropertyToolPart();
toolparts[1] = wptp;
toolparts[0] = custom;
custom.Expand(0);
return toolparts;
}
This method will put our custom properties on the top of the property list and will expand it. This section is really optional but it is worth adding since it makes configuring the webpart much easier.
protected override void OnLoad(EventArgs e)
{
Page.RegisterClientScriptBlock("rs", Globals.GetResourceScriptTags("rs.js"));
base.OnLoad (e);
}
We register our client script block when the Load event fires. We use the name “rs“ since the rs.js file is shared by all the RS webparts. If another webpart has already loaded the rs.js script it won't be loaded again.
protected override void OnPreRender(EventArgs e)
{
if (serverUrl.Length > 0 && reportPath.Length > 0)
{
Page.RegisterStartupScript("rs_info" + base.Qualifier, GetStartupScript());
}
base.OnPreRender (e);
}
We register a startup script during the PreRender event. This will be important later when the ServerUrl and ReportPath properties can be set by other webparts. For now we are just creating our startup script using the properties set in the property window.
protected override void RenderWebPart(HtmlTextWriter output)
{
if (serverUrl.Length > 0 && reportPath.Length > 0)
{
output.AddAttribute(HtmlTextWriterAttribute.Class, "ms-WPBody");
output.AddAttribute(HtmlTextWriterAttribute.Id, "divInfo");
output.AddAttribute(HtmlTextWriterAttribute.Style, "overflow:auto;");
output.RenderBeginTag(HtmlTextWriterTag.Div);
output.RenderEndTag(); // div
}
}
Here is where our webpart is actually rendered. Our webpart is really just a div tag that will get populated by the client side script which we will create next.
private string GetStartupScript()
{
StringBuilder sb = new StringBuilder();
string[] properties = propertyList.Split('|');
sb.Append("<script language='javascript'>");
sb.Append(Environment.NewLine);
sb.AppendFormat("GetReportInfo('{0}', '{1}', '", serverUrl, reportPath);
foreach(string prop in properties)
{
sb.AppendFormat("<Property><Name>{0}</Name></Property>", prop);
}
sb.Append("');"); // ends the GetReportInfo method call
sb.Append("</script>");
sb.Append(Environment.NewLine);
return sb.ToString();
}
This client side script passes in a call to the GetReportInfo method. This method needs to be added to our rs.js file (and is shown below). The properties parameter of the GetProperties web method takes an array of Property objects. If you're wondering how I figured this out you can take a look at the RS WSDL file (if you don't know what WSDL is then you probably don't want to look at the file). In the WSDL file you can see the type of XML that needs to be passed for this parameter by looking at the schema.
Finally here is the GetReportInfo method that needs to be added to the rs.js file.
function GetReportInfo(serverUrl, reportPath, properties)
{
var returnList = '';
try
{
var xmlhttp = null;
try
{
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP.4.0");
}
catch(e)
{
xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
}
xmlhttp.open("POST", serverUrl + '/reportservice.asmx', false);
var soapBuilder = RSSoapRequestBuilder("GetProperties");
soapBuilder.AddParameter("Item", reportPath);
soapBuilder.AddParameter("Properties", properties);
soapBuilder.SendSOAPMessage(xmlhttp);
returnList = xmlhttp.responseText;
}
catch(e)
{
alert(e.description);
return null;
}
try
{
var xmldom = new ActiveXObject("Msxml2.DOMDocument");
xmldom.async = false;
xmldom.loadXML(returnList);
//did we get a soap error?
isSoapError = xmldom.selectSingleNode(".//faultstring");
if(isSoapError)
{
alert(isSoapError.text);
return false;
}
var nodeList = xmldom.selectNodes(".//Property");
var output = '';
for(var i = 0; i < nodeList.length; i++)
{
output += "<b>" +
nodeList[i].selectSingleNode("Name").text +
"</b>: " +
nodeList[i].selectSingleNode("Value").text +
"<br>";
}
if (output.length == 0) output = "No information available.";
divInfo.innerHTML = output;
}
catch(e)
{
alert(e.description);
}
return false;
}
The only tasks that remain are to create the DWP file, modify the manifest, and to deploy the webpart. The DWP file is added by right clicking the project and selecting Add New Item. Select Web Part DWP for the item type and give it the same name as your class file. Below is the DWP for my web part (your file should look similar).
<?xml version="1.0" encoding="utf-8"?>
<WebPart xmlns="http://schemas.microsoft.com/WebPart/v2" >
<Title>Rs Report Info</Title>
<Description>Displays information about a report.</Description>
<Assembly>Bml.RsWebParts</Assembly>
<TypeName>Bml.RsWebParts.RsReportInfo</TypeName>
</WebPart>
The next step is to modify the manifest file to include all the information about your new webpart and its resources. You will need to add the rs.js file as a resource so that it will be deployed with the webpart. Below is my manifest file (and again your file should look similar).
<?xml version="1.0"?>
<WebPartManifest xmlns="http://schemas.microsoft.com/WebPart/v2/Manifest">
<Assemblies>
<Assembly FileName="Bml.RsWebParts.dll">
<ClassResources>
<ClassResource FileName="rs.js"/>
</ClassResources>
<SafeControls>
<SafeControl
Namespace="Bml.RsWebParts"
TypeName="*"
/>
</SafeControls>
</Assembly>
</Assemblies>
<DwpFiles>
<DwpFile FileName="RsReportInfo.dwp"/>
</DwpFiles>
</WebPartManifest>
Finally you will need to add the setup project to your existing project. Right click the solution and select add new project and choose CAB Project under Setup and Deployment Projects. Name it something useful since this will be the name you will see when using the STSADM tool. Next right click your setup project and select Add Project Outputs. Select primary output and content files (select both by holding down ctrl). Back in your webpart project right click rs.js and select properties. In the properties window change the Build Action to Content. Do the same thing for the manifest.xml and the DWP file. Build the project and as long as you don't have any build errors you're ready to deploy the project to a test server.
Copy the cab file from either the debug or release folder in the setup project's folder (using windows explorer) to your server. The next step will involve using the STSADM tool which is a very simple tool once you understand it. In order to make using this tool easier I add “c:\Program Files\Common Files\Microsoft Shared\web server extensions\60\BIN” to the path environment variable on the server (right click My Computer, select Propreties, Advanced Tab, Environment Variables, find the PATH variable and add the path above but put a semicolon between it and the last path in the variable). Once you've added that path you can just open a command window and type “stsadm -o addwppack -filename c:\path_to_your_cab_file\your_cab_file.cab” and your webpart should now be installed.

Here is a screenshot of the webpart in action. Clearly there is a lot more we could do with this, but for now this is a good start. Hopefully this has given you a good start on using RS and SP webparts together.
Article version 1.0
Updated: 4/27/2004