Možná znáte reportovací nástroj Microsoftu - SQL Server Reporting Services. Jeho samostatná komponenta Microsoft Report Viewer ale kromě využití serverového řešení obsahuje i část Local Report. Pomoci Local Report je možné vyvářet reporty (tiskové sestavy) v .NET aplikacích bez nutnosti Report Serveru nebo dalších komponent. Report lze buď tisknout, zobrazovat do náhledu nebo exportovat do Wordu, Excelu a PDF. A to je právě ideální řešení jak v nejen Desktop ale i Webových aplikacích generovat výstupy do těchto formátů.
Ukážeme si postup vytvoření jednoduché Webové ASP.NET aplikace, která bude pomoci tiskové sestavy vytvářet výstup dat do PDF.
Report Viewer
Jak už jsem psal, jediné co potřebujeme na počítač (v případě ASP.NET na webový server) doinstalovat je komponenta Report Viewer. Ta se instaluje pomoci samostatného balíčku Report Viewer Redistributable. Ale pozor jakou verzi použijete, od Visual Studia 2005 (verze 8) nám skoro s každou verzí Visual Studia (kromě té poslední VS 2013) přišla i nová verze této komponenty.
Poslední je tedy verze 11, její balíček Microsoft Report Viewer 2012 Runtime - ReportViewer.msi můžete stáhnout zde. K němu je ještě potřeba součást Microsoft SQL Server 2012 Feature Pack - balíček Microsoft System CLR Types for SQL Server 2012 zde (x64) nebo zde (x86).
Založení projektu
Do existujícího nebo nového projektu Webové aplikace (vystačíme si klidně jen s Empty project šablonou) přidáme reference na Report Viewer. Jedná se o tyto assembly:
Microsoft.ReportViewer.Common.dll (C:\Windows\assembly\GAC_MSIL\Microsoft.ReportViewer.Common\11.0.0.0__89845dcd8080cc91\Microsoft.ReportViewer.Common.dll)
Microsoft.ReportViewer.WebForms.dll (C:\Windows\assembly\GAC_MSIL\Microsoft.ReportViewer.WebForms\11.0.0.0__89845dcd8080cc91\Microsoft.ReportViewer.WebForms.dll)
Datový objekt
Report může jako datový zdroj používat přímo databázi, Local Reports kromě toho ale můžeme napojit i na libovolný .NET objekt. To může být výhodné, pokud v projektu již takové objekty (modely) používáme, například při použití Entity Framework apod. Napojením reportu na tyto objekty tak zajistíme jednotný přístup k datům jak pro reporty, tak pro zbytek aplikace. Ukážeme si tento způsob.
Já jsem si pro můj testovací projekt vytvořil tuto třídu modelu:
public class Person
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int? Age { get; set; }
}
Pozn.: Všimněte si, že jednu vlastnost mám nullable a nebude problém jí v Reporting Services použít. To je velká výhoda, vzpomínám si že například takový Crystal Reports ani ve verzi 13 (verze pro VS 2010) nullable typy nepodporoval.
Dále si vyrobíme třídu, přes kterou bude report k datům přistupovat. Třída nesmí být static a musí mít výchozí konstruktor:
namespace LocalReportSample.Reports
{
public class TestReportDataSource
{
#region action methods
public IEnumerable<Person> GetPersons()
{
//Make some test data
return new List<Person>
{
new Person() { ID = 1, FirstName = "John", LastName= "Smith", Age = 23 },
new Person() { ID = 2, FirstName = "Steven", LastName= "Thompson", Age = 48 },
new Person() { ID = 3, FirstName = "Alice", LastName= "Walters" },
new Person() { ID = 4, FirstName = "Thomas", LastName= "Bradley" },
new Person() { ID = 5, FirstName = "Edward", LastName= "Wideman", Age = 36 },
new Person() { ID = 6, FirstName = "Megan", LastName= "Gibson" }
};
}
#endregion
}
}
Aby šla připravená metoda GetPersons v reportu použít, musí být instanční a musí vracet IEnumerable datového objektu. V reálné aplikaci by v této metodě bylo volání například LINQ dotazu při použití EF datové vrstvy apod.
Vytvoření reportu
Nyní do projektu přidáme report (.rdlc soubor) – položka Report (nebo Report Wizard) v sekci Reporting dialogu Add New Item. Dále v reportu zvolíme volbu New / Dataset. Pokud vytváříte report proti datovému objektu, je možné, že jako já narazíte na následující problém. V případě, že report přidáváme přímo do ASP.NET projektu, tak dialog vyvolaný tlačítkem New... nabízí rovnou vytvoření připojení k databázi a nelze vybrat typ datového zdroje. Je proto potřeba dostat naší třídu s datovým zdrojem přímo do výběru Data source/Available datasets. Mě se to povedlo přidáním do projektu nejprve alespoň jedné Web Form aspx stránky (nevím jak se to řeší v MVC). Po zkompilování projektu již výběr Data source nabízí příslušný namespace a metodu GetPersons v mojí třídě TestReportDataSource.
V případě přidávání reportu do projektu typu Class Library se při vytváření datového zdroje (tlačítkem New…) zobrazí navíc výběr typu datového zdroje (Databáze, Služba, Objekty, SharePoint). Zde pak vybereme typ Object.
Po vytvoření datového zdroje reportu již můžeme podle potřeby vytvořit jeho design, v kontrolu tablix přitom vybereme pojmenovaný Dataset.
Volání reportu
Pro volání reportu si vytvoříme pomocnou třídu ReportDocument, která provede načtení definice reportu a volání exportu v daném formátu.
/// <summary>
/// Podporované formáty pro export reportu
/// </summary>
public enum ReportExportFormat
{
/// <summary>
/// Export do MS Excelu (.xls)
/// </summary>
Excel = 1,
/// <summary>
/// Export do PDF
/// </summary>
PDF = 2,
/// <summary>
/// Export do MS Wordu (.doc)
/// </summary>
Word = 3,
/// <summary>
/// Export do MS Excelu (.xlsx)
/// </summary>
ExcelOpenXml = 4,
/// <summary>
/// Export do MS Wordu (.docx)
/// </summary>
WordOpenXml = 5
}
/// <summary>
/// Třída pro načtení a operace s reportem
/// </summary>
public class ReportDocument
{
#region member varible and default property initialization
private string m_ReportName;
/// <summary>
/// WebForms <c>LocalReport</c> object
/// </summary>
public LocalReport LocalReport { get; private set; }
/// <summary>
/// Kolekce parametrů reportu
/// </summary>
public ReportParameterCollection Parameters { get; private set; }
#endregion
#region constructors and destructors
/// <summary>
/// Inicializace reportu
/// </summary>
/// <param name="reportName">Název reportu</param>
/// <param name="reportsAssembly"><see cref="Assembly"/> obsahující definice reportů</param>
/// <param name="reportsNamespace">Namespace definic reportů</param>
/// <param name="localReport">WebForms <c>LocalReport</c> object (volitelný)</param>
public ReportDocument(string reportName, Assembly reportsAssembly, string reportsNamespace, LocalReport localReport = null)
{
if (reportName == null)
{
throw new ArgumentNullException("reportName");
}
if (reportsAssembly == null)
{
throw new ArgumentNullException("reportsAssembly");
}
if (reportsNamespace == null)
{
throw new ArgumentNullException("reportsNamespace");
}
if (reportsNamespace.Length == 0)
{
throw new ArgumentException("reportsNamespace is empty string", "reportsNamespace");
}
if (localReport == null)
{
localReport = new LocalReport();
}
localReport.EnableExternalImages = true;
//Načtení reportu
localReport.LoadReportDefinition(GetReportDefinition(reportName, reportsAssembly, reportsNamespace));
m_ReportName = reportName;
this.LocalReport = localReport;
this.Parameters = GetParameters(localReport.GetParameters());
if (this.Parameters.ContainsKey("ReportName"))
{
this.Parameters["ReportName"].Value = reportName;
}
}
#endregion
#region action methods
/// <summary>
/// Export reportu do <see cref="byte"/> array
/// </summary>
/// <param name="outputFormat">Výstupní formát</param>
/// <returns><see cref="byte"/> array s exportovaný reportem</returns>
public byte[] Export(ReportExportFormat outputFormat)
{
if (outputFormat == 0)
{
throw new ArgumentException("Invalid outputFormat", "outputFormat");
}
//Synchronizace kolekce parametrů do reportu
SetParameters(this.LocalReport, this.Parameters);
Warning[] warnings;
string[] streamids;
string mimeType;
string encoding;
string extension;
return this.LocalReport.Render(outputFormat.ToString(), null, out mimeType,
out encoding, out extension, out streamids, out warnings);
}
#endregion
#region property getters/setters
/// <summary>
/// Název reportu
/// </summary>
public string ReportName
{
get { return m_ReportName; }
}
/// <summary>
/// Titulek reportu
/// </summary>
public string ReportTitle
{
get { return this.LocalReport.DisplayName; }
}
#endregion
#region private member functions
private static System.IO.Stream GetReportDefinition(string reportName, Assembly reportsAssembly, string reportsNamespace)
{
if (!reportName.EndsWith(".rdlc", StringComparison.Ordinal) && !reportName.EndsWith(".rdl", StringComparison.Ordinal))
{
reportName = reportName + ".rdlc";
}
return reportsAssembly.GetManifestResourceStream(reportsNamespace + "." + reportName);
}
private static ReportParameterCollection GetParameters(ReportParameterInfoCollection parameterInfoCollection)
{
var parameters = new ReportParameterCollection();
foreach (var parameterInfo in parameterInfoCollection)
{
var param = new ReportParameter()
{
ParameterName = parameterInfo.Name,
AllowBlank = parameterInfo.AllowBlank,
Nullable = parameterInfo.Nullable,
DataType = (ParameterDataType)parameterInfo.DataType,
MultiValue = parameterInfo.MultiValue,
Prompt = parameterInfo.Prompt,
PromptUser = parameterInfo.PromptUser,
ErrorMessage = parameterInfo.ErrorMessage,
Visible = parameterInfo.Visible
};
param.SetValueInternal(parameterInfo.Values);
parameters.Add(parameterInfo.Name, param);
}
return parameters;
}
private static void SetParameters(LocalReport localReport, ReportParameterCollection parameters)
{
localReport.SetParameters(from p in parameters.Values
select new Microsoft.Reporting.WebForms.ReportParameter(p.ParameterName, p.GetValueInternal(), p.Visible));
}
#endregion
}
Pro jednodušší práci s parametry reportu ještě třída používá třídy ReportParameter a ReportParameterCollection, ty najdete v přiloženém příkladu.
Http Handler
Export reportu budeme v ASP.NET aplikaci volat přes HttpHandler. Způsobů je více (Generic Handler, ASP.NET Handler), já jsem si ale oblíbil ten, vytvořit handler jako třídu a její registraci provést přes RouteTable. Již jsem to popisoval například zde, provedeme to takto:
- Do projektu přidáme pomocnou třídu RoutingExtensions, zdroj najdete v přiloženém příkladu (implementace třídy původně pochází z blogu Phil Haacka zde).
- Do konfigurace Routes (v Global.asax nebo RouteConfig.cs) vložíme tuto registraci:
//Register routes
RouteTable.Routes.MapHttpHandler<ReportHttpHandler>("ReportHttpHandler", "test-report.pdf");
- Protože jako URL pro handler používám jinou než ASP.NET koncovku (v tomto případě přímo .pdf), aby se nám handler zavolal, musíme do web.config přidat toto nastavení:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true" />
</system.webServer>
- A nyní již vytvoříme třídu handleru ReportHttpHandler odvozenou z IHttpHandler.
Implementace třídy ReportHttpHandler může být následující:
/// <summary>
/// Handler pro export sestavy do PDF
/// </summary>
public class ReportHttpHandler : IHttpHandler
{
#region action methods
/// <summary>
/// Enables processing of HTTP Web requests by a custom HttpHandler that implements the System.Web.IHttpHandler interface.
/// </summary>
/// <param name="context">An System.Web.HttpContext object</param>
public void ProcessRequest(HttpContext context)
{
context.Response.Cache.SetExpires(DateTime.MinValue);
bool open = context.Request.QueryString["Export"] != "1";
string reportUrl = context.Request.Url.Segments[context.Request.Url.Segments.Length - 1];
Byte[] data = null;
string exportFileName = null;
if (reportUrl.StartsWith("test-report.", StringComparison.OrdinalIgnoreCase))
{
exportFileName = "TestReport.pdf";
data = GenerateTestReport();
}
if (data == null)
{
context.Response.StatusCode = 404;
context.Response.StatusDescription = "Not Found";
return;
}
DownloadHandlerUtil.WriteFileToResponse(context.Response, data, exportFileName, !open, "application/pdf");
}
/// <summary>
/// Gets a value indicating whether another request can use the System.Web.IHttpHandler instance.
/// </summary>
public bool IsReusable
{
get { return true; }
}
#endregion
#region private member functions
private Byte[] GenerateTestReport()
{
var report = new ReportDocument("TestReport", System.Reflection.Assembly.GetExecutingAssembly(), "LocalReportSample.Reports");
report.LocalReport.DataSources.Add(new ReportDataSource("Persons", new TestReportDataSource().GetPersons()));
return report.Export(ReportExportFormat.PDF);
}
#endregion
}
Handler nejprve rozhodne, na který report byl zavolán (mohl by obsloužit volání více reportů) a poté pomoci metody GenerateTestReport vrátí na výstup vyrenderovaný PDF dokument. Výstup je vrácen pomocnou třídou DownloadHandlerUtil, která zajistí správné nastavení hlavičky content-disposition a ContentType pro PDF soubor, třída je včetně popisu dostupná v článku Nastavení hlaviček pro download souboru v HTTP handleru.
Vlastní metoda GenerateTestReport nejprve vytvoří objekt ReportDocument pro TestReport. Dále se do kolekce DataSources nastaví datový objekt vrácen metodou GetPersons. Zde je důležité aby uvedené jméno v DataSources kolekci (parametr name, zde hodnota ”Persons”) odpovídalo jménu zvolenému při vytváření datasetu v reportu v okně Dataset Properties. Pokud by report používal více datových zdrojů, tak je nutné zde nastavit všechny.
Také by tento handler mohl v případě potřeby zpracovat předané parametry (buď z parametru routy nebo z querystringu) a použít je při načítání datového zdroje, nebo pro nastavení Value report parametrů v kolekci ReportDocument.Parameters.
Testovací stránka
A máme hotovo, posledním úkolem je vytvoření volání handleru pro otestování. Já jsem v mém příkladu použil WebForms stránku Default.aspx s následujícími odkazy:
<form id="form1" runat="server">
<div>
<a href="~/test-report.pdf" runat="server">Open</a>
<a href="~/test-report.pdf?Export=1" runat="server">Export</a>
</div>
</form>
Celý příklad je možné stáhnout zde: