
通過 Python 使用 當當開放平臺 API 實現書籍推薦系統
圖 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 的架構
本教程的完整源代碼可在 GitHub 上找到。前端和后端代碼組織在單獨的項目中。這使得維護和部署到微服務架構中更加容易。
建議使用以下工具/技能。
本教程重點介紹以下編程技術
本教程內容由以下幾個部分組成:
第 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。
使用 Visual Studio 2019,我們將從 Github 克隆 Web API 源代碼項目。按照以下步驟下載源代碼(參見圖 3 的視覺輔助):
圖 3—從 Github 克隆 Web API 源代碼
在解決方案資源管理器中,您應該看到 CrystalReportWebAPI 項目。單擊 CrystalReportWebAPI 項目并注意 SSL URL。該 URL 應設置為 https://localhost:44369。這是 Angular 應用將調用以獲取報告的 REST API 服務器的地址。請參閱圖 4 以獲取解決方案資源管理器中項目的屏幕截圖。
圖 4—Visual Studio 解決方案資源管理器中的 CrystalReportWebAPI 項目
在本節中,我們將查看三個文件中的源代碼,這些文件負責將 Crystal Report 渲染為 PDF 以供流式傳輸。這些文件超鏈接到存儲庫,以便您知道項目中的文件位置。
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 源代碼列表
在本部分教程中,我們將從 GitHub git-clone Angular 應用程序并運行 npm install 下載節點模塊。在進行克隆之前,請確保在桌面上創建一個文件夾 C:\apps\devkit\Clients, 用于存儲 Angular 源代碼。
圖 7 — 帶有克隆 repo 選項的 Visual Code
圖 8 — Visual Code 提示保存源文件位置
圖 9 — 通過命令 npm i (安裝)恢復 NPM 包
以下是支持在 angular pdf 查看器中以 PDF 形式呈現的 Crystal Report 顯示的主要源代碼文件
// 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 文件中的源代碼列表
要運行應用程序,請首先啟動 WebAPI 解決方案,然后啟動 angular pdf 查看器。
在 Visual Studio 中,按 F5 運行解決方案。您應該看到項目正在運行,如圖 15 所示。單擊菜單 Swagger 以查看 Web API資源。
圖 15 — WebAPI 項目正在運行
在 Swagger 屏幕中,單擊資源報告以查看四個端點。請參閱圖 16 以獲得視覺輔助。
圖 16 — WebAPI Swagger 顯示資源報告和四 (4) 個端點
要運行angular pdf 查看器請從 Visual Code > Terminal 屏幕運行 ng serve -o 以在瀏覽器中自動打開 Angular 應用。您應該會看到主頁,其中包含一個用于訪問報告的儀表板,如圖 17 所示 。
圖 17 — Angular 應用儀表板
單擊“保存”鏈接可以查看 PDF 報告,如圖 18 所示。
圖 18— 渲染為 PDF 并在 Angular 中顯示的示例 Crystal Report