Viewing And Printing Reports in a Silverlight Application
Reports are an important part of a Silverlight Line of Business (LOB) application. While Silverlight 4 does provide printing capabilities, multi-page printing is still challenging. In addition, a lot of businesses have already invested considerable resources into learning to produce reports using the report designer in Visual Studio.
While there are some 3rd party controls that will display a report in Silverlight, they cost money. Also, some require you to use the full Microsoft SQL Server Reporting Services (SSRS), and that may not be an option for a lot of people. An .rdlc report created with Visual Studio does not require SSRS.
The method we will demonstrate in this article was developed by Sushant Pandey. He describes it here. The difference in this implementation is that we will use Behaviors to make it fully MVVM compliant (he uses code behind in his implementation).
Printing A Report
We will start with the Simple Silverlight Configurator/Pivot (View Model/MVVM) I published a few months ago.
The application allows the user to change the Gender, Weight, and Age selectors, and it dynamically filters the list of people.
The application has been enhanced to allow the user to click the Show Report Viewer button and the report will show in a popup window.
If they click Print PDF Report, they will see a PDF report in a popup window.
I actually prefer the Report Viewer because the user sees an animation while waiting for the report to render.
Creating the Report
The first step is to add a Report to the project.
The project already contains the following web service:
[WebMethod]
public List<Person> SearchPeople(string Gender, int AgeHigh, int AgeLow,
int WeightHigh, int WeightLow)
{
List<Person> colPeople = new List<Person>();
var result = from objPeople in People.GetPeople().AsQueryable()
where objPeople.Age <= AgeHigh
where objPeople.Age >= AgeLow
where objPeople.Weight <= WeightHigh
where objPeople.Weight >= WeightLow
select objPeople;
if (Gender != "All")
{
colPeople = (from finalresult in result
where finalresult.Gender == Gender
select finalresult).ToList();
}
else
{
colPeople = result.ToList();
}
return colPeople;
}
We are able to select that web service as a dataset for the report.
After designing the report, we place a report control on a .aspx page.
The following code, in the code behind for the page, is used to accept report parameters, and to display the Report Viewer Control (ShowReportViewer()
), or to render a .pdf version (RenderReport()
):
public partial class Report : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if
(
Request.QueryString["Gender"] != null &&
Request.QueryString["AgeHigh"] != null &&
Request.QueryString["AgeLow"] != null &&
Request.QueryString["WeightHigh"] != null &&
Request.QueryString["WeightLow"] != null
)
{
if (Request.QueryString["ShowReportViewer"] == "False")
{
RenderReport();
}
else
{
if (!Page.IsPostBack)
{
ShowReportViewer();
}
}
}
}
#region RenderReport
private void RenderReport()
{
if (LoadData().Rows.Count > 0)
{
LocalReport localReport = new LocalReport();
localReport.ReportPath = Server.MapPath("~/Report1.rdlc");
ReportDataSource reportDataSource =
new ReportDataSource("DataSet1", LoadData());
localReport.DataSources.Add(reportDataSource);
string reportType = "pdf";
string mimeType = "application/pdf";
string encoding;
string fileNameExtension;
string deviceInfo =
"<DeviceInfo>" +
" <OutputFormat>PDF</OutputFormat>" +
" <PageWidth>8.5in</PageWidth>" +
" <PageHeight>11in</PageHeight>" +
" <MarginTop>0.5in</MarginTop>" +
" <MarginLeft>1in</MarginLeft>" +
" <MarginRight>1in</MarginRight>" +
" <MarginBottom>0.5in</MarginBottom>" +
"</DeviceInfo>";
Warning[] warnings;
string[] streams;
byte[] renderedBytes;
renderedBytes = localReport.Render(
reportType,
deviceInfo,
out mimeType,
out encoding,
out fileNameExtension,
out streams,
out warnings);
Response.Clear();
Response.ContentType = mimeType;
"attachment; filename=foo." + fileNameExtension);
Response.BinaryWrite(renderedBytes);
Response.End();
}
else
{
Response.Clear();
"attachment; filename=foo." + fileNameExtension);
Response.Write("No Data");
Response.End();
}
}
#endregion
#region ShowReportViewer
private void ShowReportViewer()
{
this.ReportViewer1.ProcessingMode = ProcessingMode.Local;
this.ReportViewer1.LocalReport.ReportPath = Server.MapPath("~/Report1.rdlc");
ReportViewer1.LocalReport.DataSources.Add(
new ReportDataSource("DataSet1", LoadData()));
}
#endregion
The following method provides data for the report at run-time:
#region LoadData
private DataTable LoadData()
{
string Gender = Convert.ToString(Request.QueryString["Gender"]);
int AgeHigh = Convert.ToInt32(Request.QueryString["AgeHigh"]);
int AgeLow = Convert.ToInt32(Request.QueryString["AgeLow"]);
int WeightHigh = Convert.ToInt32(Request.QueryString["WeightHigh"]);
int WeightLow = Convert.ToInt32(Request.QueryString["WeightLow"]);
var result = from objPeople in People.GetPeople().AsQueryable()
where objPeople.Age <= AgeHigh
where objPeople.Age >= AgeLow
where objPeople.Weight <= WeightHigh
where objPeople.Weight >= WeightLow
select objPeople;
if (Gender != "All")
{
result = from finalresult in result
where finalresult.Gender == Gender
select finalresult;
}
Utility objUtility = new Utility();
DataTable dt = objUtility.LINQToDataTable(result);
return dt.DefaultView.Table;
}
#endregion
Note, that it uses a class LINQToDataTable
, that converts the Linq to SQL query into a DataTable
that the report needs.
Also note that we don't actually pass parameters to the Report Viewer control. With a .rdlc report, we simply provide the data for the report. We must filter the Linq query to filter the report.
It may also seem odd that we designed the report directly against the web service method, but we have to supply the data in the code behind. You can see this site for answers as to why the Report Viewer control has been designed this way: http://www.gotreportviewer.com.
The Silverlight Project
In the Silverlight project, we add ReportPDFURL
and ReportViewerURL
properties to the View Model. We also add a SetReportUrls
method that is triggered whenever a supporting property is changed:
private void SetReportUrls()
{
ReportViewerURL = String.Format("{0}Gender={1}&AgeHigh={2}&AgeLow={3}&
WeightHigh={4}&WeightLow={5}&ShowReportViewer=True",
GetBaseAddress(), Gender, AgeHigh.ToString(), AgeLow.ToString(),
WeightHigh.ToString(), WeightLow.ToString());
ReportPDFURL = String.Format("{0}Gender={1}&AgeHigh={2}&AgeLow={3}&
WeightHigh={4}&WeightLow={5}&ShowReportViewer=False",
GetBaseAddress(), Gender, AgeHigh.ToString(), AgeLow.ToString(),
WeightHigh.ToString(), WeightLow.ToString());
}
We create the following Behavior
that will open up the HTML Popup window:
namespace Behaviors
{
[System.ComponentModel.Description("Opens an HTML Window")]
public class HTMLPopupWindow : TargetedTriggerAction<Button>, INotifyPropertyChanged
{
#region PopupURL
public static readonly DependencyProperty PopupURLProperty =
DependencyProperty.Register("PopupURL",
typeof(string), typeof(HTMLPopupWindow), null);
public string PopupURL
{
get
{
return (string)base.GetValue(PopupURLProperty);
}
set
{
base.SetValue(PopupURLProperty, value);
this.NotifyPropertyChanged("PopupURL");
}
}
#endregion
#region PopupWidth
public static readonly DependencyProperty PopupWidthProperty =
DependencyProperty.Register("PopupWidth",
typeof(int), typeof(HTMLPopupWindow), null);
public int PopupWidth
{
get
{
return (int)base.GetValue(PopupWidthProperty);
}
set
{
base.SetValue(PopupWidthProperty, value);
this.NotifyPropertyChanged("PopupWidth");
}
}
#endregion
#region PopupHeight
public static readonly DependencyProperty PopupHeightProperty =
DependencyProperty.Register("PopupHeight",
typeof(int), typeof(HTMLPopupWindow), null);
public int PopupHeight
{
get
{
return (int)base.GetValue(PopupHeightProperty);
}
set
{
base.SetValue(PopupHeightProperty, value);
this.NotifyPropertyChanged("PopupHeight");
}
}
#endregion
protected override void OnAttached()
{
base.OnAttached();
}
protected override void Invoke(object parameter)
{
if (true == HtmlPage.IsPopupWindowAllowed)
{
System.Text.StringBuilder codeToRun = new System.Text.StringBuilder();
codeToRun.Append("window.open(");
codeToRun.Append("\"");
codeToRun.Append(string.Format("{0}", PopupURL));
codeToRun.Append("\",");
codeToRun.Append("\"");
codeToRun.Append("\",");
codeToRun.Append("\"");
codeToRun.Append("width=" + PopupWidth.ToString() +
",height=" + PopupHeight.ToString());
codeToRun.Append(",scrollbars=yes,menubar=no,toolbar=no,resizable=yes");
codeToRun.Append("\");");
try
{
HtmlPage.Window.Eval(codeToRun.ToString());
}
catch
{
MessageBox.Show("You must enable popups to view reports.
Safari browser is not supported.",
"Error", MessageBoxButton.OK);
}
}
else
{
MessageBox.Show("You must enable popups to view reports.
Safari browser is not supported.",
"Error", MessageBoxButton.OK);
}
}
protected override void OnDetaching()
{
base.OnDetaching();
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
#endregion
}
}
Next, we add buttons to the View.
We add the HTMLPopupWindow Behavior to the Buttons.
We then bind the PopupURL
to the appropriate property in the View Model, and set the other parameters.
Not Really Printing In Silverlight
The key thing with this implementation is that we are not actually printing "inside" Silverlight. However, the end user will most likely not care. They will have the functionality they need. As a developer, this method should allow you to deliver a professional solution at a reasonable cost.
Deployment
You will need to use ASP.NET 4.0, and install the Microsoft Report Viewer 2010 Redistributable Package on the web server that displays the report.
History
- 24th October, 2010: Initial post