圖 1 — Crystal Report 以 PDF 格式呈現,用于在 Angular 中顯示

二、應用程序架構概述

設想您和團隊正在致力于將幾個老舊的ASP.NET Framework Web應用程序進行現代化升級。目標技術棧是構建單頁面應用(SPA),前端使用Angular,后端則由ASP.NET Web API提供支持。這些舊應用程序包含了數百份Crystal報表。您的任務是將這些報表遷移到新的平臺上。然而,您發現Crystal Report無法在ASP.NET CORE的新環境中運行,這確實是一個意外

如圖2所示,該架構描述了Angular應用通過REST API訪問Crystal Report的方式。Crystal Report以PDF格式呈現,并通過流式傳輸的方式發送到Angular,以便在Web瀏覽器中展示。在Angular前端,我們將使用angular pdf 查看器來渲染這些PDF文件。angular pdf 查看器是一個強大的工具,它能夠無縫地集成到我們的SPA中,并且提供豐富的功能,如分頁、縮放、打印以及保存到本地等。

通過這種方式,用戶可以在Angular應用中直接查看和操作PDF格式的Crystal Report報表,而無需進行任何額外的下載或安裝。angular pdf 查看器的集成確保了報表的可訪問性和易用性,同時保持了現代化的用戶界面。

在實現過程中,后端的ASP.NET Framework 4.7 WebAPI 2將負責生成Crystal Report的PDF版本,并通過REST API將這些PDF文件以流的形式傳輸到前端。前端的Angular應用將使用angular pdf 查看器來接收這些流,并在用戶的瀏覽器中展示PDF內容。

總的來說,通過結合ASP.NET Framework 4.7 WebAPI 2和angular pdf 查看器,我們能夠以一種經濟高效的方式將老舊的Crystal Report報表遷移到新的技術棧上。這種方法不僅避免了昂貴的重寫成本,還確保了報表的現代化展示和用戶友好的交互體驗。

圖 2—Angular 應用程序和 Web API 的架構

1、Github 存儲庫

本教程的完整源代碼可在 GitHub 上找到。前端和后端代碼組織在單獨的項目中。這使得維護和部署到微服務架構中更加容易。

  1. workcontrolgit/CrystalReportWebAPI是用 ASP.NET Framework 4.7/Web API 2 編寫的后端 REST API。它以 PDF 格式呈現 Crystal Report,并可通過 REST API HTTP Get 方法訪問。自定義操作過濾器用于緩存報告以提高性能。
  2. workcontrolgit/AngularCrytalReportUI是前端 Angular v12 應用。它有一個儀表板,鏈接到由 Crystal Report 生成的四份 PDF 格式的財務報告。

2、前提條件

建議使用以下工具/技能。

  1. Visual Code ——Angular 的免費代碼編輯器
  2. Visual Studio 2019 Community — C# 的免費代碼編輯器

三、教程內容

本教程重點介紹以下編程技術

  1. 在后端 REST API 中將水晶報表渲染為 PDF 格式
  2. 將 PDF 數據文件傳輸到 Angular 前端以在瀏覽器中顯示

本教程內容由以下幾個部分組成:

第 1 部分: Git-Clone 后端 Crystal Report REST API 和代碼演練將 Asp.Net WebAPI 項目克隆到本地并檢查與將 Crystal Report 導出為 PDF 進行流式傳輸相關的代碼。

第 2 部分: Git-Clone 前端 Angular 應用程序和代碼演練 克隆 Angular 應用程序并演練 PDF 查看庫 ngx-extended-pdf-viewer 的使用代碼。

第 3 部分: 測試驅動應用程序 在本地主機上運行 Angular 和 Web API 并試用 Crystal Report。

第 1 部分: Git-Clone 后端 Crystal Report REST API 和代碼演練

任務 1.1 – Git-Clone 后端 Crystal Report REST API

使用 Visual Studio 2019,我們將從 Github 克隆 Web API 源代碼項目。按照以下步驟下載源代碼(參見圖 3 的視覺輔助):

  1. 啟動 Visual Studio 2019 并選擇選項“克隆存儲庫”
  2. 將 repo https://github.com/workcontrolgit/CrystalReportWebAPI克隆到 C:\apps\devkit\ApiResources\CrystalReportWebAPI

圖 3—從 Github 克隆 Web API 源代碼

在解決方案資源管理器中,您應該看到 CrystalReportWebAPI 項目。單擊 CrystalReportWebAPI 項目并注意 SSL URL。該 URL 應設置為 https://localhost:44369。這是 Angular 應用將調用以獲取報告的 REST API 服務器的地址。請參閱圖 4 以獲取解決方案資源管理器中項目的屏幕截圖。

圖 4—Visual Studio 解決方案資源管理器中的 CrystalReportWebAPI 項目

任務 1.2 — Crystal Report REST API 的代碼演練

在本節中,我們將查看三個文件中的源代碼,這些文件負責將 Crystal Report 渲染為 PDF 以供流式傳輸。這些文件超鏈接到存儲庫,以便您知道項目中的文件位置。

  1. ReportsController.cs— 通過 HTTP Get 公開 Crystal Report 的 Web API 控制器。請參閱圖 5 中的源代碼列表。
  2. CrystalReport.cs— 將 Crystal Report 導出為 PDF 并將響應標頭設置為 application/pdf 的源代碼。請參閱圖 6 中的源代碼列表。
  3. ClientCacheWithEtagAttribute.cs— 使用操作屬性緩存 Crystal Report 的源代碼。請參閱圖 7 中的源代碼列表。
using CrystalReportWebAPI.Utilities;
using System.Net.Http;
using System.Web.Http;

namespace CrystalReportWebAPI.Controllers
{
[RoutePrefix("api/Reports")]
public class ReportsController : ApiController
{
[AllowAnonymous]
[Route("Financial/VarianceAnalysisReport")]
[HttpGet]
[ClientCacheWithEtag(60)] //1 min client side caching
public HttpResponseMessage FinancialVarianceAnalysisReport()
{
string reportPath = "~/Reports/Financial";
string reportFileName = "YTDVarianceCrossTab.rpt";
string exportFilename = "YTDVarianceCrossTab.pdf";

HttpResponseMessage result = CrystalReport.RenderReport(reportPath, reportFileName, exportFilename);
return result;
}

[AllowAnonymous]
[Route("Demonstration/ComparativeIncomeStatement")]
[HttpGet]
[ClientCacheWithEtag(60)] //1 min client side caching
public HttpResponseMessage DemonstrationComparativeIncomeStatement()
{
string reportPath = "~/Reports/Demonstration";
string reportFileName = "ComparativeIncomeStatement.rpt";
string exportFilename = "ComparativeIncomeStatement.pdf";

HttpResponseMessage result = CrystalReport.RenderReport(reportPath, reportFileName, exportFilename);
return result;
}

[AllowAnonymous]
[Route("VersatileandPrecise/Invoice")]
[HttpGet]
[ClientCacheWithEtag(60)] //1 min client side caching
public HttpResponseMessage VersatileandPreciseInvoice()
{
string reportPath = "~/Reports/VersatileandPrecise";
string reportFileName = "Invoice.rpt";
string exportFilename = "Invoice.pdf";

HttpResponseMessage result = CrystalReport.RenderReport(reportPath, reportFileName, exportFilename);
return result;
}

[AllowAnonymous]
[Route("VersatileandPrecise/FortifyFinancialAllinOneRetirementSavings")]
[HttpGet]
[ClientCacheWithEtag(60)] //1 min client side caching
public HttpResponseMessage VersatileandPreciseFortifyFinancialAllinOneRetirementSavings()
{
string reportPath = "~/Reports/VersatileandPrecise";
string reportFileName = "FortifyFinancialAllinOneRetirementSavings.rpt";
string exportFilename = "FortifyFinancialAllinOneRetirementSavings.pdf";

HttpResponseMessage result = CrystalReport.RenderReport(reportPath, reportFileName, exportFilename);

return result;
}
}
}

圖 5 — ReportController.cs 源代碼列表

using CrystalDecisions.CrystalReports.Engine;
using CrystalDecisions.Shared;
using System.IO;
using System.Net;
using System.Net.Http;

namespace CrystalReportWebAPI.Utilities
{
public static class CrystalReport
{
public static HttpResponseMessage RenderReport(string reportPath, string reportFileName, string exportFilename)
{
var rd = new ReportDocument();

rd.Load(Path.Combine(System.Web.Hosting.HostingEnvironment.MapPath(reportPath), reportFileName));
MemoryStream ms = new MemoryStream();
using (var stream = rd.ExportToStream(ExportFormatType.PortableDocFormat))
{
stream.CopyTo(ms);
}

var result = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent(ms.ToArray())
};
result.Content.Headers.ContentDisposition =
new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
{
FileName = exportFilename
};
result.Content.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("application/pdf");
return result;
}
}
}

圖 6 — CrystalReport.cs 源代碼清單

using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;

namespace CrystalReportWebAPI.Utilities
{
/// <summary>
/// Enables HTTP Response CacheControl management with ETag values.
/// How to use ETag in Web API using action filter along with HttpResponseMessage
/// https://stackoverflow.com/questions/20145140/how-to-use-etag-in-web-api-using-action-filter-along-with-httpresponsemessage/49169225#49169225
/// </summary>
public class ClientCacheWithEtagAttribute : ActionFilterAttribute
{
private readonly TimeSpan _clientCache;

private readonly HttpMethod[] _supportedRequestMethods = {
HttpMethod.Get,
HttpMethod.Head
};

/// <summary>
/// Default constructor
/// </summary>
/// <param name="clientCacheInSeconds">Indicates for how long the client should cache the response. The value is in seconds</param>
public ClientCacheWithEtagAttribute(int clientCacheInSeconds)
{
_clientCache = TimeSpan.FromSeconds(clientCacheInSeconds);
}

public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
{
if (!_supportedRequestMethods.Contains(actionExecutedContext.Request.Method))
{
return;
}
if (actionExecutedContext.Response?.Content == null)
{
return;
}

var body = await actionExecutedContext.Response.Content.ReadAsStringAsync();
if (body == null)
{
return;
}

var computedEntityTag = GetETag(Encoding.UTF8.GetBytes(body));

if (actionExecutedContext.Request.Headers.IfNoneMatch.Any()
&& actionExecutedContext.Request.Headers.IfNoneMatch.First().Tag.Trim('"').Equals(computedEntityTag, StringComparison.InvariantCultureIgnoreCase))
{
actionExecutedContext.Response.StatusCode = HttpStatusCode.NotModified;
actionExecutedContext.Response.Content = null;
}

var cacheControlHeader = new CacheControlHeaderValue
{
Private = true,
MaxAge = _clientCache
};

actionExecutedContext.Response.Headers.ETag = new EntityTagHeaderValue($"\"{computedEntityTag}\"", false);
actionExecutedContext.Response.Headers.CacheControl = cacheControlHeader;
}

private static string GetETag(byte[] contentBytes)
{
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(contentBytes);
string hex = BitConverter.ToString(hash);
return hex.Replace("-", "");
}
}
}
}

圖 7 — ClientCacheWithEtagAttribute.cs 源代碼列表

第 2 部分: Git-Clone 前端 Angular 應用程序和代碼演練

任務 2.1 — Git-Clone 前端 Angular 應用程序

在本部分教程中,我們將從 GitHub git-clone Angular 應用程序并運行 npm install 下載節點模塊。在進行克隆之前,請確保在桌面上創建一個文件夾 C:\apps\devkit\Clients, 用于存儲 Angular 源代碼。

  1. 啟動可視化代碼
  2. 轉到菜單“View”>“Command Pallet”(或 Ctrl-Shift-P)。請參閱圖 7 以獲得視覺輔助。
  3. 輸入“Clone”并選擇 Git: Clone。參見圖 x 的視覺輔助
  4. 當提示 提供存儲庫 URL 或選擇存儲庫源時,輸入 https://github.com/workcontrolgit/AngularCrystalReportUI。 請參閱圖 8 中的視覺輔助。
  5. 選擇文件夾 C:\apps\devkit\Clients 保存源代碼。
  6. 轉到菜單“視圖”>“終端”(或 Ctrl + ‘),然后在命令行中輸入 npm i ,然后按 Enter。恢復 NPM 包可能需要幾分鐘,具體取決于您的網絡帶寬。請參閱圖 9 了解視覺輔助。

圖 7 — 帶有克隆 repo 選項的 Visual Code

圖 8 — Visual Code 提示保存源文件位置

圖 9 — 通過命令 npm i (安裝)恢復 NPM 包

任務 2.2 — 前端 Angular 應用的代碼演練

以下是支持在 angular pdf 查看器中以 PDF 形式呈現的 Crystal Report 顯示的主要源代碼文件

  1. environment.ts — 具有 webapi 服務器的設置。參見圖 10。
  2. report.service.ts — 具有使用 Angular HTTP 客戶端調用 Web API 的方法。參見圖 11。
  3. saving.module.ts — 包含客戶 PDF 查看器的設置。參見圖 12。
  4. saving.component.html — 包含顯示 pdf 的代碼。參見圖 13。
  5. saving.component.ts — 包含調用報告服務器 WebAPI 的代碼。參見圖 14。
// This file can be replaced during build by using the fileReplacements array.
// ng build --prod replaces environment.ts with environment.prod.ts.
// The list of file replacements can be found in angular.json.

// .env.ts is generated by the npm run env command
// npm run env exposes environment variables as JSON for any usage you might
// want, like displaying the version or getting extra config from your CI bot, etc.
// This is useful for granularity you might need beyond just the environment.
// Note that as usual, any environment variables you expose through it will end up in your
// bundle, and you should not use it for any sensitive information like passwords or keys.
import { env } from './.env';

export const environment = {
  production: false,
  version: env.npm_package_version + '-dev',
  serverUrl: '/api',
  defaultLanguage: 'en-US',
  supportedLanguages: ['en-US'],
  reportServer: 'https://localhost:44369'
};

/*
 * For easier debugging in development mode, you can import the following file
 * to ignore zone related error stack frames such as zone.run, zoneDelegate.invokeTask.
 *
 * This import should be commented out in production mode because it will have a negative impact
 * on performance if an error is thrown.
 */
// import 'zone.js/dist/zone-error';  // Included with Angular CLI.

圖 10— Environment.ts 文件中的 reportServer URL 設置圖

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '@env/environment';

@Injectable({
providedIn: 'root'
})
export class ReportService {

reportServer: string | null = environment.reportServer;
srvURL: string = "";

constructor(private httpClient: HttpClient) { }

getInvoice(): Observable<any> {
this.srvURL = this.reportServer + '/api/Reports/VersatileandPrecise/Invoice';

return this.httpClient.get(this.srvURL, {responseType: "blob"});
}

getSaving(): Observable<any> {
this.srvURL = this.reportServer + '/api/Reports/VersatileandPrecise/FortifyFinancialAllinOneRetirementSavings';

return this.httpClient.get(this.srvURL, {responseType: "blob"});
}

getFinancial(): Observable<any> {
this.srvURL = this.reportServer + '/api/Reports/Financial/VarianceAnalysisReport';

return this.httpClient.get(this.srvURL, {responseType: "blob"});
}
getIncome(): Observable<any> {
this.srvURL = this.reportServer + '/api/Reports/Demonstration/ComparativeIncomeStatement';

return this.httpClient.get(this.srvURL, {responseType: "blob"});
}

}

11 — report.service.ts 文件中的源代碼列表

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';

import { SavingRoutingModule } from './saving-routing.module';
import { SavingComponent } from './saving.component';
import { NgxExtendedPdfViewerModule } from 'ngx-extended-pdf-viewer';

@NgModule({
imports: [CommonModule, TranslateModule, SavingRoutingModule, NgxExtendedPdfViewerModule],
declarations: [SavingComponent],
})
export class SavingModule {}

圖 12 — saving.module.ts 文件中的源代碼列表

<div class="container-fluid">
<div class="jumbotron text-center">
<h1>
<span translate>Retirement Savings Report</span>
</h1>
</div>
<div class="container">
<ngx-extended-pdf-viewer [src]="pdfSource" [useBrowserLocale]="true"> </ngx-extended-pdf-viewer>
</div>
</div>

圖 13 — saving.component.html 文件中的源代碼列表

import { Component, OnInit } from '@angular/core';
import {ReportService} from '@app/services/report.service';

@Component({
selector: 'app-saving',
templateUrl: './saving.component.html',
styleUrls: ['./saving.component.scss'],
})
export class SavingComponent implements OnInit {

pdfSource: any;
constructor(private reportService: ReportService) {}

ngOnInit() {
this.reportService.getSaving()
.subscribe(data => {this.pdfSource = data;
});
}
}

圖 14 — saving.component.ts 文件中的源代碼列表

第 3 部分: 測試應用程序

要運行應用程序,請首先啟動 WebAPI 解決方案,然后啟動 angular pdf 查看器。

3.1 運行WebAPI

在 Visual Studio 中,按 F5 運行解決方案。您應該看到項目正在運行,如圖 15 所示。單擊菜單 Swagger 以查看 Web API資源。

圖 15 — WebAPI 項目正在運行

在 Swagger 屏幕中,單擊資源報告以查看四個端點。請參閱圖 16 以獲得視覺輔助。

圖 16 — WebAPI Swagger 顯示資源報告和四 (4) 個端點

3.2 運行 Angular 應用

要運行angular pdf 查看器請從 Visual Code > Terminal 屏幕運行 ng serve -o 以在瀏覽器中自動打開 Angular 應用。您應該會看到主頁,其中包含一個用于訪問報告的儀表板,如圖 17 所示 。

圖 17 — Angular 應用儀表板

單擊“保存”鏈接可以查看 PDF 報告,如圖 18 所示。

圖 18— 渲染為 PDF 并在 Angular 中顯示的示例 Crystal Report

原文鏈接:https://medium.com/scrum-and-coke/view-crystal-report-in-pdf-with-angular-and-asp-net-rest-api-1d6c72168e7c

上一篇:

最大化谷歌新聞曝光度:利用SEO和API進行追蹤

下一篇:

優化AI驅動的Java應用程序API管理策略
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費